geo

package
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package geo turns a request into a small, bounded geo class (an ISO-ish country code like "US"/"ES", or "unknown") for the `{geo}` cache-key normalizer. It is the v2b normalizer, mirroring v2a's device classifier: a pure, deterministic lookup compiled once at config load and evaluated as a cheap per-request pre-pass — no heavy GeoIP database and no new dependency (stdlib net/netip).

The MECHANISM (request → enum) lives here; the DATA is a pluggable Source:

  • a HEADER source reads a CDN/LB-provided country header (e.g. CF-IPCountry) — the common case when a CDN fronts cadish; or
  • a CIDR-table source resolves the client IP against a CIDR→country table loaded from a file (longest-prefix match).

Unlike {device} there is no universal default, so a site must configure a geo source; with none, `{geo}` resolves to "" (documented).

Index

Constants

View Source
const Unknown = "unknown"

Unknown is the class returned when a source cannot determine the geo (missing header, no CIDR match, invalid IP).

Variables

This section is empty.

Functions

func ClientIP

func ClientIP(remoteAddr string, hdr http.Header, trusted []netip.Prefix) netip.Addr

ClientIP resolves the REAL client IP from the socket peer (remoteAddr) and the X-Forwarded-For header, trusting XFF only when the immediate peer is a configured trusted proxy.

Rationale: X-Forwarded-For is client-spoofable, so it is trusted only when the request actually arrived from a known proxy. The algorithm:

  • Parse the peer from remoteAddr. If there are no trusted prefixes, or the peer is NOT in one, return the peer and IGNORE XFF (a direct/untrusted client cannot forge its source IP at the socket layer).
  • If the peer IS a trusted proxy, walk the XFF chain right→left (nearest proxy first) and return the first address that is NOT itself a trusted proxy — the real client. If every XFF hop is trusted (or XFF is absent), fall back to the peer.

The returned Addr is Unmap'd (4-in-6 normalized); it is invalid only when remoteAddr is unparseable and no usable XFF entry exists.

func Continent

func Continent(country string) string

Continent maps an ISO 3166-1 alpha-2 country code to a 2-letter continent code (one of AF, AN, AS, EU, NA, OC, SA), or Unknown for an unrecognized/blank code. The country is upper-cased before lookup so a lower-case CDN header value (the header source already upper-cases, but a CIDR table or test may not) resolves.

This is the {geo.continent} granularity: it lets a site express the "EU → EUR else USD" currency split (and any continent-scoped policy) WITHOUT a GeoIP database — the continent is a pure, deterministic function of the country code cadish already resolves. The mapping is a small in-tree static table (D11: no GeoIP dependency); it is built once and read-only, so lookups are allocation-free and safe for concurrent use.

func ContinentCodes

func ContinentCodes() []string

ContinentCodes returns the bounded set of continent codes {geo.continent} can emit (plus Unknown is implicit). A copy is returned so callers cannot mutate the table.

func MaxMindHasRegion

func MaxMindHasRegion(path string) bool

MaxMindHasRegion opens the .mmdb at path, reports whether it is a City edition (so it supplies {geo.region}), and closes it. A missing/corrupt DB returns false. It is used by `cadish check` to decide whether {geo.region} needs a region_header. cadish bundles no DB; this only opens an operator-supplied file during a lint.

func ParsePrefixes

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

ParsePrefixes parses CIDR strings into prefixes (for trust_proxy config).

func PeerTrusted

func PeerTrusted(remoteAddr string, trusted []netip.Prefix) bool

PeerTrusted reports whether the IMMEDIATE socket peer (parsed from remoteAddr) is a configured trusted proxy. It is the gate for honoring client-supplied geo headers (CF-IPCountry, CF-Region, …): exactly like X-Forwarded-For in ClientIP, a header-sourced geo value may be trusted ONLY when the request actually arrived from a known proxy — otherwise a direct client could spoof its country/region to bypass a geo-fence or choose its {geo} cache bucket.

With no trusted prefixes configured the direct peer is NOT a trusted proxy, so this returns false — the safe, intended behavior: header geo REQUIRES trust_proxy.

Types

type CIDREntry

type CIDREntry struct {
	Prefix  netip.Prefix
	Country string
}

CIDREntry maps a network prefix to a country code.

func LoadCIDRTable

func LoadCIDRTable(r io.Reader) ([]CIDREntry, error)

LoadCIDRTable parses a CIDR→country table: one `CIDR,COUNTRY` per line (comma or whitespace separated), `#` comments and blank lines ignored. e.g.

# offices
203.0.113.0/24, US
2001:db8::/32   ES

type MaxMindDB

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

MaxMindDB is a hot-reloadable holder around one memory-mapped maxminddb reader. It is shared read-only across all requests (the reader is safe for concurrent use) and supports an atomic reader swap on SIGHUP-driven config reload: a new reader is opened, swapped in atomically, and the old one closed. On a reload FAILURE the old reader keeps serving (swap-on-success only) — startup is strict, reload is lenient (the standard cadish posture, consistent with D32).

