Skip to main content

Module client_address

Module client_address 

Source
Expand description

The client’s address: resolved once per request, before any guard, and forwarded to the upstream by an explicit policy.

§Resolution

The connection’s peer is the client, unless it is listed in trusted_proxies. Forwarding information is not trustworthy by itself (RFC 7239 §8.1), so only then is the header the trusted proxy reports in read:

  • X-Forwarded-For (the default): every field line, in order, as one list (RFC 9110 §5.3), walked from the right through the trusted proxies to the first address outside them. What lies left of that address is the client’s own claim and is never read. When every hop is trusted, the leftmost is the client.
  • X-Real-IP, when chosen: one address, the trusted proxy’s own peer.

The result is a ClientAddress on the request, and every consumer reads that one value: the rate limits, the auth decider, extra routes, the fallback and an upstream in process. When nothing can be resolved (no connection information, or a trusted proxy’s report that cannot be read) the Resolution says why, and no address is ever made up: a trusted proxy’s own address never stands in for its client’s.

§What the upstream receives

ForwardConfig decides, independently of the resolution:

x_forwarded_forX-Forwarded-For upstreamIts first element
verified (default)the client, the trusted proxies after it, this proxy’s peerthe client
resolvedthe client alonethe client
appendthe list as it arrived, then this proxy’s peerwhatever the client wrote
preservethe list as it arrived (X-Real-IP and Forwarded too)whatever the client wrote
removenothingnone

Next to it, client_header (X-Real-IP by default) carries the resolved address alone, and audit_header (off by default) the list exactly as it arrived, for logs. These headers are the proxy’s: the request cannot set them, and neither can a guard (JWT claim headers, ext_authz, the auth decider). Native gRPC, gRPC-Web and transcoded calls receive the same values, remote or in process, and so does the fallback.

With append and preserve the first element of X-Forwarded-For is whatever the client chose to write: an upstream should then read the client header, or walk the list from the right trusting this proxy.

§Examples

Behind a load balancer in 10.0.0.0/8, forwarding as nginx does with $proxy_add_x_forwarded_for, and keeping what arrived for the access log:

client_address:
  trusted_proxies: ["10.0.0.0/8"]
  forward:
    x_forwarded_for: append
    client_header: x-real-ip
    audit_header: x-original-forwarded-for

The same in code, with the address read back in a tonic handler:

use structured_proxy::client_address::Resolution;
use structured_proxy::config::{ClientAddressConfig, XForwardedFor};
use structured_proxy::{ClientAddress, ProxyServer};

let mut client_address = ClientAddressConfig::default();
client_address.trusted_proxies = vec!["10.0.0.0/8".into()];
client_address.forward.x_forwarded_for = XForwardedFor::Append;
client_address.forward.audit_header = Some("x-original-forwarded-for".into());
let service = ProxyServer::new()
    .with_client_address(client_address)
    .service(tonic::service::Routes::default())?;

fn caller(request: &tonic::Request<()>) -> String {
    match request.extensions().get::<ClientAddress>().map(ClientAddress::resolution) {
        Some(Resolution::Peer(ip) | Resolution::Forwarded(ip)) => ip.to_string(),
        _ => "unknown".into(),
    }
}

Routers of your own that mount proxy parts such as the shield middleware get the same resolution from ClientAddressLayer.

Structs§

ClientAddress
The client address the proxy resolved for a request.
ClientAddressLayer
Resolves the client address of every request before the service it wraps sees it: puts the ClientAddress on the request and rewrites its forwarding headers, as the proxy does before its own guards.
ClientAddressService
The service of ClientAddressLayer.

Enums§

InvalidForwarding
Why a trusted proxy’s forwarded address cannot be read.
Resolution
How the client address was resolved, or why it was not.