clientip

package module
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 18, 2026 License: MIT Imports: 10 Imported by: 0

README

clientip

CI Go Reference License

Secure client IP extraction for net/http and framework-agnostic request inputs with trusted proxy validation, configurable source priority, and optional logging/metrics.

Stability

This project is pre-v1.0.0 and still before v0.1.0, so public APIs may change as the package evolves. Any breaking changes will be called out in CHANGELOG.md.

Install

go get github.com/abczzz13/clientip

Optional Prometheus adapter:

go get github.com/abczzz13/clientip/prometheus
import "github.com/abczzz13/clientip"

Compatibility

  • Core module (github.com/abczzz13/clientip) supports Go 1.21+.
  • Optional Prometheus adapter (github.com/abczzz13/clientip/prometheus) has a minimum Go version of 1.21; CI currently validates consumer mode on Go 1.21.x and 1.26.x.
  • Prometheus client dependency in the adapter is pinned to github.com/prometheus/client_golang v1.21.1.

Quick start

By default, New() extracts from RemoteAddr only.

Use these when you want setup by deployment type instead of low-level options:

  • PresetDirectConnection() app receives traffic directly (no trusted proxy headers)
  • PresetLoopbackReverseProxy() reverse proxy on same host (127.0.0.1 / ::1)
  • PresetVMReverseProxy() typical VM/private-network reverse proxy setup
  • PresetPreferredHeaderThenXFFLax("X-Frontend-IP") prefer custom header, then X-Forwarded-For, then RemoteAddr (lax fallback)
Which preset should I use?
If your setup looks like... Start with...
App is directly internet-facing (no reverse proxy) PresetDirectConnection()
NGINX/Caddy runs on the same host and proxies to your app PresetLoopbackReverseProxy()
App runs on a VM/private network behind one or more internal proxies PresetVMReverseProxy()
You have a best-effort custom header and want fallback to XFF PresetPreferredHeaderThenXFFLax("X-Frontend-IP")

Preset examples:

// Typical VM setup (reverse proxy + private networking)
vmExtractor, err := clientip.New(
    clientip.PresetVMReverseProxy(),
)

// Prefer a best-effort header, then fallback to XFF and RemoteAddr
fallbackExtractor, err := clientip.New(
    clientip.TrustLoopbackProxy(),
    clientip.PresetPreferredHeaderThenXFFLax("X-Frontend-IP"),
)

_ = vmExtractor
_ = fallbackExtractor
Simple (no proxy configuration)
extractor, err := clientip.New()
if err != nil {
    log.Fatal(err)
}

ip, err := extractor.ExtractAddr(req)
if err != nil {
    fmt.Printf("Failed: %v\n", err)
    return
}

fmt.Printf("Client IP: %s\n", ip)
Framework-friendly input API

Use ExtractFrom when your framework does not expose *http.Request directly.

input := clientip.RequestInput{
    Context:    ctx,
    RemoteAddr: remoteAddr,
    Path:       path,
    Headers:    headersProvider, // any type implementing Values(name string) []string
}

extraction, err := extractor.ExtractFrom(input)
if err != nil {
    // handle error
}

http.Header already implements the required header interface, so for net/http style frameworks (Gin, Echo, Chi) you can keep using Extract(req) directly.

ExtractFrom only requests header names required by the configured Priority(...) sources.

// Gin
extraction, err := extractor.Extract(c.Request)

// Echo
extraction, err := extractor.Extract(c.Request())

For fasthttp/Fiber style frameworks, provide a header adapter with HeaderValuesFunc and preserve duplicate header lines:

input := clientip.RequestInput{
    Context:    c.UserContext(),
    RemoteAddr: c.Context().RemoteAddr().String(),
    Path:       c.Path(),
    Headers: clientip.HeaderValuesFunc(func(name string) []string {
        raw := c.Context().Request.Header.PeekAll(name)
        if len(raw) == 0 {
            return nil
        }

        values := make([]string, len(raw))
        for i, v := range raw {
            values[i] = string(v)
        }
        return values
    }),
}

Important: do not merge repeated header lines into a single comma-joined value. Single-IP sources (for example X-Real-IP or custom headers) rely on per-line values to detect duplicates in strict mode.

Behind reverse proxies
cidrs, err := clientip.ParseCIDRs("10.0.0.0/8", "172.16.0.0/12")
if err != nil {
    log.Fatal(err)
}

