Skip to main content

parse_net/
forwarded.rs

1//! `Forwarded` (RFC 7239) and the de facto `X-Forwarded-For`,
2//! `X-Forwarded-Proto`, and `X-Forwarded-Host`, with client-address
3//! resolution through trusted proxies.
4//!
5//! The leftmost `X-Forwarded-For` address is whatever the client typed; only
6//! addresses appended by proxies you trust mean anything. [`client_address`]
7//! ignores the header unless the connection's peer is trusted, walks the
8//! chain from the right, and stops at the first address it cannot trust. A
9//! malformed chain fails closed: use the peer address.
10//!
11//! # Examples
12//!
13//! ```
14//! use core::net::IpAddr;
15//! use parse_net::{self as pn, ClientAddress, ForwardedSource};
16//!
17//! let proxy: IpAddr = "10.0.0.2".parse()?;
18//! let inner: IpAddr = "10.0.0.1".parse()?;
19//! let trusted = |a: IpAddr| a == proxy || a == inner;
20//! let xff: [&[u8]; 1] = [b"203.0.113.9, 198.51.100.7, 10.0.0.1"];
21//! let ip = pn::client_address(proxy, ForwardedSource::XForwardedFor(&xff), &trusted, 16)?;
22//! assert_eq!(ip, ClientAddress::Forwarded { addr: "198.51.100.7".parse()?, hops: 2 });
23//!
24//! // The same header from an untrusted peer is ignored entirely.
25//! let stranger: IpAddr = "192.0.2.1".parse()?;
26//! assert_eq!(pn::client_address(stranger, ForwardedSource::XForwardedFor(&xff), &trusted, 16)?,
27//!            ClientAddress::Peer(stranger));
28//! # Ok::<(), Box<dyn std::error::Error>>(())
29//! ```
30
31use core::{
32    net::{IpAddr, Ipv4Addr, Ipv6Addr},
33    str::FromStr,
34};
35
36use crate::{
37    class,
38    error::{ErrorKind, ParseError, Result, err},
39    host::{HostPort, host_port},
40    limits::Limits,
41    list,
42    scan::control_kind,
43    text::{Word, parse_word, skip_ows, trim_ows},
44};
45
46/// Decides whether an address belongs to a proxy you trust.
47///
48/// Implemented for every `Fn(IpAddr) -> bool`; a deployment's CIDR sets
49/// (net-io, bouncer-io configuration) implement it directly.
50pub trait Trust {
51    /// Whether `addr` is a trusted proxy.
52    fn is_trusted(&self, addr: IpAddr) -> bool;
53}
54
55impl<F: Fn(IpAddr) -> bool> Trust for F {
56    fn is_trusted(&self, addr: IpAddr) -> bool {
57        self(addr)
58    }
59}
60
61/// Which header the deployment reads the chain from: every field line of
62/// it, in order. The two are never merged (merging is a parser
63/// differential: two proxies could disagree on the order).
64#[derive(Clone, Copy, Debug)]
65pub enum ForwardedSource<'a> {
66    /// `Forwarded` field lines.
67    Forwarded(&'a [&'a [u8]]),
68    /// `X-Forwarded-For` field lines.
69    XForwardedFor(&'a [&'a [u8]]),
70}
71
72/// The client address a request is attributed to.
73#[non_exhaustive]
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75pub enum ClientAddress {
76    /// The connection's peer: it is not a trusted proxy (its header was not
77    /// read), or it is a trusted proxy that forwarded no chain.
78    Peer(IpAddr),
79    /// An address from the chain, found after walking `hops` elements from
80    /// the right.
81    Forwarded {
82        /// The client address.
83        addr: IpAddr,
84        /// The elements walked, the returned one included.
85        hops: u8,
86    },
87    /// The walk reached `unknown`, an obfuscated identifier, or an element
88    /// without `for` after `hops` elements: nothing to its left can be
89    /// trusted.
90    Unknown {
91        /// The elements walked, the unknown one included.
92        hops: u8,
93    },
94}
95
96/// A node port: a number or an obfuscated identifier.
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub enum NodePort<'a> {
99    /// A port number.
100    Number(u16),
101    /// An obfuscated port (`_` and letters, digits, `.`, `_`, `-`).
102    Obfuscated(&'a [u8]),
103}
104
105/// A `for` or `by` node (RFC 7239 section 6).
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum Node<'a> {
108    /// An IP address.
109    Addr(IpAddr, Option<NodePort<'a>>),
110    /// `unknown`.
111    Unknown(Option<NodePort<'a>>),
112    /// An obfuscated identifier.
113    Obfuscated(&'a [u8], Option<NodePort<'a>>),
114}
115
116/// The protocol a request arrived over at the proxy.
117#[non_exhaustive]
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
119pub enum Proto {
120    /// `http`.
121    Http,
122    /// `https`.
123    Https,
124}
125
126/// One `Forwarded` element.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
128pub struct Element<'a> {
129    /// `for=`: the client of this hop.
130    pub for_: Option<Node<'a>>,
131    /// `by=`: the proxy's receiving interface.
132    pub by: Option<Node<'a>>,
133    /// `host=`: the `Host` the proxy received.
134    pub host: Option<HostPort<'a>>,
135    /// `proto=`.
136    pub proto: Option<Proto>,
137}
138
139/// `"_" 1*( ALPHA / DIGIT / "." / "_" / "-" )`.
140fn is_obfuscated(s: &[u8]) -> bool {
141    matches!(s, [b'_', rest @ ..] if !rest.is_empty()
142        && rest.iter().all(|&b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-')))
143}
144
145fn node_port(s: &[u8]) -> Option<NodePort<'_>> {
146    if is_obfuscated(s) {
147        return Some(NodePort::Obfuscated(s));
148    }
149    if s.is_empty() || s.len() > 5 || !s.iter().all(u8::is_ascii_digit) {
150        return None;
151    }
152    let n = s.iter().fold(0u32, |n, &d| n * 10 + u32::from(d - b'0'));
153    u16::try_from(n).ok().map(NodePort::Number)
154}
155
156/// An IP literal: dotted-decimal IPv4 (no leading zeros) or IPv6.
157fn ip(text: &[u8], v6: bool) -> Option<IpAddr> {
158    let t = core::str::from_utf8(text).ok()?;
159    if v6 {
160        Ipv6Addr::from_str(t).ok().map(IpAddr::V6)
161    } else {
162        // `core` refuses leading zeros, so `01.2.3.4` cannot pass.
163        Ipv4Addr::from_str(t).ok().map(IpAddr::V4)
164    }
165}
166
167/// Parses a node value (already unquoted).
168fn node(v: &[u8]) -> Result<Node<'_>> {
169    let (name, rest) = if v.first() == Some(&b'[') {
170        match v.iter().position(|&b| b == b']') {
171            Some(close) => (
172                v.get(..=close).unwrap_or(&[]),
173                v.get(close + 1..).unwrap_or(&[]),
174            ),
175            None => return err(ErrorKind::InvalidAddress, 0),
176        }
177    } else {
178        match v.iter().position(|&b| b == b':') {
179            Some(c) => (v.get(..c).unwrap_or(&[]), v.get(c..).unwrap_or(&[])),
180            None => (v, &[][..]),
181        }
182    };
183    let port = match rest.split_first() {
184        None => None,
185        Some((b':', p)) => match node_port(p) {
186            Some(p) => Some(p),
187            None => return err(ErrorKind::InvalidPort, name.len() + 1),
188        },
189        Some(_) => return err(ErrorKind::InvalidAddress, name.len()),
190    };
191    if name == b"unknown" {
192        return Ok(Node::Unknown(port));
193    }
194    if is_obfuscated(name) {
195        return Ok(Node::Obfuscated(name, port));
196    }
197    let addr = match name {
198        [b'[', inner @ .., b']'] => ip(inner, true),
199        _ => ip(name, false),
200    };
201    match addr {
202        Some(a) => Ok(Node::Addr(a, port)),
203        None => err(ErrorKind::InvalidAddress, 0),
204    }
205}
206
207/// Parses one `forwarded-element`: `[ pair ] *( ";" [ pair ] )`, OWS around
208/// `;` accepted, names case-insensitive, a known name at most once.
209fn element(e: &[u8]) -> Result<Element<'_>> {
210    let mut out = Element {
211        for_: None,
212        by: None,
213        host: None,
214        proto: None,
215    };
216    let mut seen = [false; 4];
217    let mut i = 0usize;
218    loop {
219        i += skip_ows(e.get(i..).unwrap_or(&[]));
220        match e.get(i) {
221            None => return Ok(out),
222            Some(b';') => {
223                i += 1;
224                continue;
225            }
226            Some(_) => {}
227        }
228        let name_len = e
229            .get(i..)
230            .unwrap_or(&[])
231            .iter()
232            .take_while(|&&b| class::is_tchar(b))
233            .count();
234        if name_len == 0 {
235            return Err(refused(e, i, ErrorKind::NotToken));
236        }
237        let name_at = i;
238        let name = e.get(i..i + name_len).unwrap_or(&[]);
239        i += name_len;
240        if e.get(i) != Some(&b'=') {
241            return Err(refused(e, i, ErrorKind::MissingValue));
242        }
243        i += 1;
244        let (w, used) = parse_word(e.get(i..).unwrap_or(&[])).map_err(|x| match x.kind() {
245            ErrorKind::Empty => refused(e, i, ErrorKind::InvalidByte),
246            _ => x.shifted(i),
247        })?;
248        // Node, host, and proto values never need quoted pairs; refusing them
249        // keeps every view borrowed from the input.
250        let Some(v) = w.as_plain() else {
251            return err(ErrorKind::InvalidQuotedPair, i);
252        };
253        let value_at = i + usize::from(matches!(w, Word::Quoted(_)));
254        i += used;
255        let slot = [&b"for"[..], b"by", b"host", b"proto"]
256            .iter()
257            .position(|k| name.eq_ignore_ascii_case(k));
258        if let Some(s) = slot {
259            if seen.get(s).copied().unwrap_or(false) {
260                return err(ErrorKind::DuplicateParameter, name_at);
261            }
262            if let Some(flag) = seen.get_mut(s) {
263                *flag = true;
264            }
265        }
266        match slot {
267            Some(0) => out.for_ = Some(node(v).map_err(|x| x.shifted(value_at))?),
268            Some(1) => out.by = Some(node(v).map_err(|x| x.shifted(value_at))?),
269            Some(2) => out.host = Some(host_port(v).map_err(|x| x.shifted(value_at))?),
270            Some(3) => {
271                out.proto = Some(if v.eq_ignore_ascii_case(b"http") {
272                    Proto::Http
273                } else if v.eq_ignore_ascii_case(b"https") {
274                    Proto::Https
275                } else {
276                    return err(ErrorKind::Malformed, value_at);
277                });
278            }
279            _ => {}
280        }
281        i += skip_ows(e.get(i..).unwrap_or(&[]));
282        match e.get(i) {
283            None => return Ok(out),
284            Some(b';') => i += 1,
285            Some(_) => return Err(refused(e, i, ErrorKind::ExpectedSeparator)),
286        }
287    }
288}
289
290fn refused(e: &[u8], at: usize, kind: ErrorKind) -> ParseError {
291    match e.get(at) {
292        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
293        _ => ParseError::new(kind, at),
294    }
295}
296
297/// Validated `Forwarded` field lines.
298#[derive(Clone, Copy, Debug)]
299pub struct ForwardedElements<'a> {
300    lines: &'a [&'a [u8]],
301    count: usize,
302}
303
304impl<'a> ForwardedElements<'a> {
305    /// The elements, left to right across every line.
306    pub fn iter(&self) -> impl Iterator<Item = Element<'a>> + use<'a> {
307        let lines = self.lines;
308        lines
309            .iter()
310            .filter_map(|line| list::parse(line, &Limits::DEFAULT).ok())
311            .flat_map(|l| l.into_iter())
312            .filter_map(|e| element(e).ok())
313    }
314
315    /// The number of elements.
316    #[must_use]
317    pub const fn len(&self) -> usize {
318        self.count
319    }
320
321    /// Whether there are no elements.
322    #[must_use]
323    pub const fn is_empty(&self) -> bool {
324        self.count == 0
325    }
326}
327
328/// Parses every `Forwarded` field line (RFC 7239 section 4).
329///
330/// IPv6 must be quoted and bracketed (`for="[2001:db8::1]:4711"`); an
331/// unquoted IPv6 is refused because `:` and `[` are not token characters.
332/// Values containing quoted pairs are refused. `proto` must be `http` or
333/// `https`.
334///
335/// # Errors
336///
337/// [`ErrorKind::DuplicateParameter`] (a known name twice in one element);
338/// [`ErrorKind::InvalidAddress`], [`ErrorKind::InvalidPort`],
339/// [`ErrorKind::InvalidHost`]; [`ErrorKind::Malformed`] for another `proto`;
340/// [`ErrorKind::TooManyHops`] beyond [`Limits::max_hops`]; the list and
341/// token errors, with [`ParseError::line`] set.
342///
343/// # Examples
344///
345/// ```
346/// use parse_net::{forwarded, Limits};
347///
348/// let lines: [&[u8]; 1] = [b"for=192.0.2.60;proto=http;by=203.0.113.43, for=\"[2001:db8:cafe::17]:4711\""];
349/// let elems: Vec<_> = forwarded::parse(&lines, &Limits::DEFAULT)?.iter().collect();
350/// assert_eq!(elems.len(), 2);
351/// assert!(forwarded::parse(&[&b"for=2001:db8::1"[..]], &Limits::DEFAULT).is_err());
352/// # Ok::<(), parse_net::ParseError>(())
353/// ```
354pub fn parse<'a>(lines: &'a [&'a [u8]], limits: &Limits) -> Result<ForwardedElements<'a>> {
355    let mut count = 0usize;
356    for (n, line) in lines.iter().enumerate() {
357        let l = list::parse(line, limits).map_err(|e| e.on_line(n))?;
358        for e in l.iter() {
359            let at = (e.as_ptr() as usize).saturating_sub(line.as_ptr() as usize);
360            element(e)
361                .map(|_| ())
362                .map_err(|x| x.shifted(at).on_line(n))?;
363            count += 1;
364            if count > usize::from(limits.hops) {
365                return Err(ParseError::new(ErrorKind::TooManyHops, at).on_line(n));
366            }
367        }
368    }
369    Ok(ForwardedElements { lines, count })
370}
371
372/// One `X-Forwarded-For` member: an IPv4 or IPv6 address, IPv6 optionally
373/// bracketed, with an optional port after IPv4 or bracketed IPv6.
374fn xff_member(m: &[u8]) -> Option<(IpAddr, Option<u16>)> {
375    let port = |p: &[u8]| -> Option<Option<u16>> {
376        match node_port(p)? {
377            NodePort::Number(n) => Some(Some(n)),
378            NodePort::Obfuscated(_) => None,
379        }
380    };
381    if let [b'[', ..] = m {
382        let close = m.iter().position(|&b| b == b']')?;
383        let a = ip(m.get(1..close)?, true)?;
384        return match m.get(close + 1..)? {
385            [] => Some((a, None)),
386            [b':', p @ ..] => Some((a, port(p)?)),
387            _ => None,
388        };
389    }
390    if let Some(a) = ip(m, true) {
391        return Some((a, None));
392    }
393    match m.iter().position(|&b| b == b':') {
394        Some(c) => Some((ip(m.get(..c)?, false)?, port(m.get(c + 1..)?)?)),
395        None => Some((ip(m, false)?, None)),
396    }
397}
398
399/// The members of one `X-Forwarded-For` line, refusing empty members.
400fn xff_line(line: &[u8]) -> impl Iterator<Item = &[u8]> {
401    line.split(|&b| b == b',').map(trim_ows)
402}
403
404/// A validated `X-Forwarded-For` chain.
405#[derive(Clone, Copy, Debug)]
406pub struct XffChain<'a> {
407    lines: &'a [&'a [u8]],
408    count: usize,
409}
410
411impl<'a> XffChain<'a> {
412    /// The addresses and ports, left to right across every line.
413    pub fn iter(&self) -> impl Iterator<Item = (IpAddr, Option<u16>)> + use<'a> {
414        self.lines
415            .iter()
416            .flat_map(|line| xff_line(line))
417            .filter_map(xff_member)
418    }
419
420    /// The number of addresses.
421    #[must_use]
422    pub const fn len(&self) -> usize {
423        self.count
424    }
425
426    /// Whether there are no addresses.
427    #[must_use]
428    pub const fn is_empty(&self) -> bool {
429        self.count == 0
430    }
431}
432
433/// Parses every `X-Forwarded-For` field line.
434///
435/// There is no RFC: the de facto grammar accepted is comma-separated IPv4
436/// and IPv6 literals with OWS, brackets optional around IPv6, and an
437/// optional `:port` after IPv4 or bracketed IPv6. Anything else, an empty
438/// member included, is refused (D-016).
439///
440/// # Errors
441///
442/// [`ErrorKind::Malformed`]; [`ErrorKind::TooManyHops`] beyond
443/// [`Limits::max_hops`]; each with [`ParseError::line`] set.
444///
445/// # Examples
446///
447/// ```
448/// use parse_net::{forwarded::parse_x_forwarded_for, Limits};
449///
450/// let lines: [&[u8]; 2] = [b"203.0.113.9:4711, [2001:db8::1]:443", b"2001:db8::2"];
451/// assert_eq!(parse_x_forwarded_for(&lines, &Limits::DEFAULT)?.len(), 3);
452/// assert!(parse_x_forwarded_for(&[&b"1.2.3.4, , 5.6.7.8"[..]], &Limits::DEFAULT).is_err());
453/// # Ok::<(), parse_net::ParseError>(())
454/// ```
455pub fn parse_x_forwarded_for<'a>(lines: &'a [&'a [u8]], limits: &Limits) -> Result<XffChain<'a>> {
456    let mut count = 0usize;
457    for (n, line) in lines.iter().enumerate() {
458        crate::scan::check_field_value(line).map_err(|e| e.on_line(n))?;
459        for m in xff_line(line) {
460            let at = (m.as_ptr() as usize).saturating_sub(line.as_ptr() as usize);
461            if xff_member(m).is_none() {
462                return Err(ParseError::new(ErrorKind::Malformed, at).on_line(n));
463            }
464            count += 1;
465            if count > usize::from(limits.hops) {
466                return Err(ParseError::new(ErrorKind::TooManyHops, at).on_line(n));
467            }
468        }
469    }
470    Ok(XffChain { lines, count })
471}
472
473/// Parses `X-Forwarded-Proto`: `http` or `https`, case-insensitive; a
474/// comma list only when every member is the same.
475///
476/// # Errors
477///
478/// [`ErrorKind::Malformed`].
479///
480/// # Examples
481///
482/// ```
483/// use parse_net::forwarded::{parse_x_forwarded_proto, Proto};
484///
485/// assert_eq!(parse_x_forwarded_proto(b"HTTPS")?, Proto::Https);
486/// assert!(parse_x_forwarded_proto(b"https, http").is_err());
487/// # Ok::<(), parse_net::ParseError>(())
488/// ```
489pub fn parse_x_forwarded_proto(value: &[u8]) -> Result<Proto> {
490    let mut found: Option<Proto> = None;
491    for m in value.split(|&b| b == b',') {
492        let at = (m.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
493        let t = trim_ows(m);
494        let p = if t.eq_ignore_ascii_case(b"http") {
495            Proto::Http
496        } else if t.eq_ignore_ascii_case(b"https") {
497            Proto::Https
498        } else {
499            return err(ErrorKind::Malformed, at);
500        };
501        if found.is_some_and(|f| f != p) {
502            return err(ErrorKind::Malformed, at);
503        }
504        found = Some(p);
505    }
506    found.ok_or(ParseError::new(ErrorKind::Malformed, 0))
507}
508
509/// Parses `X-Forwarded-Host`: one `Host` value; a list is refused.
510///
511/// # Errors
512///
513/// [`ErrorKind::Malformed`] for a list; the `Host` errors.
514///
515/// # Examples
516///
517/// ```
518/// use parse_net::forwarded::parse_x_forwarded_host;
519///
520/// assert_eq!(parse_x_forwarded_host(b"example.com:8443")?.port, Some(8443));
521/// assert!(parse_x_forwarded_host(b"a.example, b.example").is_err());
522/// # Ok::<(), parse_net::ParseError>(())
523/// ```
524pub fn parse_x_forwarded_host(value: &[u8]) -> Result<HostPort<'_>> {
525    if let Some(at) = crate::scan::find_byte(b',', value) {
526        return err(ErrorKind::Malformed, at);
527    }
528    crate::host::parse_host_port(value)
529}
530
531/// What a walk step found.
532enum Step {
533    Addr(IpAddr),
534    Unknown,
535}
536
537/// Walks a chain of `count` steps from the right; `step(i)` gives the
538/// `i`-th step counted from the left.
539fn walk(
540    count: usize,
541    trust: &impl Trust,
542    mut step: impl FnMut(usize) -> Step,
543) -> Option<ClientAddress> {
544    let mut leftmost: Option<IpAddr> = None;
545    for (walked, i) in (0..count).rev().enumerate() {
546        let hops = u8::try_from(walked + 1).unwrap_or(u8::MAX);
547        match step(i) {
548            Step::Addr(a) if trust.is_trusted(a) => leftmost = Some(a),
549            Step::Addr(addr) => return Some(ClientAddress::Forwarded { addr, hops }),
550            Step::Unknown => return Some(ClientAddress::Unknown { hops }),
551        }
552    }
553    leftmost.map(|addr| ClientAddress::Forwarded {
554        addr,
555        hops: u8::try_from(count).unwrap_or(u8::MAX),
556    })
557}
558
559/// The `i`-th element (from the left) of validated lines, by index: the
560/// chain is short (at most `max_hops`), so a walk by index stays cheap and
561/// allocates nothing.
562fn nth_member<'a, I: Iterator<Item = &'a [u8]>>(
563    lines: &[&'a [u8]],
564    i: usize,
565    members: impl Fn(&'a [u8]) -> I,
566) -> Option<&'a [u8]> {
567    lines.iter().flat_map(|line| members(line)).nth(i)
568}
569
570/// Resolves the client address of a request that arrived from `peer`.
571///
572/// The algorithm: an untrusted `peer` gives [`ClientAddress::Peer`] and the
573/// header is never read. Otherwise the whole chain is parsed; a malformed
574/// chain anywhere is an error (fail closed: use the peer); more than
575/// `max_hops` elements is an error. The chain is walked from the right:
576/// trusted addresses are skipped; the first untrusted address is the client;
577/// `unknown`, an obfuscated identifier, or a `Forwarded` element without
578/// `for` stops the walk with [`ClientAddress::Unknown`]. When every element
579/// is trusted, the leftmost is the client. A trusted peer that sent no chain
580/// is itself the client.
581///
582/// # Errors
583///
584/// [`ErrorKind::Malformed`] (the error's offset and line point at the
585/// problem); [`ErrorKind::TooManyHops`].
586///
587/// # Examples
588///
589/// ```
590/// use core::net::IpAddr;
591/// use parse_net::{client_address, ClientAddress, ForwardedSource};
592///
593/// let proxy: IpAddr = "203.0.113.43".parse()?;
594/// let trusted = |a: IpAddr| a == proxy;
595/// let lines: [&[u8]; 1] = [b"for=192.0.2.60;proto=http;by=203.0.113.43"];
596/// assert_eq!(client_address(proxy, ForwardedSource::Forwarded(&lines), &trusted, 16)?,
597///            ClientAddress::Forwarded { addr: "192.0.2.60".parse()?, hops: 1 });
598/// # Ok::<(), Box<dyn std::error::Error>>(())
599/// ```
600pub fn client_address(
601    peer: IpAddr,
602    source: ForwardedSource<'_>,
603    trust: &impl Trust,
604    max_hops: u8,
605) -> Result<ClientAddress> {
606    if !trust.is_trusted(peer) {
607        return Ok(ClientAddress::Peer(peer));
608    }
609    let limits = Limits::DEFAULT.max_hops(max_hops);
610    let found = match source {
611        ForwardedSource::XForwardedFor(lines) => {
612            let chain = parse_x_forwarded_for(lines, &limits).map_err(as_malformed)?;
613            walk(chain.len(), trust, |i| {
614                match nth_member(lines, i, xff_line).and_then(xff_member) {
615                    Some((a, _)) => Step::Addr(a),
616                    None => Step::Unknown,
617                }
618            })
619        }
620        ForwardedSource::Forwarded(lines) => {
621            let elements = parse(lines, &limits).map_err(as_malformed)?;
622            walk(elements.len(), trust, |i| {
623                let e = nth_member(lines, i, |line| {
624                    list::parse(line, &Limits::DEFAULT)
625                        .ok()
626                        .into_iter()
627                        .flat_map(|l| l.into_iter())
628                })
629                .and_then(|e| element(e).ok());
630                match e.and_then(|e| e.for_) {
631                    Some(Node::Addr(a, _)) => Step::Addr(a),
632                    _ => Step::Unknown,
633                }
634            })
635        }
636    };
637    Ok(found.unwrap_or(ClientAddress::Peer(peer)))
638}
639
640/// Every parse error of a chain becomes `Malformed` (or stays
641/// `TooManyHops`), keeping its offset and line.
642fn as_malformed(e: ParseError) -> ParseError {
643    if e.kind() == ErrorKind::TooManyHops {
644        e
645    } else {
646        ParseError::new(ErrorKind::Malformed, e.offset()).on_line(e.line())
647    }
648}
649
650/// [`client_address`] and whether the other header kind was present too.
651#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
652pub struct Resolved {
653    /// The client address.
654    pub address: ClientAddress,
655    /// Whether the header kind not configured had a non-empty line: worth
656    /// logging as a likely spoofing attempt (the two are never merged).
657    pub other_present: bool,
658}
659
660/// As [`client_address`], and reports whether `other_lines` (the field
661/// lines of the header kind not configured) were present.
662///
663/// # Errors
664///
665/// As [`client_address`].
666///
667/// # Examples
668///
669/// ```
670/// use core::net::IpAddr;
671/// use parse_net::{forwarded::client_address_detailed, ForwardedSource};
672///
673/// let peer: IpAddr = "10.0.0.1".parse()?;
674/// let xff: [&[u8]; 1] = [b"198.51.100.7"];
675/// let fwd: [&[u8]; 1] = [b"for=192.0.2.1"];
676/// let r = client_address_detailed(peer, ForwardedSource::XForwardedFor(&xff), &fwd, &|a: IpAddr| a == peer, 16)?;
677/// assert!(r.other_present);
678/// # Ok::<(), Box<dyn std::error::Error>>(())
679/// ```
680pub fn client_address_detailed(
681    peer: IpAddr,
682    source: ForwardedSource<'_>,
683    other_lines: &[&[u8]],
684    trust: &impl Trust,
685    max_hops: u8,
686) -> Result<Resolved> {
687    Ok(Resolved {
688        address: client_address(peer, source, trust, max_hops)?,
689        other_present: other_lines.iter().any(|l| !trim_ows(l).is_empty()),
690    })
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    fn ip4(s: &str) -> IpAddr {
698        s.parse().unwrap()
699    }
700
701    #[test]
702    fn test_forwarded_rfc_examples() {
703        let l = Limits::DEFAULT;
704        let one = |v: &'static [u8]| -> Element<'static> {
705            let lines: &'static [&'static [u8]] = std::boxed::Box::leak(std::boxed::Box::new([v]));
706            parse(lines, &l).unwrap().iter().next().unwrap()
707        };
708        let e = one(b"for=\"_gazonk\"");
709        assert_eq!(e.for_, Some(Node::Obfuscated(b"_gazonk", None)));
710        let e = one(b"For=\"[2001:db8:cafe::17]:4711\"");
711        assert!(matches!(
712            e.for_,
713            Some(Node::Addr(IpAddr::V6(_), Some(NodePort::Number(4711))))
714        ));
715        let e = one(b"for=192.0.2.60;proto=http;by=203.0.113.43");
716        assert_eq!(
717            (e.proto, e.by),
718            (
719                Some(Proto::Http),
720                Some(Node::Addr(ip4("203.0.113.43"), None))
721            )
722        );
723        assert_eq!(one(b"for=unknown").for_, Some(Node::Unknown(None)));
724        assert_eq!(
725            one(b"for=\"10.0.0.1:_p1\"").for_,
726            Some(Node::Addr(
727                ip4("10.0.0.1"),
728                Some(NodePort::Obfuscated(b"_p1"))
729            ))
730        );
731        assert_eq!(
732            one(b"host=example.com; proto=https").proto,
733            Some(Proto::Https)
734        );
735        assert_eq!(
736            one(b";;for=1.2.3.4;").for_,
737            Some(Node::Addr(ip4("1.2.3.4"), None))
738        );
739    }
740
741    #[test]
742    fn test_forwarded_refusals() {
743        let l = Limits::DEFAULT;
744        let k = |v: &[u8]| parse(&[v], &l).unwrap_err().kind();
745        assert_ne!(k(b"for=2001:db8::1"), ErrorKind::TooManyHops);
746        assert_eq!(k(b"for=1.2.3.4;for=5.6.7.8"), ErrorKind::DuplicateParameter);
747        assert_eq!(k(b"for=1.2.3"), ErrorKind::InvalidAddress);
748        assert_eq!(k(b"for=01.2.3.4"), ErrorKind::InvalidAddress);
749        assert_eq!(k(b"for=\"[::1]:99999\""), ErrorKind::InvalidPort);
750        assert_eq!(k(b"proto=ftp"), ErrorKind::Malformed);
751        assert_eq!(k(b"for"), ErrorKind::MissingValue);
752        assert_eq!(k(b"for=\"a\\\"b\""), ErrorKind::InvalidQuotedPair);
753        assert_eq!(k(b"for=1.2.3.4 x"), ErrorKind::ExpectedSeparator);
754        let seventeen = [b"for=1.1.1.1,".repeat(17)].concat();
755        assert_eq!(
756            parse(&[&seventeen[..]], &l).unwrap_err().kind(),
757            ErrorKind::TooManyHops
758        );
759        let sixteen = [b"for=1.1.1.1,".repeat(16)].concat();
760        assert_eq!(parse(&[&sixteen[..]], &l).unwrap().len(), 16);
761    }
762
763    #[test]
764    fn test_xff_shapes() {
765        let l = Limits::DEFAULT;
766        let ok = |v: &[u8]| parse_x_forwarded_for(&[v], &l).map(|c| c.len());
767        assert_eq!(ok(b"1.2.3.4"), Ok(1));
768        assert_eq!(ok(b" 1.2.3.4 , 5.6.7.8:80 "), Ok(2));
769        assert_eq!(ok(b"2001:db8::1, [2001:db8::2]:443, [::1]"), Ok(3));
770        for bad in [
771            &b"1.2.3.4, , 5.6.7.8"[..],
772            b"",
773            b"garbage",
774            b"1.2.3.4:x",
775            b"2001:db8::1:80:",
776            b"[::1",
777            b"1.2.3.4\r",
778        ] {
779            assert!(
780                ok(bad).is_err(),
781                "{}",
782                std::string::String::from_utf8_lossy(bad)
783            );
784        }
785    }
786
787    #[test]
788    fn test_xff_untrusted_peer_ignored() {
789        let t = |a: IpAddr| a == ip4("10.0.0.1");
790        let lines: [&[u8]; 1] = [b"garbage that would be refused"];
791        assert_eq!(
792            client_address(
793                ip4("192.0.2.9"),
794                ForwardedSource::XForwardedFor(&lines),
795                &t,
796                16
797            ),
798            Ok(ClientAddress::Peer(ip4("192.0.2.9")))
799        );
800    }
801
802    #[test]
803    fn test_xff_leftmost_spoof_ignored() {
804        let t = |a: IpAddr| a == ip4("10.0.0.1") || a == ip4("10.0.0.2");
805        let lines: [&[u8]; 1] = [b"1.1.1.1, 203.0.113.7, 10.0.0.2"];
806        assert_eq!(
807            client_address(
808                ip4("10.0.0.1"),
809                ForwardedSource::XForwardedFor(&lines),
810                &t,
811                16
812            ),
813            Ok(ClientAddress::Forwarded {
814                addr: ip4("203.0.113.7"),
815                hops: 2
816            })
817        );
818    }
819
820    #[test]
821    fn test_xff_malformed_fails_closed() {
822        let t = |_: IpAddr| true;
823        let lines: [&[u8]; 2] = [b"1.1.1.1", b"10.0.0.2, nope"];
824        let e = client_address(
825            ip4("10.0.0.1"),
826            ForwardedSource::XForwardedFor(&lines),
827            &t,
828            16,
829        )
830        .unwrap_err();
831        assert_eq!((e.kind(), e.line()), (ErrorKind::Malformed, 1));
832        let too_many: [&[u8]; 1] = [b"1.1.1.1, 1.1.1.2, 1.1.1.3"];
833        assert_eq!(
834            client_address(
835                ip4("10.0.0.1"),
836                ForwardedSource::XForwardedFor(&too_many),
837                &t,
838                2
839            )
840            .unwrap_err()
841            .kind(),
842            ErrorKind::TooManyHops
843        );
844    }
845
846    #[test]
847    fn test_client_address_walk_cases() {
848        let t = |a: IpAddr| a.is_loopback() || a == ip4("10.0.0.1");
849        let peer = ip4("10.0.0.1");
850        let x = |v: &'static [u8]| {
851            client_address(
852                peer,
853                ForwardedSource::XForwardedFor(std::boxed::Box::leak(std::boxed::Box::new([v]))),
854                &t,
855                16,
856            )
857        };
858        assert_eq!(
859            x(b"127.0.0.1, 10.0.0.1"),
860            Ok(ClientAddress::Forwarded {
861                addr: ip4("127.0.0.1"),
862                hops: 2
863            })
864        );
865        let none: [&[u8]; 0] = [];
866        assert_eq!(
867            client_address(peer, ForwardedSource::XForwardedFor(&none), &t, 16),
868            Ok(ClientAddress::Peer(peer))
869        );
870        let f = |v: &'static [u8]| {
871            client_address(
872                peer,
873                ForwardedSource::Forwarded(std::boxed::Box::leak(std::boxed::Box::new([v]))),
874                &t,
875                16,
876            )
877        };
878        assert_eq!(
879            f(b"for=192.0.2.1, for=unknown, for=127.0.0.1"),
880            Ok(ClientAddress::Unknown { hops: 2 })
881        );
882        assert_eq!(
883            f(b"for=192.0.2.1, for=_hidden"),
884            Ok(ClientAddress::Unknown { hops: 1 })
885        );
886        assert_eq!(
887            f(b"for=192.0.2.1, proto=https"),
888            Ok(ClientAddress::Unknown { hops: 1 })
889        );
890        assert_eq!(
891            f(b"for=\"[2001:db8::7]\""),
892            Ok(ClientAddress::Forwarded {
893                addr: "2001:db8::7".parse().unwrap(),
894                hops: 1
895            })
896        );
897        assert_eq!(
898            f(b"for=2001:db8::7").unwrap_err().kind(),
899            ErrorKind::Malformed
900        );
901    }
902
903    #[test]
904    fn test_both_forwarded_headers_reported() {
905        let t = |_: IpAddr| true;
906        let xff: [&[u8]; 1] = [b"192.0.2.1"];
907        let fwd: [&[u8]; 1] = [b"for=198.51.100.1"];
908        let r = client_address_detailed(
909            ip4("10.0.0.1"),
910            ForwardedSource::XForwardedFor(&xff),
911            &fwd,
912            &t,
913            16,
914        )
915        .unwrap();
916        assert!(r.other_present);
917        assert_eq!(
918            r.address,
919            ClientAddress::Forwarded {
920                addr: ip4("192.0.2.1"),
921                hops: 1
922            }
923        );
924        let empty: [&[u8]; 1] = [b"  "];
925        assert!(
926            !client_address_detailed(
927                ip4("10.0.0.1"),
928                ForwardedSource::XForwardedFor(&xff),
929                &empty,
930                &t,
931                16
932            )
933            .unwrap()
934            .other_present
935        );
936    }
937
938    #[test]
939    fn test_x_forwarded_proto_and_host() {
940        assert_eq!(parse_x_forwarded_proto(b"http, HTTP"), Ok(Proto::Http));
941        assert!(parse_x_forwarded_proto(b"").is_err());
942        assert!(parse_x_forwarded_proto(b"wss").is_err());
943        assert!(parse_x_forwarded_host(b"[::1]:8080").is_ok());
944        assert_eq!(
945            parse_x_forwarded_host(b"a,b").unwrap_err().kind(),
946            ErrorKind::Malformed
947        );
948    }
949}