SAFETY (use-after-unmap): the reader is a memory-mapped buffer with no refcount; closing it munmaps the pages, so an in-flight Lookup that has already loaded the old reader and is about to read it would fault (SIGSEGV/SIGBUS, uncatchable) if a concurrent Reload/Close munmapped underneath it. We make that impossible STRUCTURALLY with an RWMutex: every Lookup holds the RLock across the whole load+Lookup+Decode, and Reload/Close take the write Lock before swapping AND closing the old reader — so a close can never overlap an in-flight lookup. The atomic.Pointer is retained for the load itself, but correctness rests on the lock, not the atomic. The RLock is cheap and the geo path is already opt-in (gated behind UsesGeoToken), so this adds no cost to the non-geo fast path.

func OpenMaxMind

func OpenMaxMind(path string) (*MaxMindDB, error)

OpenMaxMind memory-maps the .mmdb at path and returns a holder. A missing or corrupt database is a FATAL config error (returned here) — a geo source the operator asked for that cannot load is a misconfiguration, not a silent degrade.

func (*MaxMindDB) Close

func (db *MaxMindDB) Close() error

Close releases the memory-mapped reader.

func (*MaxMindDB) DatabaseType

func (db *MaxMindDB) DatabaseType() string

DatabaseType returns the DB's edition string from its metadata (e.g. "GeoIP2-City", "GeoLite2-Country"), or "" if the reader is closed.

func (*MaxMindDB) HasRegion

func (db *MaxMindDB) HasRegion() bool

HasRegion reports whether this DB is a City edition — i.e. carries subdivisions and so can supply {geo.region}. A Country edition has no subdivisions.

func (*MaxMindDB) Path

func (db *MaxMindDB) Path() string

Path returns the .mmdb path this holder was opened from.

func (*MaxMindDB) Reload

func (db *MaxMindDB) Reload() error

Reload re-opens the holder's .mmdb into a fresh reader and swaps it in atomically, closing the previous reader after the swap. On any open error the OLD reader is kept (no swap) and the error is returned, so a fat-fingered DB swap never takes the site down. Safe to call concurrently with Lookup.

type Source

type Source interface {
	Lookup(clientIP netip.Addr, hdr http.Header) string
}

Source resolves a request's geo class. Sources differ in input — a CDN-set country header versus the client IP — so Lookup receives both the resolved real client IP and the request headers; a given source uses whichever it needs. Implementations must be safe for concurrent use.

func NewCIDRSource

func NewCIDRSource(entries []CIDREntry) Source

NewCIDRSource builds a Source that resolves the client IP against a CIDR→country table by LONGEST-PREFIX match. Country codes are upper-cased. A miss (or invalid IP) yields Unknown.

func NewFallbackSource

func NewFallbackSource(primary, secondary Source) Source

NewFallbackSource composes two Sources into a precedence chain: Lookup tries primary and returns its result unless it is Unknown, in which case it falls through to secondary. This is the narrow "CDN AND MaxMind" composition (declared-order precedence): the FIRST-declared source wins per lookup, the second only fills a total miss. It is intentionally a thin two-source chain, not a general N-source merge, and never merges at finer granularity (one source answers a given lookup).

func NewHeaderSource

func NewHeaderSource(name string) Source

NewHeaderSource builds a Source that reads the geo class from a request header (e.g. "CF-IPCountry", "X-Geo-Country") set by an upstream CDN/LB. The value is upper-cased; an absent/blank header yields Unknown.

func NewMaxMindCountrySource

func NewMaxMindCountrySource(db *MaxMindDB) Source

NewMaxMindCountrySource builds a Source that resolves the {geo} country class (country.iso_code) by looking up the resolved client IP in the MaxMind DB. A miss, an invalid/empty record, or an invalid IP yields Unknown. The value passes through boundGeoClass so cache-key cardinality stays bounded — the same contract as the header source (uniform, and cheap on a trusted DB).

func NewMaxMindRegionSource

func NewMaxMindRegionSource(db *MaxMindDB) Source

NewMaxMindRegionSource builds a Source that resolves the {geo.region} class — ISO 3166-2 "COUNTRY-SUBDIVISION" (e.g. "US-WA") — from the City edition's country.iso_code + subdivisions[0].iso_code. A Country-edition DB has no subdivisions, so this yields Unknown there (the operator should pair a `region_header` or use a City DB). A miss / invalid IP also yields Unknown.

func NewRegionSource

func NewRegionSource(name string) Source

NewRegionSource builds a Source that reads the region / subdivision class (e.g. "US-UT", "US-TX") from a configurable upstream CDN/LB header (CF-Region, X-Geo-Region, or any operator-named header). The value is upper-cased; an absent/blank header yields Unknown.

Why a header, not the IP: turning a raw client IP into a US state needs a GeoIP database, a dependency cadish deliberately avoids (D11). So — exactly like the country, which comes from CF-IPCountry today — the region is sourced from an upstream geo header the CDN already computed. There is intentionally no bundled GeoIP DB and no CIDR-subdivision table: region granularity REQUIRES an upstream geo header. It is mechanically identical to the country header source.

Jump to

Keyboard shortcuts

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