extractor, err := clientip.New(
    // min=0 allows requests where proxy headers contain only the client IP
    // (trusted RemoteAddr is validated separately).
    clientip.TrustProxyPrefixes(cidrs...),
    clientip.MinTrustedProxies(0),
    clientip.MaxTrustedProxies(3),
    clientip.Priority(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
    clientip.WithChainSelection(clientip.RightmostUntrustedIP),
)
if err != nil {
    log.Fatal(err)
}
Custom header priority
extractor, err := clientip.New(
    clientip.TrustPrivateProxyRanges(),
    clientip.Priority(
        "CF-Connecting-IP",
        clientip.SourceXForwardedFor,
        clientip.SourceRemoteAddr,
    ),
)
Security mode (strict vs lax)
// Strict is default and fails closed on security errors
// (including malformed Forwarded and invalid present source values).
strictExtractor, _ := clientip.New(
    clientip.TrustProxyAddrs(netip.MustParseAddr("1.1.1.1")),
    clientip.Priority("X-Frontend-IP", clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
    clientip.WithSecurityMode(clientip.SecurityModeStrict),
)

// Lax mode allows fallback to lower-priority sources after those errors.
laxExtractor, _ := clientip.New(
    clientip.TrustProxyAddrs(netip.MustParseAddr("1.1.1.1")),
    clientip.Priority("X-Frontend-IP", clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
    clientip.WithSecurityMode(clientip.SecurityModeLax),
)
Logging (bring your own)

By default, logging is disabled. Use WithLogger to opt in.

WithLogger accepts any implementation of:

type Logger interface {
    WarnContext(context.Context, string, ...any)
}

This intentionally mirrors slog.Logger.WarnContext, so *slog.Logger works directly with WithLogger (no adapter needed).

The context passed to logger calls comes from req.Context() (Extract) or RequestInput.Context (ExtractFrom), so trace/span IDs added by middleware remain available in logs.

Structured log attributes are passed as alternating key/value pairs, matching the style used by slog.

When configured, the extractor emits warning logs for security-significant conditions such as multiple_headers, malformed_forwarded, chain_too_long, untrusted_proxy, no_trusted_proxies, too_few_trusted_proxies, and too_many_trusted_proxies.

extractor, err := clientip.New(
    clientip.WithLogger(slog.Default()),
)

For loggers without context-aware APIs, adapters can simply ignore ctx:

type stdLoggerAdapter struct{ l *log.Logger }

func (a stdLoggerAdapter) WarnContext(_ context.Context, msg string, args ...any) {
    a.l.Printf("WARN %s %v", msg, args)
}

extractor, err := clientip.New(
    clientip.WithLogger(stdLoggerAdapter{l: log.Default()}),
)

Tiny adapters for other popular loggers:

type zapAdapter struct{ l *zap.SugaredLogger }

func (a zapAdapter) WarnContext(_ context.Context, msg string, args ...any) {
    a.l.With(args...).Warn(msg)
}
type logrusAdapter struct{ l *logrus.Logger }

func (a logrusAdapter) WarnContext(_ context.Context, msg string, args ...any) {
    fields := logrus.Fields{}
    for i := 0; i+1 < len(args); i += 2 {
        key, ok := args[i].(string)
        if !ok {
            continue
        }
        fields[key] = args[i+1]
    }
    a.l.WithFields(fields).Warn(msg)
}
type zerologAdapter struct{ l zerolog.Logger }

func (a zerologAdapter) WarnContext(_ context.Context, msg string, args ...any) {
    event := a.l.Warn()
    for i := 0; i+1 < len(args); i += 2 {
        key, ok := args[i].(string)
        if !ok {
            continue
        }
        event = event.Interface(key, args[i+1])
    }
    event.Msg(msg)
}

If your stack stores trace metadata in context.Context, enrich the adapter by extracting that value and appending it to args.

Prometheus metrics (simple setup)
import clientipprom "github.com/abczzz13/clientip/prometheus"

extractor, err := clientip.New(
    clientipprom.WithMetrics(),
)
Prometheus metrics (custom registerer)
import (
    clientipprom "github.com/abczzz13/clientip/prometheus"
    "github.com/prometheus/client_golang/prometheus"
)

registry := prometheus.NewRegistry()

extractor, err := clientip.New(
    clientipprom.WithRegisterer(registry),
)

You can also construct metrics explicitly with clientipprom.New() or clientipprom.NewWithRegisterer(...) and pass them via clientip.WithMetrics(...).

Options

New(opts...) accepts one or more Option builders.

For one-shot extraction without reusing an extractor, use:

  • ExtractWithOptions(req, opts...)

  • ExtractAddrWithOptions(req, opts...)

  • ExtractFromWithOptions(input, opts...)

  • ExtractAddrFromWithOptions(input, opts...)

  • TrustProxyPrefixes(...netip.Prefix) add trusted proxy network prefixes

  • TrustLoopbackProxy() trust loopback upstream proxies (127.0.0.0/8, ::1/128)

  • TrustPrivateProxyRanges() trust private upstream proxy ranges (10/8, 172.16/12, 192.168/16, fc00::/7)

  • TrustLocalProxyDefaults() trust loopback + private proxy ranges

  • TrustProxyAddrs(...netip.Addr) add trusted upstream proxy host addresses

  • PresetDirectConnection() remote-address only extraction preset

  • PresetLoopbackReverseProxy() loopback reverse-proxy preset (X-Forwarded-For, then RemoteAddr)

  • PresetVMReverseProxy() VM/private-network reverse-proxy preset (X-Forwarded-For, then RemoteAddr)

  • PresetPreferredHeaderThenXFFLax(string) preferred-header fallback preset in lax mode

  • MinTrustedProxies(int) / MaxTrustedProxies(int) set trusted-proxy count bounds for chain headers

  • AllowPrivateIPs(bool) allow private client IPs

  • AllowReservedClientPrefixes(...netip.Prefix) explicitly allow selected reserved/special-use client ranges

  • ParseCIDRs(...string) parse CIDR strings to []netip.Prefix for typed options

  • MaxChainLength(int) limit proxy chain length from Forwarded/X-Forwarded-For (default 100)

  • WithChainSelection(ChainSelection) choose RightmostUntrustedIP (default) or LeftmostUntrustedIP

  • Priority(...string) set source order; built-ins: SourceForwarded, SourceXForwardedFor, SourceXRealIP, SourceRemoteAddr (built-in aliases are canonicalized, e.g. "Forwarded", "X-Forwarded-For", "X_Real_IP", "Remote-Addr"), with at most one chain header source (SourceForwarded or SourceXForwardedFor) per extractor

  • WithSecurityMode(SecurityMode) choose SecurityModeStrict (default) or SecurityModeLax

  • WithLogger(Logger) inject logger implementation

  • WithMetrics(Metrics) inject custom metrics implementation directly

  • WithMetricsFactory(func() (Metrics, error)) lazily construct metrics after option validation (last metrics option wins)

  • WithDebugInfo(bool) include chain analysis in Extraction.DebugInfo

Default source order is SourceRemoteAddr.

Any header-based source requires trusted upstream proxy ranges (TrustProxyPrefixes or one of the trust helpers).

Prometheus adapter helpers from github.com/abczzz13/clientip/prometheus:

  • WithMetrics() install Prometheus metrics on default registerer
  • WithRegisterer(prometheus.Registerer) install Prometheus metrics on custom registerer
  • New() / NewWithRegisterer(prometheus.Registerer) for explicit metrics construction

Proxy count bounds (min/max) apply to trusted proxies present in Forwarded (from for= values) and X-Forwarded-For. The immediate proxy (RemoteAddr) is validated for trust separately before either header is trusted.

AllowReservedClientPrefixes only bypasses reserved/special-use filtering for matching ranges. It does not bypass loopback/link-local/multicast/unspecified rejection, and private-IP policy remains controlled by AllowPrivateIPs.

Extraction

type Extraction struct {
    IP                netip.Addr
    Source            string // "forwarded", "x_forwarded_for", "x_real_ip", "remote_addr", or normalized custom header
    TrustedProxyCount int
    DebugInfo         *ChainDebugInfo
}

type HeaderValues interface {
    Values(name string) []string
}

type RequestInput struct {
    Context    context.Context
    RemoteAddr string
    Path       string
    Headers    HeaderValues
}

func (e *Extractor) Extract(req *http.Request, overrides ...OverrideOptions) (Extraction, error)
func (e *Extractor) ExtractAddr(req *http.Request, overrides ...OverrideOptions) (netip.Addr, error)
func (e *Extractor) ExtractFrom(input RequestInput, overrides ...OverrideOptions) (Extraction, error)
func (e *Extractor) ExtractAddrFrom(input RequestInput, overrides ...OverrideOptions) (netip.Addr, error)

When Extract returns a non-nil error, the returned Extraction value is best-effort metadata only (typically Source when available). For chain diagnostics, inspect typed errors like ProxyValidationError and InvalidIPError.

Per-call overrides let you temporarily adjust policy for a single extraction:

extraction, err := extractor.Extract(
    req,
    clientip.OverrideOptions{
        SecurityMode: clientip.Set(clientip.SecurityModeLax),
    },
)

Multiple OverrideOptions values are merged left-to-right; later set values win. Only policy fields are overrideable (logger and metrics stay fixed per extractor instance).

Custom header names are normalized via NormalizeSourceName (lowercase with underscores).

Errors

_, err := extractor.Extract(req)
if err != nil {
    switch {
    case errors.Is(err, clientip.ErrMultipleSingleIPHeaders):
        // Duplicate single-IP header values received
    case errors.Is(err, clientip.ErrInvalidForwardedHeader):
        // Malformed Forwarded header
    case errors.Is(err, clientip.ErrUntrustedProxy):
        // Forwarded/XFF came from an untrusted immediate proxy
    case errors.Is(err, clientip.ErrNoTrustedProxies):
        // No trusted proxies found in the chain
    case errors.Is(err, clientip.ErrTooFewTrustedProxies):
        // Trusted proxy count is below configured minimum
    case errors.Is(err, clientip.ErrTooManyTrustedProxies):
        // Trusted proxy count exceeds configured maximum
    case errors.Is(err, clientip.ErrInvalidIP):
        // Invalid or implausible client IP
    case errors.Is(err, clientip.ErrSourceUnavailable):
        // Requested source was not present on this request
    }

    var mh *clientip.MultipleHeadersError
    if errors.As(err, &mh) {
        // Inspect mh.HeaderName, mh.HeaderCount, or mh.RemoteAddr
    }
}

Typed chain-related errors expose additional context:

  • ProxyValidationError: Chain, TrustedProxyCount, MinTrustedProxies, MaxTrustedProxies
  • InvalidIPError: Chain, ExtractedIP, Index, TrustedProxies

Security notes

  • Parses RFC7239 Forwarded header (for= chain) and rejects malformed values
  • Parses multiple X-Forwarded-For header lines as one chain (wire order preserved)
  • Rejects multiple values for single-IP headers (for example repeated X-Real-IP)
  • Requires the immediate proxy (RemoteAddr) to be trusted before honoring Forwarded or X-Forwarded-For (when trusted proxy prefixes are configured)
  • Requires trusted proxy prefixes for any header-based source
  • Allows at most one chain-header source (Forwarded or X-Forwarded-For) per extractor configuration
  • Enforces trusted proxy count bounds and chain length
  • Filters implausible IPs (loopback, multicast, reserved); optional private-IP and reserved-prefix allowlists
  • Strict fail-closed behavior is the default (SecurityModeStrict) for security-significant errors and invalid present source values
  • Set WithSecurityMode(SecurityModeLax) to continue fallback after security errors

Security anti-patterns

  • Do not include multiple competing header-based sources in Priority(...) for security decisions (for example custom header + chain header fallback). Prefer one canonical trusted header plus SourceRemoteAddr fallback only when required.
  • Do not enable SecurityModeLax for security-enforcement decisions (ACLs, fraud/risk controls, authz). Use strict mode and fail closed.
  • Do not trust broad proxy CIDRs unless they are truly under your control. Keep trusted ranges minimal and explicit.
  • Do not treat a missing/invalid source as benign in critical paths; monitor and remediate extraction errors.

Performance

  • O(n) in chain length; extractor is safe for concurrent reuse

Benchmark workflow with just:

# Capture a stable baseline (6 samples by default)
just bench-save before "BenchmarkExtract|BenchmarkChainAnalysis|BenchmarkParseIP"

# Make changes, then capture again
just bench-save after "BenchmarkExtract|BenchmarkChainAnalysis|BenchmarkParseIP"

# Compare with benchstat table output (delta + significance)
just bench-compare-saved before after

You can compare arbitrary files directly via just bench-compare <before-file> <after-file>.

Maintainer notes (multi-module)

  • prometheus/go.mod intentionally does not use a local replace directive for github.com/abczzz13/clientip.
  • For local co-development, create an uncommitted workspace with go work init . ./prometheus.
  • Validate the adapter as a consumer with GOWORK=off go -C prometheus test ./....
  • just and CI validate the adapter in consumer mode by default (GOWORK=off); set CLIENTIP_ADAPTER_GOWORK=auto locally when you intentionally want workspace-mode adapter checks.
  • Release in this order: tag root module vX.Y.Z, bump prometheus/go.mod to that version, then tag adapter module prometheus/vX.Y.Z.

License

See LICENSE.

Documentation

Overview

Package clientip provides secure client IP extraction from HTTP requests and framework-agnostic request inputs with support for proxy chains, trusted proxy validation, and multiple header sources.

Features

  • Security-first design with protection against IP spoofing and header injection
  • Flexible proxy configuration with min/max trusted proxy ranges in proxy chains
  • Multiple source support: Forwarded, X-Forwarded-For, X-Real-IP, RemoteAddr, custom headers
  • Framework-friendly RequestInput API for non-net/http integrations
  • Safe defaults: RemoteAddr-only unless header sources are explicitly configured
  • Deployment presets for common topologies (direct, loopback proxy, VM proxy)
  • Optional observability with context-aware logging and pluggable metrics
  • Type-safe using modern Go netip.Addr

Basic Usage

Simple extraction without proxy configuration:

extractor, err := clientip.New()
if err != nil {
    log.Fatal(err)
}

extraction, err := extractor.Extract(req)
if err != nil {
    log.Printf("extract failed: %v", err)
    return
}

fmt.Printf("Client IP: %s from %s\n", extraction.IP, extraction.Source)

Framework-agnostic input is available via ExtractFrom:

extraction, err := extractor.ExtractFrom(clientip.RequestInput{
    Context:    ctx,
    RemoteAddr: remoteAddr,
    Path:       path,
    Headers:    headerProvider,
})

Behind Reverse Proxy

Configure trusted proxy prefixes with flexible min/max proxy count:

cidrs, _ := clientip.ParseCIDRs("10.0.0.0/8", "172.16.0.0/12")
extractor, err := clientip.New(
    clientip.TrustProxyPrefixes(cidrs...), // Trust upstream proxy ranges
    clientip.MinTrustedProxies(0),         // Count trusted proxies present in proxy headers
    clientip.MaxTrustedProxies(2),
    clientip.Priority(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
    clientip.WithChainSelection(clientip.RightmostUntrustedIP),
    clientip.AllowPrivateIPs(false),
)

Custom Headers

Support for cloud providers and custom proxy headers:

extractor, _ := clientip.New(
    clientip.TrustLoopbackProxy(),
    clientip.Priority(
        "CF-Connecting-IP",                   // Cloudflare
        clientip.SourceXForwardedFor,
        clientip.SourceRemoteAddr,
    ),
)

Header sources require trusted upstream proxy ranges. Use TrustProxyPrefixes (with ParseCIDRs for string inputs) or helper options like TrustLoopbackProxy, TrustPrivateProxyRanges, TrustLocalProxyDefaults, or TrustProxyAddrs.

Presets are available for common setups:

extractor, _ := clientip.New(clientip.PresetVMReverseProxy())

Observability

Add logging and metrics for production monitoring: (Prometheus adapter package: github.com/abczzz13/clientip/prometheus) The logger receives req.Context(), allowing trace/span IDs to flow through.

import clientipprom "github.com/abczzz13/clientip/prometheus"

metrics, _ := clientipprom.New()

extractor, err := clientip.New(
    clientip.TrustProxyPrefixes(cidrs...),
    clientip.MinTrustedProxies(0),
    clientip.MaxTrustedProxies(3),
    clientip.Priority(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
    clientip.WithLogger(slog.Default()),
    clientip.WithMetrics(metrics),
)

Security Considerations

The package includes several security features:

  • Detection of malformed Forwarded headers and duplicate single-IP header values
  • Immediate proxy trust enforcement before honoring Forwarded/X-Forwarded-For
  • Validation of proxy counts (min/max enforcement)
  • Chain length limits to prevent DoS
  • Rejection of invalid/implausible IPs (loopback, multicast, etc.)
  • Optional private IP filtering and explicit reserved CIDR allowlisting
  • Strict fail-closed behavior by default (SecurityModeStrict)

Security Anti-Patterns

  • Do not combine multiple competing header sources for security decisions.
  • Do not use SecurityModeLax for ACL/risk/authz enforcement paths.
  • Do not trust broad proxy CIDRs unless they are truly controlled by your edge.

Security Modes

Security behavior can be configured per extractor:

  • SecurityModeStrict (default): fail closed on security-significant errors and invalid present source values.
  • SecurityModeLax: allow fallback to lower-priority sources for those errors.

Example:

extractor, _ := clientip.New(
    clientip.WithSecurityMode(clientip.SecurityModeLax),
)

Thread Safety

Extractor instances are safe for concurrent use. They are typically created once at application startup and reused across all requests.

Index

Examples

Constants

View Source
const (
	// SourceForwarded resolves from the RFC7239 Forwarded header.
	SourceForwarded = "forwarded"
	// SourceXForwardedFor resolves from the X-Forwarded-For header.
	SourceXForwardedFor = "x_forwarded_for"
	// SourceXRealIP resolves from the X-Real-IP header.
	SourceXRealIP = "x_real_ip"
	// SourceRemoteAddr resolves from Request.RemoteAddr.
	SourceRemoteAddr = "remote_addr"
)
View Source
const (
	// DefaultMaxChainLength is the maximum number of IPs allowed in a proxy
	// chain. This prevents DoS attacks using extremely long header values that
	// could cause excessive memory allocation or CPU usage during parsing. 100
	// is chosen as a reasonable upper bound that accommodates complex
	// multi-region, multi-CDN setups while still providing protection. Typical
	// proxy chains rarely exceed 5-10 entries.
	DefaultMaxChainLength = 100
)

Variables

View Source
var (
	// ErrNoTrustedProxies indicates no trusted proxies were found in a parsed
	// chain when at least one is required.
	ErrNoTrustedProxies = errors.New("no trusted proxies found in proxy chain")

	// ErrSourceUnavailable indicates the selected source is not present on the
	// request.
	ErrSourceUnavailable = errors.New("source unavailable")

	// ErrMultipleSingleIPHeaders indicates multiple values were provided for a
	// single-IP header source.
	ErrMultipleSingleIPHeaders = errors.New("multiple single-IP headers received")

	// ErrUntrustedProxy indicates a header source was provided by an untrusted
	// immediate proxy.
	ErrUntrustedProxy = errors.New("request from untrusted proxy")

	// ErrTooFewTrustedProxies indicates trusted proxies in the chain are below
	// the configured minimum.
	ErrTooFewTrustedProxies = errors.New("too few trusted proxies in proxy chain")

	// ErrTooManyTrustedProxies indicates trusted proxies in the chain exceed the
	// configured maximum.
	ErrTooManyTrustedProxies = errors.New("too many trusted proxies in proxy chain")

	// ErrInvalidIP indicates the extracted client IP is invalid or implausible.
	ErrInvalidIP = errors.New("invalid or implausible IP address")

	// ErrChainTooLong indicates a Forwarded/X-Forwarded-For chain exceeded the
	// configured maximum length.
	ErrChainTooLong = errors.New("proxy chain too long")

	// ErrInvalidForwardedHeader indicates a malformed RFC7239 Forwarded header.
	ErrInvalidForwardedHeader = errors.New("invalid Forwarded header")
)

Functions

func ExtractAddrFromWithOptions added in v0.0.6

func ExtractAddrFromWithOptions(input RequestInput, opts ...Option) (netip.Addr, error)

ExtractAddrFromWithOptions is a one-shot convenience helper.

It constructs a temporary extractor from opts and resolves only the client IP address from framework-agnostic request input.

func ExtractAddrWithOptions added in v0.0.4

func ExtractAddrWithOptions(r *http.Request, opts ...Option) (netip.Addr, error)

ExtractAddrWithOptions is a one-shot convenience helper.

It constructs a temporary extractor from opts and resolves only the client IP address for r.

func NormalizeSourceName

func NormalizeSourceName(headerName string) string

NormalizeSourceName canonicalizes a source/header name for reporting.

It lowercases the value and replaces hyphens with underscores.

func ParseCIDRs

func ParseCIDRs(cidrs ...string) ([]netip.Prefix, error)

ParseCIDRs parses one or more CIDR strings.

Types

type ChainDebugInfo

type ChainDebugInfo struct {
	FullChain      []string
	ClientIndex    int
	TrustedIndices []int
}

ChainDebugInfo describes parsed chain-analysis details for diagnostics.

type ChainSelection added in v0.0.4

type ChainSelection int

ChainSelection controls how the client candidate is selected from a parsed proxy chain after trusted proxy validation.

const (
	// Start at 1 to avoid zero-value confusion and make invalid selections
	// explicit.
	//
	// RightmostUntrustedIP selects the rightmost untrusted address in the chain.
	RightmostUntrustedIP ChainSelection = iota + 1
	// LeftmostUntrustedIP selects the leftmost untrusted address in the chain.
	LeftmostUntrustedIP
)

func (ChainSelection) String added in v0.0.4

func (s ChainSelection) String() string

String returns the canonical text representation of s.

type ChainTooLongError

type ChainTooLongError struct {
	ExtractionError
	ChainLength int
	MaxLength   int
}

ChainTooLongError reports an overlong Forwarded/X-Forwarded-For chain.

func (*ChainTooLongError) Error

func (e *ChainTooLongError) Error() string

Error implements error.

type Extraction added in v0.0.4

type Extraction struct {
	IP netip.Addr

	Source string

	TrustedProxyCount int

	DebugInfo *ChainDebugInfo
}

Extraction contains extraction metadata.

On error, Source may still be set when available.

For additional diagnostics (such as chain details or trusted-proxy counts), inspect typed errors like ProxyValidationError and InvalidIPError.

func ExtractFromWithOptions added in v0.0.6

func ExtractFromWithOptions(input RequestInput, opts ...Option) (Extraction, error)

ExtractFromWithOptions is a one-shot convenience helper.

It constructs a temporary extractor from opts and resolves metadata from framework-agnostic request input.

func ExtractWithOptions added in v0.0.4

func ExtractWithOptions(r *http.Request, opts ...Option) (Extraction, error)

ExtractWithOptions is a one-shot convenience helper.

It constructs a temporary extractor from opts and resolves metadata for r.

type ExtractionError

type ExtractionError struct {
	Err    error
	Source string
}

ExtractionError wraps a source-specific extraction failure.

func (*ExtractionError) Error

func (e *ExtractionError) Error() string

Error implements error.

func (*ExtractionError) SourceName

func (e *ExtractionError) SourceName() string

SourceName returns the source identifier associated with this error.

func (*ExtractionError) Unwrap

func (e *ExtractionError) Unwrap() error

Unwrap returns the underlying sentinel or wrapped error.

type Extractor

type Extractor struct {
	// contains filtered or unexported fields
}

Extractor resolves client IP information from HTTP requests and framework-agnostic request inputs.

Extractor instances are safe for concurrent reuse.

func New

func New(opts ...Option) (*Extractor, error)

New creates an Extractor from one or more Option builders.

Example (Cloudflare)
package main

import (
	"fmt"
	"net/http"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.TrustLoopbackProxy(),
		clientip.Priority("CF-Connecting-IP", clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
	)

	req := &http.Request{RemoteAddr: "127.0.0.1:12345", Header: make(http.Header)}
	req.Header.Set("CF-Connecting-IP", "1.1.1.1")

	extraction, _ := extractor.Extract(req)
	fmt.Printf("Client IP: %s (from %s)\n", extraction.IP, extraction.Source)
}
Example (FlexibleProxyRange)
package main

import (
	"fmt"
	"net/http"
	"net/netip"

	"github.com/abczzz13/clientip"
)

func main() {
	cidrs, _ := netip.ParsePrefix("10.0.0.0/8")

	extractor, _ := clientip.New(
		clientip.TrustProxyPrefixes(cidrs),
		clientip.MinTrustedProxies(1),
		clientip.MaxTrustedProxies(3),
		clientip.Priority(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
	)

	req1 := &http.Request{RemoteAddr: "10.0.0.1:12345", Header: make(http.Header)}
	req1.Header.Set("X-Forwarded-For", "1.1.1.1, 10.0.0.1")
	extraction1, _ := extractor.Extract(req1)
	fmt.Printf("1 proxy: %s\n", extraction1.IP)

	req2 := &http.Request{RemoteAddr: "10.0.0.3:12345", Header: make(http.Header)}
	req2.Header.Set("X-Forwarded-For", "8.8.8.8, 10.0.0.2, 10.0.0.3")
	extraction2, _ := extractor.Extract(req2)
	fmt.Printf("2 proxies: %s\n", extraction2.IP)
}
Example (Forwarded)
package main

import (
	"fmt"
	"net/http"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.TrustLoopbackProxy(),
		clientip.Priority(clientip.SourceForwarded, clientip.SourceRemoteAddr),
	)

	req := &http.Request{RemoteAddr: "127.0.0.1:12345", Header: make(http.Header)}
	req.Header.Set("Forwarded", "for=1.1.1.1")

	extraction, _ := extractor.Extract(req)
	fmt.Println(extraction.IP, extraction.Source)
}
Output:

1.1.1.1 forwarded
Example (Simple)
package main

import (
	"fmt"
	"net/http"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, err := clientip.New()
	if err != nil {
		panic(err)
	}

	req := &http.Request{RemoteAddr: "8.8.4.4:12345", Header: make(http.Header)}

	ip, err := extractor.ExtractAddr(req)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Client IP: %s\n", ip)
}
Example (WithOptions)
package main

import (
	"fmt"
	"log/slog"
	"net/http"
	"net/netip"
	"os"

	"github.com/abczzz13/clientip"
)

func main() {
	cidrs, _ := netip.ParsePrefix("10.0.0.0/8")

	extractor, err := clientip.New(
		clientip.TrustProxyPrefixes(cidrs),
		clientip.MinTrustedProxies(1),
		clientip.MaxTrustedProxies(2),
		clientip.Priority(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
		clientip.AllowPrivateIPs(false),
		clientip.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, nil))),
	)
	if err != nil {
		panic(err)
	}

	req := &http.Request{RemoteAddr: "10.0.1.5:12345", Header: make(http.Header)}
	req.Header.Set("X-Forwarded-For", "1.1.1.1, 10.0.1.5")

	extraction, _ := extractor.Extract(req)
	fmt.Printf("Client IP: %s from source: %s\n", extraction.IP, extraction.Source)
}

func (*Extractor) Extract added in v0.0.4

func (e *Extractor) Extract(r *http.Request, overrides ...OverrideOptions) (Extraction, error)

Extract resolves client IP and metadata for the request.

When overrides are provided, they are merged left-to-right and applied only for this call.

func (*Extractor) ExtractAddr added in v0.0.4

func (e *Extractor) ExtractAddr(r *http.Request, overrides ...OverrideOptions) (netip.Addr, error)

ExtractAddr resolves only the client IP address.

func (*Extractor) ExtractAddrFrom added in v0.0.6

func (e *Extractor) ExtractAddrFrom(input RequestInput, overrides ...OverrideOptions) (netip.Addr, error)

ExtractAddrFrom resolves only the client IP address from framework-agnostic request input.

func (*Extractor) ExtractFrom added in v0.0.6

func (e *Extractor) ExtractFrom(input RequestInput, overrides ...OverrideOptions) (Extraction, error)

ExtractFrom resolves client IP and metadata from framework-agnostic request input.

When overrides are provided, they are merged left-to-right and applied only for this call.

Example
package main

import (
	"context"
	"fmt"
	"net/textproto"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.TrustLoopbackProxy(),
		clientip.Priority("CF-Connecting-IP", clientip.SourceRemoteAddr),
	)

	cfHeader := textproto.CanonicalMIMEHeaderKey("CF-Connecting-IP")
	input := clientip.RequestInput{
		Context:    context.Background(),
		RemoteAddr: "127.0.0.1:12345",
		Path:       "/framework-request",
		Headers: clientip.HeaderValuesFunc(func(name string) []string {
			if name == cfHeader {
				return []string{"8.8.8.8"}
			}
			return nil
		}),
	}

	extraction, _ := extractor.ExtractFrom(input)
	fmt.Println(extraction.IP, extraction.Source)
}
Output:

8.8.8.8 cf_connecting_ip

type HeaderValues added in v0.0.6

type HeaderValues interface {
	Values(name string) []string
}

HeaderValues provides access to request header values by name.

Implementations should return one slice entry per received header line. Single-IP sources rely on per-line values to detect duplicates, and chain sources preserve wire order across repeated lines.

Header names are requested in canonical MIME format (for example "X-Forwarded-For").

net/http's http.Header satisfies this interface directly.

type HeaderValuesFunc added in v0.0.6

type HeaderValuesFunc func(name string) []string

HeaderValuesFunc adapts a function to the HeaderValues interface.

func (HeaderValuesFunc) Values added in v0.0.6

func (f HeaderValuesFunc) Values(name string) []string

Values implements HeaderValues.

type InvalidIPError

type InvalidIPError struct {
	ExtractionError
	Chain          string
	ExtractedIP    string
	Index          int
	TrustedProxies int
}

InvalidIPError reports an invalid or implausible extracted client IP.

func (*InvalidIPError) Error

func (e *InvalidIPError) Error() string

Error implements error.

type Logger added in v0.0.3

type Logger interface {
	WarnContext(ctx context.Context, msg string, args ...any)
}

Logger records security-significant events emitted by Extractor.

Implementations should be safe for concurrent use, as a single Extractor instance is typically shared across many goroutines.

The provided context comes from the inbound HTTP request and can carry tracing metadata (for example, trace or span IDs).

The interface intentionally mirrors slog's WarnContext signature, so *slog.Logger can be used directly without an adapter.

type Metrics

type Metrics interface {
	// RecordExtractionSuccess is called when a source successfully returns a
	// client IP.
	RecordExtractionSuccess(source string)
	// RecordExtractionFailure is called when a source is attempted but cannot
	// return a valid client IP.
	RecordExtractionFailure(source string)
	// RecordSecurityEvent is called when the extractor observes a
	// security-relevant condition.
	RecordSecurityEvent(event string)
}

Metrics records extraction outcomes and security events emitted by Extractor.

Implementations should be safe for concurrent use, as a single Extractor instance is typically shared across many goroutines.

type MultipleHeadersError

type MultipleHeadersError struct {
	ExtractionError
	HeaderCount int
	HeaderName  string
	RemoteAddr  string
}

MultipleHeadersError reports duplicate header-line values for a source that expects a single header line.

func (*MultipleHeadersError) Error

func (e *MultipleHeadersError) Error() string

Error implements error.

type Option

type Option func(*config) error

Option configures an Extractor.

Construct options using package-provided option builder functions.

func AllowPrivateIPs

func AllowPrivateIPs(allow bool) Option

AllowPrivateIPs configures whether private client IPs are accepted.

func AllowReservedClientPrefixes added in v0.0.6

func AllowReservedClientPrefixes(prefixes ...netip.Prefix) Option

AllowReservedClientPrefixes configures reserved client prefixes to explicitly allow.

Example
package main

import (
	"fmt"
	"net/http"
	"net/netip"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.AllowReservedClientPrefixes(netip.MustParsePrefix("198.51.100.0/24")),
	)

	req := &http.Request{RemoteAddr: "198.51.100.10:12345", Header: make(http.Header)}

	extraction, _ := extractor.Extract(req)
	fmt.Println(extraction.IP, extraction.Source)
}
Output:

198.51.100.10 remote_addr

func MaxChainLength

func MaxChainLength(max int) Option

MaxChainLength sets the maximum number of entries accepted in proxy chains.

func MaxTrustedProxies added in v0.0.6

func MaxTrustedProxies(max int) Option

MaxTrustedProxies sets the maximum trusted proxy count for chain-header sources.

func MinTrustedProxies added in v0.0.6

func MinTrustedProxies(min int) Option

MinTrustedProxies sets the minimum trusted proxy count for chain-header sources.

func PresetDirectConnection added in v0.0.4

func PresetDirectConnection() Option

PresetDirectConnection configures extraction for direct client-to-app traffic.

This preset extracts from RemoteAddr only.

func PresetLoopbackReverseProxy added in v0.0.4

func PresetLoopbackReverseProxy() Option

PresetLoopbackReverseProxy configures extraction for apps behind a reverse proxy on the same host (for example NGINX on localhost).

It trusts loopback proxy CIDRs and uses X-Forwarded-For with RemoteAddr fallback.

func PresetPreferredHeaderThenXFFLax added in v0.0.4

func PresetPreferredHeaderThenXFFLax(header string) Option

PresetPreferredHeaderThenXFFLax configures extraction to prefer a single custom header, then fall back to X-Forwarded-For and RemoteAddr.

It also enables SecurityModeLax so invalid values in the preferred header can fall through to lower-priority sources.

Header-based sources still require trusted proxy CIDRs.

Example
package main

import (
	"fmt"
	"net/http"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.TrustLoopbackProxy(),
		clientip.PresetPreferredHeaderThenXFFLax("X-Frontend-IP"),
	)

	req := &http.Request{RemoteAddr: "127.0.0.1:12345", Header: make(http.Header)}
	req.Header.Set("X-Frontend-IP", "not-an-ip")
	req.Header.Set("X-Forwarded-For", "8.8.8.8")

	extraction, _ := extractor.Extract(req)
	fmt.Println(extraction.IP, extraction.Source)
}
Output:

