pub fn resolve_client_ip(
headers: &HeaderMap,
extensions: &Extensions,
) -> Option<IpAddr>Expand description
Resolves the client IP address from HTTP headers and extensions.
This function checks sources in order of priority:
- X-Forwarded-For (rightmost entry of the last header field line – the address the nearest trusted proxy observed)
- X-Real-IP (used by some proxies like nginx)
- Socket address from extensions (direct connection)
The rightmost X-Forwarded-For entry is used, not the leftmost, because the AWS ALB every
service is deployed behind appends the address it observed rather than overwriting the
header (routing.http.xff_header_processing.mode = append, the ALB default). Every entry to
the left of the last one is caller-supplied and therefore spoofable; the last entry is the
ALB’s own observation and cannot be forged by the caller for requests that actually traversed
the ALB – this holds even if the caller sends its own X-Forwarded-For as a separate
header field line, since HeaderMap::get returns only the first such line and would surface a
fully caller-chosen value; get_all(...).iter().next_back() is required to reach the ALB’s
line. This is correct for exactly one trusted proxy hop – putting a second trusted proxy in front of the ALB
would mean skipping one more entry from the right. For a request that reaches this service
without going through the ALB (local dev, an in-cluster peer, or any other direct connection),
there is no trusted proxy appending anything, so the rightmost entry is just as caller-chosen
as every other entry and this guarantee does not apply – the socket-address fallback (branch
3) is the only non-forgeable source in that case.
Every candidate is validated as a parseable IpAddr before being returned; a header value
that fails to parse (e.g. malformed input or an attempted log-injection payload) is treated as
absent and the function falls through to the next source in priority order instead of
returning the raw text.
The resolved address is always run through IpAddr::to_canonical before being returned, so
an IPv4-mapped IPv6 form (::ffff:a.b.c.d) arriving from a dual-stack listener unmaps to
a.b.c.d here – the single normalization point, independent of listener configuration –
before an crate::ip_allowlist::IpAllowlist ever sees the address, so an allowlist entry of
a.b.c.d/32 matches either arrival form. This is a deliberate behavior change from the
get_client_ip this was moved from, which returned the un-normalized form; it also normalizes
the form logged for audit purposes.
Returns None if no IP can be extracted.