Skip to main content

resolve_client_ip

Function resolve_client_ip 

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

  1. X-Forwarded-For (rightmost entry of the last header field line – the address the nearest trusted proxy observed)
  2. X-Real-IP (used by some proxies like nginx)
  3. 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.