8.8.8.8 x_forwarded_for

func PresetVMReverseProxy added in v0.0.4

func PresetVMReverseProxy() Option

PresetVMReverseProxy configures extraction for apps behind a reverse proxy in a typical VM or private-network setup.

It trusts loopback and private proxy CIDRs and uses X-Forwarded-For with RemoteAddr fallback.

Example
package main

import (
	"fmt"
	"net/http"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(clientip.PresetVMReverseProxy())

	req := &http.Request{RemoteAddr: "127.0.0.1:12345", Header: make(http.Header)}
	req.Header.Set("X-Forwarded-For", "1.1.1.1")

	extraction, _ := extractor.Extract(req)
	fmt.Println(extraction.IP, extraction.Source)
}
Output:

1.1.1.1 x_forwarded_for

func Priority

func Priority(sources ...string) Option

Priority sets extraction source order.

Source names are canonicalized so built-in aliases resolve to canonical constants.

func TrustLocalProxyDefaults added in v0.0.4

func TrustLocalProxyDefaults() Option

TrustLocalProxyDefaults adds loopback and private network CIDRs.

func TrustLoopbackProxy added in v0.0.4

func TrustLoopbackProxy() Option

TrustLoopbackProxy adds loopback CIDRs to trusted proxy ranges.

func TrustPrivateProxyRanges added in v0.0.4

