Secure client IP resolution for net/http and framework-agnostic request inputs with trusted proxy validation, explicit source modeling, and operational fallback.
This project is pre-v0.1.0; public APIs may change before stabilization.
go get github.com/abczzz13/clientipOptional Prometheus adapter:
go get github.com/abczzz13/clientip/observe/prometheusDirect client-to-app traffic trusts only RemoteAddr:
resolver, err := clientip.New()
if err != nil {
log.Fatal(err)
}
result := resolver.Resolve(req)
if result.Err != nil {
// fail closed for security-sensitive decisions
return
}
fmt.Println(result.IP)Loopback reverse proxy using X-Forwarded-For:
resolver, err := clientip.New(clientip.PresetLoopbackReverseProxy())Custom trusted proxy topology:
resolver, err := clientip.New(
clientip.WithTrustedProxies(prefixes...),
clientip.WithSources(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
)Header-based sources require trusted proxy prefixes. clientip.New(clientip.WithSources(clientip.SourceXForwardedFor)) returns an error.
Use Resolve for security-sensitive decisions. It returns a Result with Err set when the request cannot be safely attributed.
Use ResolveOperational only for best-effort analytics/logging paths where fallback is acceptable:
result := resolver.ResolveOperational(req, clientip.RemoteAddrFallback())
if result.FallbackUsed {
fmt.Println(result.FallbackReason)
}Operational fallback success clears Err and sets FallbackUsed plus FallbackReason. Do not use fallback results for authorization, ACLs, rate-limit identity, or other trust-boundary decisions.
StaticFallback(ip) is for caller-supplied operational defaults. The address is normalized but is not checked against client-IP plausibility rules, so validate it yourself if it must be routable or policy-valid.
Middleware is pass-through. It stores the strict Result in request context and never rejects by itself.
Rejection responses are intentionally application-owned so services can control
status codes, response bodies, headers, logging, and tracing.
handler := resolver.Middleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result, ok := clientip.FromContext(r.Context())
if !ok || result.Err != nil {
http.Error(w, "bad client IP", http.StatusBadRequest)
return
}
_, _ = w.Write([]byte(result.IP.String()))
}))Use Input when a framework does not expose *http.Request directly. It exists to preserve repeated header-line semantics: duplicate single-IP headers must be detectable, and chain headers must preserve the order in which repeated header lines arrived. Header providers must therefore return repeated header lines as separate values.
result := resolver.ResolveInput(clientip.Input{
Context: ctx,
RemoteAddr: remoteAddr,
Headers: headersProvider,
})For plain http.Header values:
result := resolver.ResolveHeaders(ctx, remoteAddr, headers)Generic option presets are available:
PresetDirectConnection()trusts onlyRemoteAddr.PresetLoopbackReverseProxy()trusts loopback proxies and usesX-Forwarded-For, thenRemoteAddr.PresetVMReverseProxy()trusts loopback/private proxy ranges and usesX-Forwarded-For, thenRemoteAddr.
Vendor-specific contrib packages are intentionally not included for now because vendor IP ranges become stale and dynamic range fetchers add operational policy. Vendor-specific configurations should explicitly pair the header with caller-supplied trusted peer ranges. For example, Cloudflare-style configuration should trust CF-Connecting-IP only when the immediate peer is in Cloudflare’s published CIDRs:
resolver, err := clientip.New(
clientip.WithTrustedProxies(cloudflarePrefixes...),
clientip.WithSources(clientip.HeaderSource("CF-Connecting-IP"), clientip.SourceRemoteAddr),
)AWS ALB append mode typically uses X-Forwarded-For with trusted VPC/ALB peer ranges:
resolver, err := clientip.New(
clientip.WithTrustedProxies(albOrVpcPrefixes...),
clientip.WithSources(clientip.SourceXForwardedFor, clientip.SourceRemoteAddr),
)Use WithObserver for result-level metrics/tracing:
metrics, err := prometheus.New()
if err != nil {
log.Fatal(err)
}
resolver, err := clientip.New(clientip.WithObserver(metrics))Prometheus support lives in the optional github.com/abczzz13/clientip/observe/prometheus adapter module. OpenTelemetry and other adapters can use the same Observer interface without adding dependencies to the core package.
Result.Classify() returns a low-cardinality outcome suitable for metrics labels.
RemoteAddris the only inherently trustworthy source.- Forwarding headers are trusted only when the immediate peer is in
WithTrustedProxies. - The default chain algorithm is rightmost-untrusted before the trusted proxy suffix.
- Do not use operational fallback for security decisions.
- Count-only proxy trust is intentionally unsupported:
WithMinTrustedProxies/WithMaxTrustedProxiesvalidate CIDR-trusted hop counts and do not by themselves make a header source trusted.