Skip to main content

seamless_auth/
client_ip.rs

1use std::fmt;
2use std::net::IpAddr;
3
4use axum::http::HeaderMap;
5use axum::http::header::USER_AGENT;
6
7const MAX_USER_AGENT_LENGTH: usize = 512;
8
9/// A subnet, or single address, of a proxy in front of the application.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11struct Prefix {
12    addr: IpAddr,
13    bits: u8,
14}
15
16impl Prefix {
17    fn contains(&self, ip: IpAddr) -> bool {
18        match (self.addr, ip) {
19            (IpAddr::V4(net), IpAddr::V4(ip)) => {
20                masked(u32::from(net).into(), self.bits, 32)
21                    == masked(u32::from(ip).into(), self.bits, 32)
22            }
23            (IpAddr::V6(net), IpAddr::V6(ip)) => {
24                masked(u128::from(net), self.bits, 128) == masked(u128::from(ip), self.bits, 128)
25            }
26            _ => false,
27        }
28    }
29}
30
31fn masked(value: u128, bits: u8, width: u8) -> u128 {
32    if bits == 0 {
33        return 0;
34    }
35    value >> (width - bits)
36}
37
38/// A proxy address [`TrustedProxies::new`] could not read.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct InvalidProxy(pub String);
41
42impl fmt::Display for InvalidProxy {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(
45            f,
46            "seamless-auth: {:?} is not an address or CIDR subnet",
47            self.0
48        )
49    }
50}
51
52impl std::error::Error for InvalidProxy {}
53
54/// Resolves the end user's address for an app behind proxies with known
55/// addresses or subnets (`"10.0.0.0/8"`, `"127.0.0.1"`).
56///
57/// It walks `X-Forwarded-For` from the right, skipping trusted proxies, and
58/// returns the first address that is not one. There is deliberately no hop
59/// count: it cannot tell a proxy from a client that sent its own header.
60#[derive(Clone, Debug)]
61pub struct TrustedProxies {
62    prefixes: Vec<Prefix>,
63}
64
65impl TrustedProxies {
66    pub fn new<I, S>(proxies: I) -> Result<Self, InvalidProxy>
67    where
68        I: IntoIterator<Item = S>,
69        S: AsRef<str>,
70    {
71        let mut prefixes = Vec::new();
72        for proxy in proxies {
73            let raw = proxy.as_ref().trim();
74            let prefix = match raw.split_once('/') {
75                Some((addr, bits)) => {
76                    let addr: IpAddr = addr.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
77                    let bits: u8 = bits.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
78                    if bits > width(addr) {
79                        return Err(InvalidProxy(raw.to_string()));
80                    }
81                    Prefix { addr, bits }
82                }
83                None => {
84                    let addr: IpAddr = raw.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
85                    Prefix {
86                        addr,
87                        bits: width(addr),
88                    }
89                }
90            };
91            prefixes.push(prefix);
92        }
93        Ok(TrustedProxies { prefixes })
94    }
95
96    fn trusted(&self, ip: IpAddr) -> bool {
97        let ip = ip.to_canonical();
98        self.prefixes.iter().any(|p| p.contains(ip))
99    }
100
101    /// The end user's address, given the request headers and the connecting peer.
102    pub fn resolve(&self, headers: &HeaderMap, peer: Option<IpAddr>) -> Option<IpAddr> {
103        let peer = peer?;
104        if !self.trusted(peer) {
105            return Some(peer);
106        }
107
108        let hops: Vec<&str> = headers
109            .get_all("x-forwarded-for")
110            .iter()
111            .filter_map(|v| v.to_str().ok())
112            .flat_map(|v| v.split(','))
113            .map(str::trim)
114            .filter(|hop| !hop.is_empty())
115            .collect();
116
117        for hop in hops.iter().rev() {
118            match hop.parse::<IpAddr>() {
119                Ok(ip) if self.trusted(ip) => continue,
120                Ok(ip) => return Some(ip),
121                // Not an address, so not a trusted proxy either: whoever wrote it
122                // is the client, and it has no usable address.
123                Err(_) => return None,
124            }
125        }
126        match hops.first() {
127            Some(first) => first.parse().ok(),
128            None => Some(peer),
129        }
130    }
131}
132
133fn width(addr: IpAddr) -> u8 {
134    match addr {
135        IpAddr::V4(_) => 32,
136        IpAddr::V6(_) => 128,
137    }
138}
139
140/// The address forwarded to the auth API, in canonical form.
141pub(crate) fn canonical(ip: IpAddr) -> String {
142    ip.to_canonical().to_string()
143}
144
145/// The user agent forwarded to the auth API, trimmed and capped.
146pub(crate) fn client_user_agent(headers: &HeaderMap) -> Option<Vec<u8>> {
147    let raw = headers.get(USER_AGENT)?.as_bytes();
148    let trimmed = raw.trim_ascii();
149    if trimmed.is_empty() {
150        return None;
151    }
152    Some(trimmed[..trimmed.len().min(MAX_USER_AGENT_LENGTH)].to_vec())
153}