func TrustPrivateProxyRanges() Option

TrustPrivateProxyRanges adds private network CIDRs to trusted proxy ranges.

func TrustProxyAddrs added in v0.0.6

func TrustProxyAddrs(addrs ...netip.Addr) Option

TrustProxyAddrs adds trusted upstream proxy host addresses.

func TrustProxyPrefixes added in v0.0.6

func TrustProxyPrefixes(prefixes ...netip.Prefix) Option

TrustProxyPrefixes adds trusted proxy network prefixes.

func WithChainSelection added in v0.0.4

func WithChainSelection(selection ChainSelection) Option

WithChainSelection sets how client candidates are chosen from chain headers.

Example (LeftmostUntrusted)
package main

import (
	"fmt"
	"net/http"
	"net/netip"

	"github.com/abczzz13/clientip"
)

func main() {
	cloudflareCIDRs, _ := netip.ParsePrefix("173.245.48.0/20")

	extractor, _ := clientip.New(
		clientip.TrustProxyPrefixes(cloudflareCIDRs),
		clientip.MinTrustedProxies(1),
		clientip.MaxTrustedProxies(3),
		clientip.Priority(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
		clientip.WithChainSelection(clientip.LeftmostUntrustedIP),
	)

	req := &http.Request{RemoteAddr: "173.245.48.5:443", Header: make(http.Header)}
	req.Header.Set("X-Forwarded-For", "1.1.1.1, 173.245.48.5")

	ip, _ := extractor.ExtractAddr(req)
	fmt.Printf("Client IP: %s\n", ip)
}

func WithDebugInfo

func WithDebugInfo(enable bool) Option

WithDebugInfo controls whether chain-debug metadata is included in results.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sets the logger implementation used for warning events.

func WithMetrics

func WithMetrics(metrics Metrics) Option

WithMetrics sets a concrete metrics implementation.

If previously configured, a metrics factory is disabled.

func WithMetricsFactory added in v0.0.4

func WithMetricsFactory(factory func() (Metrics, error)) Option

WithMetricsFactory configures a lazy metrics constructor.

The factory is invoked only for the final winning metrics option after option validation succeeds.

func WithSecurityMode

func WithSecurityMode(mode SecurityMode) Option

WithSecurityMode sets strict or lax fallback behavior after security errors.

Example (Lax)
package main

import (
	"fmt"
	"net/http"
	"net/netip"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.TrustProxyAddrs(netip.MustParseAddr("1.1.1.1")),
		clientip.Priority(clientip.SourceForwarded, clientip.SourceRemoteAddr),
		clientip.WithSecurityMode(clientip.SecurityModeLax),
	)

	req := &http.Request{RemoteAddr: "1.1.1.1:12345", Header: make(http.Header)}
	req.Header.Set("Forwarded", `for="1.1.1.1`)

	extraction, _ := extractor.Extract(req)
	fmt.Println(extraction.IP, extraction.Source)
}
Output:

