Client IP
- Start here: Client IP guide
- Common recipes:
resolve_client_address(request.scope, trusted)returns the address a trusted proxy vouched for.ClientAddressMiddlewareresolves it once per request so every consumer reads one value.
grelmicro.clientip
Client IP resolution behind trusted proxies.
X-Forwarded-For is append-only, so its leftmost entry is whatever the
original caller claimed. Reading it is the flaw behind a long line of
spoofing advisories. This module resolves the address a trusted proxy
actually vouched for, and never returns anything else.
Read more in the Client IP docs.
TrustedProxies
TrustedProxies(
networks: Iterable[str | IPAddress | IPNetwork],
/,
*,
max_hops: int | None = None,
max_entries: int = 64,
max_header_bytes: int = 8192,
)
The proxies whose X-Forwarded-For entries may be believed.
Build one at startup and reuse it. Every entry is parsed strictly, so a typo fails here rather than becoming a rule that silently matches nothing.
There is no wildcard. To trust every peer, pass ["0.0.0.0/0", "::/0"],
which is visible in a configuration review in a way a * is not.
Compile the trusted set, rejecting anything unparsable.
| PARAMETER | DESCRIPTION |
|---|---|
networks
|
Addresses and CIDR ranges of your own proxies. Required, with no default. An empty iterable is legal and means trust nothing, so resolution always returns the transport peer.
TYPE:
|
max_hops
|
Most trusted proxies to walk past. Bounds a forged chain. None means no cap.
TYPE:
|
max_entries
|
Most header entries to parse before giving up.
TYPE:
|
max_header_bytes
|
Most header bytes to parse before giving up.
TYPE:
|
ClientAddress
dataclass
ClientAddress(
ip: IPAddress,
port: int | None,
reason: ClientAddressReason,
hops: int,
)
A resolved client address, with how it was reached.
ip
instance-attribute
ip: IPAddress
The address. Always parsed, never raw header text.
port
instance-attribute
port: int | None
The source port, when one was given.
reason
instance-attribute
reason: ClientAddressReason
Why this address was chosen. RESOLVED is the trusted case.
hops
instance-attribute
hops: int
Trusted proxies skipped before landing on this address.
forwarded
property
forwarded: bool
Whether a trusted proxy vouched for this address.
degraded
property
degraded: bool
Whether this is a fallback rather than the caller's own address.
True when the forwarded chain could not be believed, so ip holds
the connecting peer instead. Behind a proxy that peer is the proxy,
so any check that treats it as the caller (an allowlist, a private
network test) must refuse when this is True.
key
property
key: str
Canonical form, safe to use as a rate limiter key.
An IPv4-mapped address folds onto its IPv4 form, so one caller never occupies two buckets.
ClientAddressReason
Bases: StrEnum
Why ClientAddress.ip holds the address it holds.
Only RESOLVED means a trusted proxy vouched for the address. Every
other value means the header could not be believed and the verified
transport peer was used instead, which a caller can log or reject.
RESOLVED
class-attribute
instance-attribute
RESOLVED = 'resolved'
NO_FORWARDED_HEADER
class-attribute
instance-attribute
NO_FORWARDED_HEADER = 'no-forwarded-header'
UNTRUSTED_PEER
class-attribute
instance-attribute
UNTRUSTED_PEER = 'untrusted-peer'
CHAIN_EXHAUSTED
class-attribute
instance-attribute
CHAIN_EXHAUSTED = 'chain-exhausted'
HOP_LIMIT
class-attribute
instance-attribute
HOP_LIMIT = 'hop-limit'
MALFORMED_ENTRY
class-attribute
instance-attribute
MALFORMED_ENTRY = 'malformed-entry'
HEADER_TOO_LARGE
class-attribute
instance-attribute
HEADER_TOO_LARGE = 'header-too-large'
TOO_MANY_ENTRIES
class-attribute
instance-attribute
TOO_MANY_ENTRIES = 'too-many-entries'
ClientAddressMiddleware
ClientAddressMiddleware(
app: ASGIApp,
*,
trusted: TrustedProxies,
overwrite_scope_client: bool = False,
)
Resolve the client address once and cache it on the request.
Stores a ClientAddress under scope["state"]["client_address"], so
every handler and every other middleware reads one value instead of
each resolving its own and drifting apart.
from grelmicro.clientip import ClientAddressMiddleware, TrustedProxies
app.add_middleware(
ClientAddressMiddleware, trusted=TrustedProxies(["10.0.0.0/8"])
)
Pure ASGI, so it works on any ASGI server. It acts on http and
websocket scopes and passes every other scope through untouched.
Initialize the middleware with the trusted set.
| PARAMETER | DESCRIPTION |
|---|---|
app
|
The next ASGI application in the chain.
TYPE:
|
trusted
|
The proxies whose forwarded entries may be believed.
TYPE:
|
overwrite_scope_client
|
Also rewrite Off by default, because it discards the verified transport
peer that an audit record wants, and because a server that
already rewrites it would then be running the walk twice.
Turn it on for code that reads
TYPE:
|
app
instance-attribute
app = app
trusted
instance-attribute
trusted = trusted
overwrite_scope_client
instance-attribute
overwrite_scope_client = overwrite_scope_client
resolve_client_address
resolve_client_address(
scope: Scope, trusted: TrustedProxies
) -> ClientAddress | None
Resolve the client address a trusted proxy vouched for.
Returns None when the transport peer is absent or unparsable, which a caller must handle rather than being handed a fabricated address.
The header is read only when the peer itself is trusted, and the walk runs right to left, because the rightmost entries are the ones your own proxies wrote. The first entry that is not a trusted proxy is the client. A malformed entry stops the walk rather than being skipped, since skipping lets padding shift which entry is chosen.
When every entry is trusted, the peer is returned with
CHAIN_EXHAUSTED rather than the leftmost entry. No trusted proxy
vouched for that entry against an untrusted peer, so it is exactly
the value that must not become a rate limiter key.
| PARAMETER | DESCRIPTION |
|---|---|
scope
|
The ASGI scope of the request.
TYPE:
|
trusted
|
The proxies whose forwarded entries may be believed.
TYPE:
|