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:
- Direct peer NOT in
trusted_proxies→ client = peer IP (headers are lies). An emptytrusted_proxieslist therefore means the forwarded header is ignored entirely. - Direct peer trusted → parse the forwarded header (
XFF, or thetrusted_headeroverride), split on,, trim each entry, walk RIGHT-TO-LEFT skippingnum_trusted_hopsentries (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 thannum_trusted_hops→ fall back to the peer IP and emit atracing::warn!. - No
ConnectInfoavailable → the caller appliesMissingClientPolicy(defaultMissingClientPolicy::Reject→ fail closed with503).
§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 defaultnum_trusted_hops = 1skipsalb-ip, yieldingclient.internet → nginx → app(nginx is the only proxy and the direct peer): XFF is"client"; setnum_trusted_hops = 0to take the rightmost entry. Leaving the default1here 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§
- Client
IpConfig - Configuration for deriving the rate-limit key from the client IP.
- Missing
Client Identity - No peer address was available (no
ConnectInfoin the request extensions) — the caller applies itsMissingClientPolicy. - Resolved
Client - A successfully resolved client identity.
Enums§
- Client
IpError - Errors from
IpNet::parse. - Client
IpSource - Where a resolved client identity came from.
- IpNet
- An IP network: CIDR prefix membership without any new dependency.
- Missing
Client Policy - Policy for requests whose client identity cannot be resolved
(algorithm step 3: no
ConnectInfoin 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.