1.1.1.1 remote_addr
Example (Strict)
package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/netip"

	"github.com/abczzz13/clientip"
)

func main() {
	extractor, _ := clientip.New(
		clientip.TrustProxyAddrs(netip.MustParseAddr("1.1.1.1")),
		clientip.Priority(clientip.SourceForwarded, clientip.SourceRemoteAddr),
		clientip.WithSecurityMode(clientip.SecurityModeStrict),
	)

	req := &http.Request{RemoteAddr: "1.1.1.1:12345", Header: make(http.Header)}
	req.Header.Set("Forwarded", `for="1.1.1.1`)

	extraction, err := extractor.Extract(req)
	fmt.Println(err == nil, errors.Is(err, clientip.ErrInvalidForwardedHeader), extraction.Source)
}
Output:

false true forwarded

type OverrideOptions added in v0.0.4

type OverrideOptions struct {
	TrustedProxyPrefixes SetValue[[]netip.Prefix]
	MinTrustedProxies    SetValue[int]
	MaxTrustedProxies    SetValue[int]

	AllowPrivateIPs             SetValue[bool]
	AllowReservedClientPrefixes SetValue[[]netip.Prefix]
	MaxChainLength              SetValue[int]
	ChainSelection              SetValue[ChainSelection]
	SecurityMode                SetValue[SecurityMode]
	DebugInfo                   SetValue[bool]

	SourcePriority SetValue[[]string]
}

