{"id":43980453,"url":"https://github.com/abczzz13/clientip","last_synced_at":"2026-04-24T07:04:55.147Z","repository":{"id":336935502,"uuid":"1151255888","full_name":"abczzz13/clientip","owner":"abczzz13","description":"Security-first client IP extraction for Go net/http services with trusted proxy validation, configurable source priority, and optional Prometheus metrics.","archived":false,"fork":false,"pushed_at":"2026-04-23T19:05:36.000Z","size":403,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-23T20:22:23.588Z","etag":null,"topics":["client-ip","go","golang","ip-extraction","net-http","reverse-proxy","security","trusted-proxy","x-forwarded-for","x-real-ip"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/abczzz13.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-06T08:38:42.000Z","updated_at":"2026-04-15T18:28:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/abczzz13/clientip","commit_stats":null,"previous_names":["abczzz13/clientip"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/abczzz13/clientip","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abczzz13%2Fclientip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abczzz13%2Fclientip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abczzz13%2Fclientip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abczzz13%2Fclientip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/abczzz13","download_url":"https://codeload.github.com/abczzz13/clientip/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abczzz13%2Fclientip/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32212813,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T03:15:14.334Z","status":"ssl_error","status_checked_at":"2026-04-24T03:15:11.608Z","response_time":64,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["client-ip","go","golang","ip-extraction","net-http","reverse-proxy","security","trusted-proxy","x-forwarded-for","x-real-ip"],"created_at":"2026-02-07T09:01:31.955Z","updated_at":"2026-04-24T07:04:55.142Z","avatar_url":"https://github.com/abczzz13.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# clientip\n\n[![CI](https://github.com/abczzz13/clientip/actions/workflows/ci.yml/badge.svg)](https://github.com/abczzz13/clientip/actions/workflows/ci.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/abczzz13/clientip.svg)](https://pkg.go.dev/github.com/abczzz13/clientip)\n[![License](https://img.shields.io/github/license/abczzz13/clientip)](LICENSE)\n\nSecure client IP extraction for `net/http` and framework-agnostic request inputs with trusted proxy validation, explicit source modeling, and request-scoped resolver caching.\n\n## Stability\n\nThis project is pre-`v1.0.0` and still before `v0.1.0`, so public APIs may change as the package evolves.\nAny breaking changes are called out in `CHANGELOG.md`.\nThis README tracks the current `main` branch rather than the latest tagged release.\n\n## Contents\n\n- [Install](#install)\n- [Choose the API](#choose-the-api)\n- [Common Setups](#common-setups)\n- [Rules To Remember](#rules-to-remember)\n- [Quick Start](#quick-start)\n- [Preferred Resolution And Fallback](#preferred-resolution-and-fallback)\n- [Framework-Agnostic Input](#framework-agnostic-input)\n- [Presets](#presets)\n- [Config](#config)\n- [Low-Level Extraction](#low-level-extraction)\n- [Errors](#errors)\n- [Logging](#logging)\n- [Prometheus Metrics](#prometheus-metrics)\n- [Security Guidance](#security-guidance)\n- [Compatibility](#compatibility)\n- [Performance](#performance)\n- [Maintainer Notes (Multi-Module)](#maintainer-notes-multi-module)\n- [License](#license)\n\n## Install\n\n```bash\ngo get github.com/abczzz13/clientip\n```\n\nOptional Prometheus adapter:\n\n```bash\ngo get github.com/abczzz13/clientip/prometheus\n```\n\n\u003e Version note: the published adapter module is still pinned to `github.com/abczzz13/clientip v0.0.6` until the next coordinated release. This README documents the current `main`-branch API, so the Prometheus snippets below describe the upcoming adapter wiring after that release lands.\n\n## Choose the API\n\nUse this as a quick decision guide:\n\n| Need | Use |\n| --- | --- |\n| Security-sensitive or audit-oriented result | `Resolver.ResolveStrict` or `Extractor` |\n| Best-effort operational IP with explicit fallback | `Resolver.ResolvePreferred` |\n| Framework integration without `*http.Request` | `Input` with `Resolver` or `Extractor` |\n| Parse `RemoteAddr` outside extraction | `ParseRemoteAddr` |\n| Coarse policy branching on error categories | `ClassifyError` |\n\nConstruct an `Extractor` once and reuse it. Build a `Resolver` on top when you want strict or preferred request-scoped resolution.\n\n## Common Setups\n\nMost integrations start with a preset and only drop to a fully manual `Config` when the proxy topology is unusual.\n\nDirect app-to-client traffic:\n\n```go\nextractor, err := clientip.New(clientip.PresetDirectConnection())\n```\n\nReverse proxy on the same host:\n\n```go\nextractor, err := clientip.New(clientip.PresetLoopbackReverseProxy())\n```\n\nReverse proxy on a VM or private network:\n\n```go\nextractor, err := clientip.New(clientip.PresetVMReverseProxy())\n```\n\nCustom trusted header source:\n\n```go\nextractor, err := clientip.New(clientip.Config{\n    TrustedProxyPrefixes: clientip.LoopbackProxyPrefixes(),\n    Sources: []clientip.Source{\n        clientip.HeaderSource(\"CF-Connecting-IP\"),\n        clientip.SourceRemoteAddr,\n    },\n})\n```\n\nFramework request input:\n\nUse `Input` when the framework does not hand you `*http.Request` directly. The same extractor and resolver rules still apply.\n\n## Rules To Remember\n\n- `Resolver` is the primary integration-facing API; `Extractor` is the lower-level strict primitive.\n- Header-based sources require `TrustedProxyPrefixes`.\n- Prefer a preset first, then tweak `Config` only when needed.\n- Only configure one proxy-chain source at a time: `SourceForwarded` or `SourceXForwardedFor`.\n- Preferred fallback is operationally useful, but not suitable for authorization, ACLs, or trust-boundary enforcement.\n- `Input.Headers` must preserve repeated header lines as separate slice entries; merging them breaks duplicate detection and chain parsing semantics.\n\n## Quick Start\n\nUse `Resolver.ResolveStrict` for security-sensitive or audit-oriented decisions.\n\n```go\nextractor, err := clientip.New(clientip.PresetLoopbackReverseProxy())\nif err != nil {\n    log.Fatal(err)\n}\n\nresolver, err := clientip.NewResolver(extractor, clientip.ResolverConfig{})\nif err != nil {\n    log.Fatal(err)\n}\n\nreq := \u0026http.Request{RemoteAddr: \"127.0.0.1:12345\", Header: make(http.Header)}\nreq.Header.Set(\"X-Forwarded-For\", \"8.8.8.8\")\n\nreq, resolution := resolver.ResolveStrict(req)\nif resolution.Err != nil {\n    log.Fatal(resolution.Err)\n}\n\nfmt.Printf(\"Client IP: %s from %s\\n\", resolution.IP, resolution.Source)\n\nif cached, ok := clientip.StrictResolutionFromContext(req.Context()); ok {\n    fmt.Printf(\"Cached: %s\\n\", cached.IP)\n}\n```\n\n## Preferred Resolution And Fallback\n\nUse `Resolver.ResolvePreferred` when best-effort client IPs are operationally useful, such as rate limiting, analytics, or request tracing.\n\n```go\nextractor, err := clientip.New(clientip.Config{\n    TrustedProxyPrefixes: clientip.LoopbackProxyPrefixes(),\n    Sources:              []clientip.Source{clientip.SourceXForwardedFor},\n})\nif err != nil {\n    log.Fatal(err)\n}\n\nresolver, err := clientip.NewResolver(extractor, clientip.ResolverConfig{\n    PreferredFallback: clientip.PreferredFallbackRemoteAddr,\n})\nif err != nil {\n    log.Fatal(err)\n}\n\nreq := \u0026http.Request{RemoteAddr: \"1.1.1.1:12345\", Header: make(http.Header)}\n\n_, resolution := resolver.ResolvePreferred(req)\nif resolution.Err != nil {\n    log.Fatal(resolution.Err)\n}\n\nfmt.Printf(\"Client IP: %s from %s (fallback=%t)\\n\", resolution.IP, resolution.Source, resolution.FallbackUsed)\n```\n\nImportant fallback guidance:\n\n- Preferred fallback is explicit and only lives on `Resolver`.\n- `PreferredFallbackRemoteAddr` is operationally useful, but it is not equivalent to validated proxy-header extraction.\n- Preferred resolution is not suitable for authorization, ACLs, or other trust-boundary decisions.\n- Fallback observability is result-only in this phase. Inspect `Resolution.FallbackUsed`; do not expect a separate fallback metric or log event.\n\nIf you want a synthetic fallback value instead of `RemoteAddr`, set `ResolverConfig{PreferredFallback: clientip.PreferredFallbackStaticIP, StaticFallbackIP: ...}`. Successful static fallback reports `SourceStaticFallback`.\n\n## Framework-Agnostic Input\n\nUse `Input` with either `Extractor` or `Resolver` when your framework does not expose `*http.Request` directly.\n\n```go\ninput := clientip.Input{\n    Context:    ctx,\n    RemoteAddr: remoteAddr,\n    Path:       path,\n    Headers:    headersProvider,\n}\n\ninput, resolution := resolver.ResolveInputStrict(input)\nif resolution.Err != nil {\n    // handle error\n}\n\nif cached, ok := clientip.StrictResolutionFromContext(input.Context); ok {\n    _ = cached\n}\n```\n\n`Input.Headers` must preserve repeated header lines as separate slice entries. Do not merge duplicate lines into a single comma-joined string.\n\nFor `fasthttp`/Fiber style integrations:\n\n```go\ninput := clientip.Input{\n    Context:    c.UserContext(),\n    RemoteAddr: c.Context().RemoteAddr().String(),\n    Path:       c.Path(),\n    Headers: clientip.HeaderValuesFunc(func(name string) []string {\n        raw := c.Context().Request.Header.PeekAll(name)\n        if len(raw) == 0 {\n            return nil\n        }\n\n        values := make([]string, len(raw))\n        for i, v := range raw {\n            values[i] = string(v)\n        }\n        return values\n    }),\n}\n```\n\n## Presets\n\nPresets return a flat `clientip.Config` that you can pass directly to `New` or tweak before construction.\n\n- `PresetDirectConnection()` uses `RemoteAddr` only.\n- `PresetLoopbackReverseProxy()` trusts loopback proxies and prioritizes `X-Forwarded-For` before `RemoteAddr`.\n- `PresetVMReverseProxy()` trusts loopback and common private-network proxy ranges and prioritizes `X-Forwarded-For` before `RemoteAddr`.\n\n```go\nextractor, err := clientip.New(clientip.PresetVMReverseProxy())\nif err != nil {\n    log.Fatal(err)\n}\n```\n\nIf you need to tweak a preset, modify the returned config before calling `New`:\n\n```go\ncfg := clientip.PresetVMReverseProxy()\ncfg.Sources = []clientip.Source{clientip.SourceForwarded, clientip.SourceRemoteAddr}\n\nextractor, err := clientip.New(cfg)\nif err != nil {\n    log.Fatal(err)\n}\n```\n\nPresets configure `Config`, not `ResolverConfig`. Preferred resolver fallback stays an explicit resolver-level choice.\n\n## Config\n\n`Config` stays flat in the current API.\n\nMost callers should start from `PresetDirectConnection`, `PresetLoopbackReverseProxy`, or `PresetVMReverseProxy`, then adjust the returned `Config` if they need custom trust ranges, source order, or observability wiring.\n\nImportant fields:\n\n- `TrustedProxyPrefixes []netip.Prefix`\n- `MinTrustedProxies int`\n- `MaxTrustedProxies int`\n- `AllowPrivateIPs bool`\n- `AllowedReservedClientPrefixes []netip.Prefix`\n- `MaxChainLength int`\n- `ChainSelection ChainSelection`\n- `DebugInfo bool`\n- `Sources []Source`\n- `Logger Logger`\n- `Metrics Metrics`\n\nUseful helpers:\n\n- `DefaultConfig()`\n- `ParseCIDRs(...string)`\n- `LoopbackProxyPrefixes()`\n- `PrivateProxyPrefixes()`\n- `LocalProxyPrefixes()`\n- `ProxyPrefixesFromAddrs(...netip.Addr)`\n\nBuilt-in extractor sources:\n\n- `SourceForwarded`\n- `SourceXForwardedFor`\n- `SourceXRealIP`\n- `SourceRemoteAddr`\n- `HeaderSource(name)` for custom headers\n\nResolver-only result source:\n\n- `SourceStaticFallback`\n\n`Extractor` walks `Config.Sources` in order. `ErrSourceUnavailable` allows the next source to run, while security-significant failures remain terminal.\n\n## Low-Level Extraction\n\nUse `Extractor` directly when you want strict extraction without request-scoped caching or preferred fallback.\n\n```go\nextractor, err := clientip.New(clientip.DefaultConfig())\nif err != nil {\n    log.Fatal(err)\n}\n\nextraction, err := extractor.Extract(req)\nif err != nil {\n    log.Fatal(err)\n}\n\nfmt.Printf(\"Client IP: %s from %s\\n\", extraction.IP, extraction.Source)\n```\n\nFramework-agnostic extraction is also available:\n\n```go\nextraction, err := extractor.ExtractInput(input)\nif err != nil {\n    log.Fatal(err)\n}\n```\n\n## Errors\n\nTyped errors remain the detailed error surface:\n\n```go\n_, resolution := resolver.ResolveStrict(req)\nif resolution.Err != nil {\n    switch {\n    case errors.Is(resolution.Err, clientip.ErrMultipleSingleIPHeaders):\n    case errors.Is(resolution.Err, clientip.ErrInvalidForwardedHeader):\n    case errors.Is(resolution.Err, clientip.ErrUntrustedProxy):\n    case errors.Is(resolution.Err, clientip.ErrNoTrustedProxies):\n    case errors.Is(resolution.Err, clientip.ErrTooFewTrustedProxies):\n    case errors.Is(resolution.Err, clientip.ErrTooManyTrustedProxies):\n    case errors.Is(resolution.Err, clientip.ErrInvalidIP):\n    case errors.Is(resolution.Err, clientip.ErrSourceUnavailable):\n    case errors.Is(resolution.Err, clientip.ErrNilRequest):\n    }\n}\n```\n\n`ClassifyError` provides a smaller policy-oriented layer on top of those typed errors:\n\n```go\nswitch clientip.ClassifyError(resolution.Err) {\ncase clientip.ResultSuccess:\ncase clientip.ResultUnavailable:\ncase clientip.ResultInvalid:\ncase clientip.ResultUntrusted:\ncase clientip.ResultMalformed:\ncase clientip.ResultCanceled:\ncase clientip.ResultUnknown:\n}\n```\n\n`ResultUnknown` covers non-nil errors outside the package's standard extraction and resolution categories.\n\n`ErrInvalidForwardedHeader` covers malformed RFC7239 syntax, including present-but-empty `Forwarded` values and empty elements or parameters introduced by stray delimiters. In strict extraction, malformed `Forwarded` remains terminal and does not fall through to a lower-priority source.\n\nTyped chain-related errors expose additional context:\n\n- `ProxyValidationError`: `Chain`, `TrustedProxyCount`, `MinTrustedProxies`, `MaxTrustedProxies`\n- `InvalidIPError`: `Chain`, `ExtractedIP`, `Index`, `TrustedProxies`\n- `RemoteAddrError`: `RemoteAddr`\n- `ChainTooLongError`: `ChainLength`, `MaxLength`\n\n## Logging\n\nLogging is disabled by default. Set `Config.Logger` to opt in.\n\n```go\nextractor, err := clientip.New(clientip.Config{\n    Logger: slog.Default(),\n})\n```\n\nThe logger interface intentionally matches `slog.Logger.WarnContext`:\n\n```go\ntype Logger interface {\n    WarnContext(context.Context, string, ...any)\n}\n```\n\nThe context passed to logger calls comes from `req.Context()` (`Extract`) or `Input.Context` (`ExtractInput`).\n\nSecurity event labels passed through `Metrics.RecordSecurityEvent(...)` are the stable exported `clientip.SecurityEvent...` constants.\n\n## Prometheus Metrics\n\nConstruct Prometheus metrics explicitly and pass them through `Config.Metrics`.\n\nThis constructor-based wiring reflects the current `main` branch. Tagged-release consumers of `github.com/abczzz13/clientip/prometheus` still get the older adapter release that depends on root `v0.0.6` until the next coordinated adapter tag is published.\n\n```go\nimport clientipprom \"github.com/abczzz13/clientip/prometheus\"\n\nmetrics, err := clientipprom.New()\nif err != nil {\n    panic(err)\n}\n\nextractor, err := clientip.New(clientip.Config{Metrics: metrics})\nif err != nil {\n    panic(err)\n}\n\nresolver, err := clientip.NewResolver(extractor, clientip.ResolverConfig{})\nif err != nil {\n    panic(err)\n}\n```\n\nWith a custom registerer:\n\n```go\nregistry := prometheus.NewRegistry()\n\nmetrics, err := clientipprom.NewWithRegisterer(registry)\nif err != nil {\n    panic(err)\n}\n```\n\n## Security Guidance\n\n- Use `ResolveStrict` or `Extractor` for security-sensitive and audit-oriented behavior.\n- Use `ResolvePreferred` only when explicit fallback is acceptable for operational reasons.\n- Do not use preferred fallback for authorization, ACLs, or trust-boundary enforcement.\n- Do not include multiple competing header-based sources for security decisions.\n- Do not trust broad proxy CIDRs unless they are truly under your control.\n- Header-based sources require `TrustedProxyPrefixes`.\n- `LeftmostUntrustedIP` only makes sense when trusted proxy prefixes are configured.\n\n## Compatibility\n\n- Core module (`github.com/abczzz13/clientip`) supports Go `1.21+`.\n- 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` against the released root module until the next coordinated adapter release is tagged.\n- The published adapter module is currently pinned to `github.com/abczzz13/clientip v0.0.6`; the constructor-based `Config.Metrics` wiring documented above becomes the tagged-release path once that coordinated release ships.\n- Prometheus client dependency in the adapter is pinned to `github.com/prometheus/client_golang v1.21.1`.\n\n## Performance\n\n- Extraction is `O(n)` in proxy-chain length.\n- `Extractor` is safe for concurrent reuse.\n- `Resolver` adds request-scoped caching on top of a reusable extractor.\n\nBenchmark workflow with `just`:\n\n```bash\n# Capture a stable baseline (6 samples by default)\njust bench-save before \"BenchmarkExtract|BenchmarkChainAnalysis|BenchmarkParseIP\"\n\n# Make changes, then capture again\njust bench-save after \"BenchmarkExtract|BenchmarkChainAnalysis|BenchmarkParseIP\"\n\n# Compare with benchstat table output (delta + significance)\njust bench-compare-saved before after\n```\n\nYou can compare arbitrary files directly via `just bench-compare \u003cbefore-file\u003e \u003cafter-file\u003e`.\n\n## Maintainer Notes (Multi-Module)\n\n- `prometheus/go.mod` intentionally does not use a local `replace` directive for `github.com/abczzz13/clientip`.\n- For local co-development, create an uncommitted workspace with `go work init . ./prometheus`.\n- Validate the adapter as a consumer with `GOWORK=off go -C prometheus test ./...`; until the next coordinated release, this intentionally exercises the released root module `v0.0.6` instead of the unreleased workspace API.\n- `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.\n- 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`.\n\n## License\n\nSee `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabczzz13%2Fclientip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fabczzz13%2Fclientip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabczzz13%2Fclientip/lists"}