structured_proxy/client_address.rs
1//! The client's address: resolved once per request, before any guard, and
2//! forwarded to the upstream by an explicit policy.
3//!
4//! # Resolution
5//!
6//! The connection's peer is the client, unless it is listed in
7//! [`trusted_proxies`](ClientAddressConfig::trusted_proxies). Forwarding
8//! information is not trustworthy by itself (RFC 7239 §8.1), so only then is
9//! the header the trusted proxy reports in read:
10//!
11//! - `X-Forwarded-For` (the default): every field line, in order, as one list
12//! (RFC 9110 §5.3), walked from the right through the trusted proxies to the
13//! first address outside them. What lies left of that address is the
14//! client's own claim and is never read. When every hop is trusted, the
15//! leftmost is the client.
16//! - `X-Real-IP`, when chosen: one address, the trusted proxy's own peer.
17//!
18//! The result is a [`ClientAddress`] on the request, and every consumer reads
19//! that one value: the rate limits, the auth decider, extra routes, the
20//! fallback and an upstream in process. When nothing can be resolved (no
21//! connection information, or a trusted proxy's report that cannot be read)
22//! the [`Resolution`] says why, and no address is ever made up: a trusted
23//! proxy's own address never stands in for its client's.
24//!
25//! # What the upstream receives
26//!
27//! [`ForwardConfig`](crate::config::ForwardConfig) decides, independently of
28//! the resolution:
29//!
30//! | `x_forwarded_for` | `X-Forwarded-For` upstream | Its first element |
31//! |---|---|---|
32//! | `verified` (default) | the client, the trusted proxies after it, this proxy's peer | the client |
33//! | `resolved` | the client alone | the client |
34//! | `append` | the list as it arrived, then this proxy's peer | whatever the client wrote |
35//! | `preserve` | the list as it arrived (`X-Real-IP` and `Forwarded` too) | whatever the client wrote |
36//! | `remove` | nothing | none |
37//!
38//! Next to it, `client_header` (`X-Real-IP` by default) carries the resolved
39//! address alone, and `audit_header` (off by default) the list exactly as it
40//! arrived, for logs. These headers are the proxy's: the request cannot set
41//! them, and neither can a guard (JWT claim headers, ext_authz, the auth
42//! decider). Native gRPC, gRPC-Web and transcoded calls receive the same
43//! values, remote or in process, and so does the fallback.
44//!
45//! With `append` and `preserve` the first element of `X-Forwarded-For` is
46//! whatever the client chose to write: an upstream should then read the
47//! client header, or walk the list from the right trusting this proxy.
48//!
49//! # Examples
50//!
51//! Behind a load balancer in `10.0.0.0/8`, forwarding as nginx does with
52//! `$proxy_add_x_forwarded_for`, and keeping what arrived for the access log:
53//!
54//! ```yaml
55//! client_address:
56//! trusted_proxies: ["10.0.0.0/8"]
57//! forward:
58//! x_forwarded_for: append
59//! client_header: x-real-ip
60//! audit_header: x-original-forwarded-for
61//! ```
62//!
63//! The same in code, with the address read back in a tonic handler:
64//!
65//! ```
66//! use structured_proxy::client_address::Resolution;
67//! use structured_proxy::config::{ClientAddressConfig, XForwardedFor};
68//! use structured_proxy::{ClientAddress, ProxyServer};
69//!
70//! # fn build() -> anyhow::Result<()> {
71//! let mut client_address = ClientAddressConfig::default();
72//! client_address.trusted_proxies = vec!["10.0.0.0/8".into()];
73//! client_address.forward.x_forwarded_for = XForwardedFor::Append;
74//! client_address.forward.audit_header = Some("x-original-forwarded-for".into());
75//! let service = ProxyServer::new()
76//! .with_client_address(client_address)
77//! .service(tonic::service::Routes::default())?;
78//! # let _ = service;
79//! # Ok(())
80//! # }
81//! # build().unwrap();
82//!
83//! fn caller(request: &tonic::Request<()>) -> String {
84//! match request.extensions().get::<ClientAddress>().map(ClientAddress::resolution) {
85//! Some(Resolution::Peer(ip) | Resolution::Forwarded(ip)) => ip.to_string(),
86//! _ => "unknown".into(),
87//! }
88//! }
89//! # let _ = caller;
90//! ```
91//!
92//! Routers of your own that mount proxy parts such as the
93//! [`shield`](crate::shield) middleware get the same resolution from
94//! [`ClientAddressLayer`].
95
96use std::net::{IpAddr, Ipv6Addr, SocketAddr};
97use std::sync::Arc;
98use std::task::{Context, Poll};
99
100use axum::extract::ConnectInfo;
101use http::header::{HeaderName, FORWARDED};
102use http::{HeaderMap, HeaderValue};
103use ipnet::IpNet;
104use tower::{Layer, Service};
105
106use crate::config::{ClientAddressConfig, ForwardingHeader, XForwardedFor};
107
108/// `X-Forwarded-For`.
109pub(crate) const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
110/// `X-Real-IP`.
111pub(crate) const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip");
112
113/// Whether `name` is a header carrying a client's address: `X-Forwarded-For`,
114/// `X-Real-IP` or `Forwarded`. The forwarding policy decides what they hold
115/// upstream, so no guard (claim headers, ext_authz, the auth decider) may set
116/// them.
117pub(crate) fn owns(name: &str) -> bool {
118 name.eq_ignore_ascii_case("x-forwarded-for")
119 || name.eq_ignore_ascii_case("x-real-ip")
120 || name.eq_ignore_ascii_case("forwarded")
121}
122
123/// Most list elements of `X-Forwarded-For` read before an address outside the
124/// trusted proxies turns up. Only trusted hops and empty elements count
125/// towards it, and no real chain of proxies comes near it.
126const MAX_HOPS: usize = 32;
127
128/// Longest list element read, spaces around it included. The longest address
129/// with a port, `[ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255]:65535`, is 53
130/// bytes; anything longer is not an address. With [`MAX_HOPS`] it bounds the
131/// bytes of a header read at all, whatever its size.
132const MAX_ELEMENT: usize = 128;
133
134/// The client address the proxy resolved for a request.
135///
136/// A request extension on every request the proxy serves: an
137/// [`AuthDecider`](crate::hooks::AuthDecider) and an
138/// [`ExtraRouteHandler`](crate::hooks::ExtraRouteHandler) get it in their
139/// request view, a fallback and a tonic upstream in process read it with
140/// `request.extensions().get::<ClientAddress>()`. The connection's own record
141/// (`Request::remote_addr`, `Request::peer_certs`) stays as it is.
142///
143/// # Examples
144///
145/// ```
146/// use structured_proxy::client_address::{ClientAddress, Resolution};
147///
148/// // What a tonic handler behind the proxy does with it.
149/// fn caller(request: &tonic::Request<()>) -> Option<std::net::IpAddr> {
150/// request.extensions().get::<ClientAddress>()?.ip()
151/// }
152///
153/// let client = ClientAddress::from_peer(Some("203.0.113.7:51234".parse().unwrap()));
154/// assert_eq!(client.resolution(), Resolution::Peer("203.0.113.7".parse().unwrap()));
155/// let mut request = tonic::Request::new(());
156/// request.extensions_mut().insert(client);
157/// assert_eq!(caller(&request), Some("203.0.113.7".parse().unwrap()));
158/// ```
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct ClientAddress {
161 peer: Option<SocketAddr>,
162 resolution: Resolution,
163 /// The resolved address as a header value, made once for every header
164 /// and metadata entry that carries it.
165 value: Option<HeaderValue>,
166}
167
168/// How the client address was resolved, or why it was not.
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170#[non_exhaustive]
171pub enum Resolution {
172 /// The connection's peer is the client: it is not a trusted proxy, or it
173 /// is one that forwarded no address.
174 Peer(IpAddr),
175 /// A trusted proxy forwarded the address: the first one outside the
176 /// trusted proxies, read right to left, or the leftmost when every hop is
177 /// trusted.
178 Forwarded(IpAddr),
179 /// A trusted proxy forwarded something that cannot be read where the
180 /// client's address should be. No address resolves: the peer is a proxy,
181 /// not the client.
182 Invalid(InvalidForwarding),
183 /// The server recorded no connection information, so there is no peer to
184 /// start from.
185 Unavailable,
186}
187
188/// Why a trusted proxy's forwarded address cannot be read.
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190#[non_exhaustive]
191pub enum InvalidForwarding {
192 /// An element that is not an IP address with an optional port.
193 Malformed,
194 /// More trusted hops than the proxy reads before reaching the client.
195 TooManyHops,
196 /// More than one `X-Real-IP` field.
197 Repeated,
198}
199
200impl std::fmt::Display for InvalidForwarding {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.write_str(match self {
203 Self::Malformed => "malformed",
204 Self::TooManyHops => "beyond too many trusted hops",
205 Self::Repeated => "repeated",
206 })
207 }
208}
209
210impl ClientAddress {
211 /// The address of a request from `peer` through no trusted proxy: the
212 /// peer itself, or [`Resolution::Unavailable`] without one. The view a
213 /// hook's own tests hand it.
214 pub fn from_peer(peer: Option<SocketAddr>) -> Self {
215 let resolution = match peer {
216 Some(peer) => Resolution::Peer(peer.ip().to_canonical()),
217 None => Resolution::Unavailable,
218 };
219 Self::new(peer, resolution)
220 }
221
222 fn new(peer: Option<SocketAddr>, resolution: Resolution) -> Self {
223 let value = match resolution {
224 Resolution::Peer(ip) | Resolution::Forwarded(ip) => Some(header_value(ip)),
225 Resolution::Invalid(_) | Resolution::Unavailable => None,
226 };
227 Self {
228 peer,
229 resolution,
230 value,
231 }
232 }
233
234 /// The client's address, when one resolved.
235 pub fn ip(&self) -> Option<IpAddr> {
236 match self.resolution {
237 Resolution::Peer(ip) | Resolution::Forwarded(ip) => Some(ip),
238 Resolution::Invalid(_) | Resolution::Unavailable => None,
239 }
240 }
241
242 /// The connection's peer: the client, or the last proxy before this one.
243 pub fn peer(&self) -> Option<SocketAddr> {
244 self.peer
245 }
246
247 /// How the address was resolved, or why it was not.
248 pub fn resolution(&self) -> Resolution {
249 self.resolution
250 }
251
252 /// The resolved address as a header value: the client header's, and
253 /// `X-Forwarded-For`'s in the `resolved` mode.
254 pub(crate) fn header_value(&self) -> Option<&HeaderValue> {
255 self.value.as_ref()
256 }
257}
258
259/// `ip` as a header value: one allocation, for the bytes the value owns.
260fn header_value(ip: IpAddr) -> HeaderValue {
261 use std::io::Write;
262 // The longest text form, an IPv6 address ending in IPv4 notation, is 45
263 // bytes.
264 let mut buf = [0u8; 48];
265 let mut rest: &mut [u8] = &mut buf;
266 write!(rest, "{ip}").expect("an IP address fits in 48 bytes");
267 let len = 48 - rest.len();
268 HeaderValue::from_bytes(&buf[..len]).expect("an IP address is a header value")
269}
270
271/// [`ClientAddressConfig`], compiled.
272#[derive(Debug, Default)]
273pub(crate) struct Resolver {
274 trusted: Box<[IpNet]>,
275 header: ForwardingHeader,
276 required: bool,
277 forward: XForwardedFor,
278 /// Receives the resolved address.
279 client_header: Option<HeaderName>,
280 /// Receives the request's `X-Forwarded-For` as it arrived.
281 audit_header: Option<HeaderName>,
282}
283
284impl Resolver {
285 /// # Errors
286 ///
287 /// A `trusted_proxies` entry that is neither a CIDR range nor an address,
288 /// or a `forward` header name that is not a gRPC metadata key or names a
289 /// header the forwarding already writes.
290 pub(crate) fn build(config: &ClientAddressConfig) -> Result<Self, String> {
291 let trusted = config
292 .trusted_proxies
293 .iter()
294 .map(|entry| parse_trusted(entry))
295 .collect::<Result<_, _>>()?;
296 let forward = &config.forward;
297 let client_header = forward
298 .client_header
299 .as_deref()
300 .map(|name| forward_header("client_header", name, &["x-forwarded-for", "forwarded"]))
301 .transpose()?;
302 let audit_header = forward
303 .audit_header
304 .as_deref()
305 .map(|name| {
306 let mut taken = vec!["x-forwarded-for", "x-real-ip", "forwarded"];
307 taken.extend(client_header.as_ref().map(HeaderName::as_str));
308 forward_header("audit_header", name, &taken)
309 })
310 .transpose()?;
311 Ok(Self {
312 trusted,
313 header: config.header,
314 required: config.required,
315 forward: forward.x_forwarded_for,
316 client_header,
317 audit_header,
318 })
319 }
320
321 /// Whether a request whose address does not resolve is refused.
322 pub(crate) fn required(&self) -> bool {
323 self.required
324 }
325
326 /// Whether the proxy alone writes `name`: a client-address header, or a
327 /// header the forwarding policy writes. A guard may not set it.
328 pub(crate) fn reserves(&self, name: &str) -> bool {
329 owns(name)
330 || self
331 .configured_headers()
332 .any(|own| name.eq_ignore_ascii_case(own.as_str()))
333 }
334
335 /// The headers named in `forward`, the ones [`owns`] does not cover.
336 pub(crate) fn configured_headers(&self) -> impl Iterator<Item = &HeaderName> {
337 self.client_header.iter().chain(&self.audit_header)
338 }
339
340 /// Every header the forwarding policy may leave on a request: what a
341 /// transcoded call forwards as metadata in their place.
342 pub(crate) fn forwarded_headers(&self) -> impl Iterator<Item = &str> {
343 ["x-forwarded-for", "x-real-ip", "forwarded"]
344 .into_iter()
345 .chain(self.configured_headers().map(HeaderName::as_str))
346 }
347
348 fn trusts(&self, ip: IpAddr) -> bool {
349 self.trusted.iter().any(|net| net.contains(&ip))
350 }
351
352 /// The client address of a request from `peer` carrying `headers`, without
353 /// touching the request: what [`apply`](Self::apply) puts on it.
354 #[cfg(test)]
355 pub(crate) fn resolve(&self, peer: Option<SocketAddr>, headers: &HeaderMap) -> ClientAddress {
356 self.resolve_noting(peer, headers, None)
357 }
358
359 /// The client address of a request from `peer` carrying `headers` (what
360 /// [`apply`](Self::apply) puts on it), noting in `hops` the trusted
361 /// proxies between the client and this proxy, nearest first.
362 fn resolve_noting(
363 &self,
364 peer: Option<SocketAddr>,
365 headers: &HeaderMap,
366 hops: Option<&mut Vec<IpAddr>>,
367 ) -> ClientAddress {
368 let Some(peer) = peer else {
369 return ClientAddress::new(None, Resolution::Unavailable);
370 };
371 let peer_ip = peer.ip().to_canonical();
372 if !self.trusts(peer_ip) {
373 // Whatever an untrusted peer forwarded is its own assertion.
374 return ClientAddress::new(Some(peer), Resolution::Peer(peer_ip));
375 }
376 let forwarded = match self.header {
377 ForwardingHeader::XForwardedFor => {
378 self.walk(headers.get_all(X_FORWARDED_FOR).iter(), hops)
379 }
380 ForwardingHeader::XRealIp => real_ip(headers.get_all(X_REAL_IP).iter()),
381 };
382 let resolution = match forwarded {
383 Ok(Some(ip)) => Resolution::Forwarded(ip),
384 Ok(None) => Resolution::Peer(peer_ip),
385 Err(invalid) => Resolution::Invalid(invalid),
386 };
387 ClientAddress::new(Some(peer), resolution)
388 }
389
390 /// Resolve the address of `request` from `peer`, put it on the request as
391 /// a [`ClientAddress`], and rewrite its forwarding headers by the
392 /// `forward` policy, so every consumer past this point (the guards, the
393 /// fallback, the upstream) sees exactly what the upstream will:
394 ///
395 /// - the audit header, when set, gets the request's `X-Forwarded-For`
396 /// lines as they arrived, and nothing the client sent under its name;
397 /// - `X-Forwarded-For` as the mode says (see [`XForwardedFor`]);
398 /// - `X-Real-IP` and `Forwarded` as they arrived with `preserve`, removed
399 /// otherwise, since they would contradict it;
400 /// - the client header, last, the resolved address, so it overrides even
401 /// a preserved `X-Real-IP`; absent when nothing resolved.
402 pub(crate) fn apply<B>(&self, request: &mut http::Request<B>, peer: Option<SocketAddr>) {
403 let mut hops = Vec::new();
404 let noting = (self.forward == XForwardedFor::Verified).then_some(&mut hops);
405 let client = self.resolve_noting(peer, request.headers(), noting);
406 let headers = request.headers_mut();
407 if let Some(audit) = &self.audit_header {
408 let arrived: Vec<HeaderValue> =
409 headers.get_all(X_FORWARDED_FOR).iter().cloned().collect();
410 headers.remove(audit);
411 for line in arrived {
412 headers.append(audit.clone(), line);
413 }
414 }
415 let xff = match self.forward {
416 XForwardedFor::Verified => verified_chain(&client, &hops),
417 XForwardedFor::Resolved => client.header_value().cloned(),
418 XForwardedFor::Append => appended_chain(headers, client.peer),
419 XForwardedFor::Preserve => None,
420 XForwardedFor::Remove => None,
421 };
422 if self.forward != XForwardedFor::Preserve {
423 headers.remove(X_FORWARDED_FOR);
424 headers.remove(X_REAL_IP);
425 headers.remove(FORWARDED);
426 if let Some(xff) = xff {
427 headers.insert(X_FORWARDED_FOR, xff);
428 }
429 }
430 if let Some(name) = &self.client_header {
431 headers.remove(name);
432 if let Some(value) = client.header_value() {
433 headers.insert(name.clone(), value.clone());
434 }
435 }
436 request.extensions_mut().insert(client);
437 }
438
439 /// The client's address in `X-Forwarded-For`, `lines` in wire order
440 /// (RFC 9110 §5.3: several field lines form one list): the first element
441 /// outside the trusted proxies from the right, or the leftmost when all
442 /// are trusted, or `None` without an element. Elements left of the client
443 /// are the client's own assertions and are never read. An element that
444 /// is not an address where the walk needs one makes the whole result
445 /// invalid: skipping it would credit a hop the chain never vouched for.
446 /// `hops`, when given, receives the trusted proxies right of the client,
447 /// nearest first.
448 fn walk<'a>(
449 &self,
450 lines: impl DoubleEndedIterator<Item = &'a HeaderValue>,
451 mut hops: Option<&mut Vec<IpAddr>>,
452 ) -> Result<Option<IpAddr>, InvalidForwarding> {
453 let mut read = 0;
454 let mut leftmost = None;
455 for line in lines.rev() {
456 let mut rest = line.as_bytes();
457 loop {
458 read += 1;
459 if read > MAX_HOPS {
460 return Err(InvalidForwarding::TooManyHops);
461 }
462 let (element, before) = last_element(rest)?;
463 // RFC 9110 §5.6.1: a recipient ignores empty list elements.
464 if !element.is_empty() {
465 let ip = parse_address(element)
466 .ok_or(InvalidForwarding::Malformed)?
467 .to_canonical();
468 if !self.trusts(ip) {
469 return Ok(Some(ip));
470 }
471 if let Some(hops) = hops.as_deref_mut() {
472 hops.push(ip);
473 }
474 leftmost = Some(ip);
475 }
476 match before {
477 Some(before) => rest = before,
478 None => break,
479 }
480 }
481 }
482 // Every hop trusted: the leftmost is the client, not a hop.
483 if leftmost.is_some() {
484 if let Some(hops) = hops {
485 hops.pop();
486 }
487 }
488 Ok(leftmost)
489 }
490}
491
492/// The verified `X-Forwarded-For`: the resolved client, the trusted proxies
493/// `hops` (nearest first) in the order they forwarded, then this proxy's peer,
494/// each in canonical form; `None` when nothing resolved. A client that is the
495/// peer itself is listed once.
496fn verified_chain(client: &ClientAddress, hops: &[IpAddr]) -> Option<HeaderValue> {
497 use std::fmt::Write;
498 let value = client.header_value()?;
499 let Resolution::Forwarded(ip) = client.resolution else {
500 // The peer is the client: nothing else is verified.
501 return Some(value.clone());
502 };
503 let peer = client.peer?.ip().to_canonical();
504 let mut chain = ip.to_string();
505 for hop in hops.iter().rev().chain([&peer]) {
506 write!(chain, ", {hop}").expect("writing to a String cannot fail");
507 }
508 Some(HeaderValue::try_from(chain).expect("addresses and commas are a header value"))
509}
510
511/// The request's `X-Forwarded-For` as it arrived, its field lines joined in
512/// order, with `peer` appended; the lines alone without a peer, and `None`
513/// with neither.
514fn appended_chain(headers: &HeaderMap, peer: Option<SocketAddr>) -> Option<HeaderValue> {
515 use std::io::Write;
516 let mut lines = headers.get_all(X_FORWARDED_FOR).iter();
517 let Some(peer) = peer else {
518 // Nothing to append: one line stays as it is, several are joined.
519 let first = lines.next()?;
520 let mut joined = first.as_bytes().to_vec();
521 for line in lines {
522 joined.extend_from_slice(b", ");
523 joined.extend_from_slice(line.as_bytes());
524 }
525 return Some(
526 HeaderValue::from_maybe_shared(bytes::Bytes::from(joined))
527 .expect("joined header values are a header value"),
528 );
529 };
530 let mut chain = Vec::new();
531 for line in lines {
532 chain.extend_from_slice(line.as_bytes());
533 chain.extend_from_slice(b", ");
534 }
535 write!(chain, "{}", peer.ip().to_canonical()).expect("writing to a Vec cannot fail");
536 Some(
537 HeaderValue::from_maybe_shared(bytes::Bytes::from(chain))
538 .expect("header values, commas and an address are a header value"),
539 )
540}
541
542/// A `forward` header name: a text gRPC metadata key, so a transcoded call can
543/// carry it, other than the headers in `taken`. A `-bin` key would need base64
544/// values (gRPC PROTOCOL-HTTP2, "Custom-Metadata"), and an address or an
545/// address list is text.
546fn forward_header(setting: &str, name: &str, taken: &[&str]) -> Result<HeaderName, String> {
547 let header = HeaderName::from_bytes(name.as_bytes())
548 .ok()
549 .filter(|header| crate::transcode::metadata::is_grpc_key(header.as_str()))
550 .ok_or_else(|| {
551 format!("forward.{setting} {name:?} is not a gRPC metadata key (letters, digits, '_', '-' and '.')")
552 })?;
553 if header.as_str().ends_with("-bin") {
554 return Err(format!(
555 "forward.{setting} {name:?} is a binary metadata key, whose values must be base64; \
556 the address is text"
557 ));
558 }
559 if taken.contains(&header.as_str()) {
560 return Err(format!(
561 "forward.{setting} {name:?} names a header the forwarding already writes"
562 ));
563 }
564 Ok(header)
565}
566
567/// The last element of the list `bytes`, trimmed of the spaces and tabs
568/// around it (RFC 9110 §5.6.1), and what precedes its comma, if any. At most
569/// [`MAX_ELEMENT`] bytes are read.
570fn last_element(bytes: &[u8]) -> Result<(&[u8], Option<&[u8]>), InvalidForwarding> {
571 let window = bytes.len().saturating_sub(MAX_ELEMENT + 1);
572 let (element, before) = match bytes[window..].iter().rposition(|&b| b == b',') {
573 Some(comma) => (&bytes[window + comma + 1..], Some(&bytes[..window + comma])),
574 None if window == 0 => (bytes, None),
575 None => return Err(InvalidForwarding::Malformed),
576 };
577 Ok((trim_whitespace(element), before))
578}
579
580fn trim_whitespace(bytes: &[u8]) -> &[u8] {
581 let is_ows = |b: &u8| matches!(b, b' ' | b'\t');
582 let start = bytes.iter().position(|b| !is_ows(b)).unwrap_or(bytes.len());
583 let end = bytes
584 .iter()
585 .rposition(|b| !is_ows(b))
586 .map_or(start, |end| end + 1);
587 &bytes[start..end]
588}
589
590/// The client's address in `X-Real-IP`: one field with one address, written
591/// by the trusted proxy for its own peer, or `None` without the field.
592fn real_ip<'a>(
593 mut lines: impl Iterator<Item = &'a HeaderValue>,
594) -> Result<Option<IpAddr>, InvalidForwarding> {
595 let Some(line) = lines.next() else {
596 return Ok(None);
597 };
598 if lines.next().is_some() {
599 return Err(InvalidForwarding::Repeated);
600 }
601 // Bounded before trimming, so the spaces around the address count too and
602 // no value is read past `MAX_ELEMENT`, as for an `X-Forwarded-For` element.
603 let raw = line.as_bytes();
604 if raw.len() > MAX_ELEMENT {
605 return Err(InvalidForwarding::Malformed);
606 }
607 parse_address(trim_whitespace(raw))
608 .map(|ip| Some(ip.to_canonical()))
609 .ok_or(InvalidForwarding::Malformed)
610}
611
612/// An address as forwarding headers carry it: bare, IPv6 in brackets, or
613/// either with a port, which is dropped. Nothing else, so a name, `unknown`
614/// or an obfuscated identifier (RFC 7239 §6.3) never counts as an address.
615fn parse_address(bytes: &[u8]) -> Option<IpAddr> {
616 let text = std::str::from_utf8(bytes).ok()?;
617 if let Ok(ip) = text.parse::<IpAddr>() {
618 return Some(ip);
619 }
620 if let Ok(socket) = text.parse::<SocketAddr>() {
621 return Some(socket.ip());
622 }
623 let v6 = text.strip_prefix('[')?.strip_suffix(']')?;
624 v6.parse::<Ipv6Addr>().ok().map(IpAddr::V6)
625}
626
627/// A `trusted_proxies` entry: a CIDR range, or an address for one host.
628fn parse_trusted(entry: &str) -> Result<IpNet, String> {
629 let net = match entry.parse::<IpNet>() {
630 Ok(net) => net,
631 Err(_) => match entry.parse::<IpAddr>() {
632 Ok(ip) => IpNet::from(ip),
633 Err(_) => {
634 return Err(format!(
635 "trusted_proxies entry {entry:?} is neither a CIDR range nor an IP address"
636 ))
637 }
638 },
639 };
640 Ok(canonical_net(net))
641}
642
643/// `net` in the form peers are compared in: a range inside the IPv4-mapped
644/// block `::ffff:0:0/96` (RFC 4291 §2.5.5.2) becomes the IPv4 range it maps,
645/// since peers are canonicalized to IPv4 before matching. A wider IPv6 range
646/// stays IPv6, so trusting `::/0` does not trust every IPv4 peer.
647fn canonical_net(net: IpNet) -> IpNet {
648 if let IpNet::V6(v6) = net {
649 if let (Some(v4), Some(prefix)) = (
650 v6.network().to_ipv4_mapped(),
651 v6.prefix_len().checked_sub(96),
652 ) {
653 return ipnet::Ipv4Net::new(v4, prefix)
654 .expect("a prefix of at most 128 - 96 fits IPv4")
655 .into();
656 }
657 }
658 net
659}
660
661/// Resolves the client address of every request before the service it wraps
662/// sees it: puts the [`ClientAddress`] on the request and rewrites its
663/// forwarding headers, as the proxy does before its own guards.
664///
665/// The proxy applies it itself. Use it in a router of your own that mounts
666/// proxy parts such as the [`shield`](crate::shield) middleware directly, so
667/// they key by the client behind your trusted proxies rather than by the
668/// proxies' own address. The peer comes from the `ConnectInfo<SocketAddr>` an
669/// axum server records (`into_make_service_with_connect_info`); without one
670/// the address is [`Resolution::Unavailable`]. `required` is not enforced
671/// here: read [`ClientAddress::resolution`] where it matters.
672///
673/// # Examples
674///
675/// ```
676/// use structured_proxy::client_address::ClientAddressLayer;
677/// use structured_proxy::config::ClientAddressConfig;
678///
679/// let mut trust = ClientAddressConfig::default();
680/// trust.trusted_proxies = vec!["10.0.0.0/8".into()];
681/// let app: axum::Router = axum::Router::new()
682/// .route("/", axum::routing::get(|| async { "ok" }))
683/// .layer(ClientAddressLayer::new(&trust).unwrap());
684/// # let _ = app;
685/// ```
686#[derive(Clone, Debug)]
687pub struct ClientAddressLayer {
688 resolver: Arc<Resolver>,
689}
690
691impl ClientAddressLayer {
692 /// The resolution `config` describes.
693 ///
694 /// # Errors
695 ///
696 /// A `trusted_proxies` entry that is neither a CIDR range nor an address.
697 pub fn new(config: &ClientAddressConfig) -> anyhow::Result<Self> {
698 let resolver = Resolver::build(config)
699 .map_err(|e| anyhow::anyhow!("invalid client_address config: {e}"))?;
700 Ok(Self::with(Arc::new(resolver)))
701 }
702
703 /// The layer over a resolver the proxy already compiled.
704 pub(crate) fn with(resolver: Arc<Resolver>) -> Self {
705 Self { resolver }
706 }
707}
708
709impl<S> Layer<S> for ClientAddressLayer {
710 type Service = ClientAddressService<S>;
711
712 fn layer(&self, inner: S) -> ClientAddressService<S> {
713 ClientAddressService {
714 inner,
715 resolver: self.resolver.clone(),
716 }
717 }
718}
719
720/// The service of [`ClientAddressLayer`].
721#[derive(Clone, Debug)]
722pub struct ClientAddressService<S> {
723 inner: S,
724 resolver: Arc<Resolver>,
725}
726
727impl<S, B> Service<http::Request<B>> for ClientAddressService<S>
728where
729 S: Service<http::Request<B>>,
730{
731 type Response = S::Response;
732 type Error = S::Error;
733 type Future = S::Future;
734
735 #[inline]
736 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
737 self.inner.poll_ready(cx)
738 }
739
740 fn call(&mut self, mut request: http::Request<B>) -> Self::Future {
741 let peer = request
742 .extensions()
743 .get::<ConnectInfo<SocketAddr>>()
744 .map(|ConnectInfo(peer)| *peer);
745 self.resolver.apply(&mut request, peer);
746 self.inner.call(request)
747 }
748}
749
750#[cfg(test)]
751mod tests;