OverrideOptions applies per-call policy overrides.

Only policy-related fields are overrideable. Logger and Metrics remain fixed at extractor construction time.

type ProxyValidationError

type ProxyValidationError struct {
	ExtractionError
	Chain             string
	TrustedProxyCount int
	MinTrustedProxies int
	MaxTrustedProxies int
}

ProxyValidationError reports failures from trusted-proxy chain validation.

func (*ProxyValidationError) Error

func (e *ProxyValidationError) Error() string

Error implements error.

type RemoteAddrError

type RemoteAddrError struct {
	ExtractionError
	RemoteAddr string
}

RemoteAddrError reports an invalid or implausible Request.RemoteAddr value.

func (*RemoteAddrError) Error

func (e *RemoteAddrError) Error() string

Error implements error.

type RequestInput added in v0.0.6

type RequestInput struct {
	Context    context.Context
	RemoteAddr string
	Path       string
	Headers    HeaderValues
}

RequestInput provides framework-agnostic request data for extraction.

Context defaults to context.Background() when nil.

For Headers, preserve repeated header lines as separate values for each header name (for example two X-Forwarded-For lines should yield a slice with length 2, and two X-Real-IP lines should also yield length 2).

type SecurityMode

type SecurityMode int

SecurityMode controls fallback behavior after security-significant errors.

const (
	// SecurityModeStrict fails closed and stops on security-significant errors.
	SecurityModeStrict SecurityMode = iota + 1
	// SecurityModeLax allows fallback to lower-priority sources after such errors.
	SecurityModeLax
)

func (SecurityMode) String

func (m SecurityMode) String() string

String returns the canonical text representation of m.

type SetValue added in v0.0.4

type SetValue[T any] struct {
	// contains filtered or unexported fields
}

SetValue represents an optional per-call override value.

Use Set(v) to mark an override as explicitly provided.

func Set added in v0.0.4

func Set[T any](value T) SetValue[T]

Set marks a value as explicitly set for OverrideOptions.

Directories

Path Synopsis
prometheus module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL