Skip to main content

Module client_ip

Module client_ip 

Source
Available on crate feature tower only.
Expand description

Client identity resolution for rate limiting behind proxies.

The rate-limit key derived from a request is attacker-controllable territory: any client can set X-Forwarded-For (XFF) and, if the service believes it blindly, mint a fresh GCRA budget per request (the threat model’s former OPEN-1, a complete bypass). This module resolves a client identity that is secure by default.

§Resolution algorithm (spec)

Given the request’s direct peer address (from ConnectInfo<SocketAddr> in the request extensions) and the ClientIpConfig:

  1. Direct peer NOT in trusted_proxies → client = peer IP (headers are lies). An empty trusted_proxies list therefore means the forwarded header is ignored entirely.
  2. Direct peer trusted → parse the forwarded header (XFF, or the trusted_header override), split on ,, trim each entry, walk RIGHT-TO-LEFT skipping num_trusted_hops entries (our proxy chain), and take the next entry leftward as the client. NEVER trust left-to-right: every entry left of the resolved one is attacker-controlled. Malformed header (missing, non-ASCII, or a chosen entry that is not an IP address), missing header, or chain shorter than num_trusted_hops → fall back to the peer IP and emit a tracing::warn!.
  3. No ConnectInfo available → the caller applies MissingClientPolicy (default MissingClientPolicy::Reject → fail closed with 503).

§What num_trusted_hops counts

The rightmost header entries were appended by our own trusted infrastructure (each trusted proxy appends the address it saw). num_trusted_hops is how many of those rightmost entries belong to our proxy chain in front of the direct peer and must be skipped:

  • internet → ALB → nginx → app (both ALB and nginx trusted, nginx is the direct peer): nginx appends the ALB’s address, so XFF is "client, alb-ip" and the default num_trusted_hops = 1 skips alb-ip, yielding client.
  • internet → nginx → app (nginx is the only proxy and the direct peer): XFF is "client"; set num_trusted_hops = 0 to take the rightmost entry. Leaving the default 1 here falls back to the peer (nginx’s) address — all clients then share one bucket. That is safe but coarse; never compensate by trusting entries further left than your own infrastructure actually appends.

Spoofed entries always sit LEFT of the entries our proxies appended, so a correctly configured hop count never reads them.

§IPv4-mapped IPv6

v4-mapped IPv6 addresses (::ffff:a.b.c.d) are treated as IPv4 everywhere: for IpNet membership and for the resolved identity. A dual-stack socket reporting the peer as ::ffff:203.0.113.9 thus matches a 203.0.113.0/24 trust entry and yields the key "203.0.113.9". They never match IPv6 networks.

§axum plumbing

The peer address comes from ConnectInfo<SocketAddr>, which axum only injects when the router is served with .into_make_service_with_connect_info::<SocketAddr>(). Without it, step 3 applies: requests are rejected (default) or share a fallback bucket.

Structs§

ClientIpConfig
Configuration for deriving the rate-limit key from the client IP.
MissingClientIdentity
No peer address was available (no ConnectInfo in the request extensions) — the caller applies its MissingClientPolicy.
ResolvedClient
A successfully resolved client identity.

Enums§

ClientIpError
Errors from IpNet::parse.
ClientIpSource
Where a resolved client identity came from.
IpNet
An IP network: CIDR prefix membership without any new dependency.
MissingClientPolicy
Policy for requests whose client identity cannot be resolved (algorithm step 3: no ConnectInfo in the request extensions).

Functions§

peer_ip_from_extensions
Extract the direct peer IP from axum’s ConnectInfo<SocketAddr> request extension, if present.
resolve_client_identity
Resolve the client identity for a request.
resolve_from_request
Convenience wrapper: resolve the client identity for a full request.