Skip to content

Client IP

  • Start here: Client IP guide
  • Common recipes: resolve_client_address(request.scope, trusted) returns the address a trusted proxy vouched for. ClientAddressMiddleware resolves 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: Iterable[str | IPAddress | IPNetwork]

max_hops

Most trusted proxies to walk past. Bounds a forged chain. None means no cap.

TYPE: int | None DEFAULT: None

max_entries

Most header entries to parse before giving up.

TYPE: int DEFAULT: 64

max_header_bytes

Most header bytes to parse before giving up.

TYPE: int DEFAULT: 8192

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

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: ASGIApp

trusted

The proxies whose forwarded entries may be believed.

TYPE: TrustedProxies

overwrite_scope_client

Also rewrite scope["client"] with the resolved address.

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 request.client.host and cannot be changed.

TYPE: bool DEFAULT: False

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: Scope

trusted

The proxies whose forwarded entries may be believed.

TYPE: TrustedProxies