1
//! Deciding which address a request really came from.
2
//!
3
//! `X-Forwarded-For` is a claim, not a fact: anyone who can reach the port can
4
//! set it. It is trustworthy only on the hop written by a proxy we control, so
5
//! the header is consulted **only** when the immediate peer is a configured
6
//! trusted proxy, and ignored entirely otherwise. Without that check a per-IP
7
//! throttle keyed on the header is bypassed by changing one header value.
8

            
9
use axum::http::HeaderMap;
10
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
11

            
12
#[cfg(test)]
13
mod tests;
14

            
15
/// An address range trusted to speak for its clients.
16
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
pub struct Cidr {
18
    network: IpAddr,
19
    prefix: u8,
20
}
21

            
22
/// Network bits kept when throttling an IPv6 client.
23
///
24
/// A single customer is routinely handed a whole /64, so keying on the full
25
/// address would give one attacker 2^64 independent budgets — the limit would
26
/// exist but never bind. /64 is the smallest block a client is assumed to
27
/// control entirely.
28
const IPV6_CLIENT_PREFIX: u32 = 64;
29

            
30
/// Collapses an address to the identity a throttle should count.
31
///
32
/// IPv4 is used as-is; IPv6 is truncated to [`IPV6_CLIENT_PREFIX`] so host
33
/// rotation inside one allocation cannot mint fresh budgets.
34
#[must_use]
35
6854
pub fn throttle_key(addr: IpAddr) -> IpAddr {
36
6854
    match addr.to_canonical() {
37
6196
        IpAddr::V4(v4) => IpAddr::V4(v4),
38
658
        IpAddr::V6(v6) => {
39
658
            let masked = u128::from(v6) & (u128::MAX << (128 - IPV6_CLIENT_PREFIX));
40
658
            IpAddr::V6(Ipv6Addr::from(masked))
41
        }
42
    }
43
6854
}
44

            
45
/// Joins every `X-Forwarded-For` value on the request, in order.
46
///
47
/// A conforming proxy sends one comma-separated header, but nothing stops a
48
/// client from sending its own and a proxy from appending a second line. Only
49
/// the rightmost hop across all of them is evidence, so they must be read as
50
/// one list.
51
#[must_use]
52
1277
pub fn forwarded_for(headers: &HeaderMap) -> Option<String> {
53
1277
    let hops: Vec<&str> = headers
54
1277
        .get_all("x-forwarded-for")
55
1277
        .iter()
56
1277
        .filter_map(|value| value.to_str().ok())
57
1277
        .collect();
58

            
59
1277
    (!hops.is_empty()).then(|| hops.join(","))
60
1277
}
61

            
62
impl Cidr {
63
    /// Parses `10.104.0.0/16`, or a bare address as a single-host range.
64
    #[must_use]
65
96
    pub fn parse(spec: &str) -> Option<Self> {
66
96
        let (addr, prefix) = match spec.split_once('/') {
67
82
            Some((addr, prefix)) => (addr, prefix.parse().ok()?),
68
14
            None => (spec, 0),
69
        };
70

            
71
94
        let network: IpAddr = addr.trim().parse().ok()?;
72
80
        let full = if network.is_ipv4() { 32 } else { 128 };
73
80
        let prefix = if spec.contains('/') { prefix } else { full };
74

            
75
80
        (prefix <= full).then_some(Self { network, prefix })
76
96
    }
77

            
78
    #[must_use]
79
6968
    pub fn contains(&self, addr: IpAddr) -> bool {
80
6968
        match (self.network, addr) {
81
6316
            (IpAddr::V4(net), IpAddr::V4(other)) => {
82
6316
                masked(&net.octets(), &other.octets(), self.prefix)
83
            }
84
            (IpAddr::V6(net), IpAddr::V6(other)) => {
85
                masked(&net.octets(), &other.octets(), self.prefix)
86
            }
87
            // A v4-mapped peer must not match a v6 range, or vice versa.
88
652
            _ => false,
89
        }
90
6968
    }
91
}
92

            
93
/// Compares the first `prefix` bits of two addresses.
94
6316
fn masked(network: &[u8], addr: &[u8], prefix: u8) -> bool {
95
6316
    let whole = usize::from(prefix / 8);
96
6316
    let bits = prefix % 8;
97

            
98
6316
    if network[..whole] != addr[..whole] {
99
6222
        return false;
100
94
    }
101

            
102
94
    if bits == 0 {
103
60
        return true;
104
34
    }
105

            
106
34
    let mask = 0xffu8 << (8 - bits);
107
34
    network[whole] & mask == addr[whole] & mask
108
6316
}
109

            
110
/// Why a trusted-proxy configuration was rejected at startup.
111
#[derive(Debug)]
112
pub enum TrustedProxyError {
113
    Unset,
114
    NoUsableEntry(String),
115
    Unparseable(String),
116
}
117

            
118
impl std::fmt::Display for TrustedProxyError {
119
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120
        match self {
121
            Self::Unset => write!(
122
                f,
123
                "TRUSTED_PROXIES is not set. Give the CIDRs of the proxies in \
124
                 front of this server, or `none` if it is directly exposed"
125
            ),
126
            Self::NoUsableEntry(spec) => write!(
127
                f,
128
                "TRUSTED_PROXIES ({spec:?}) contains no usable CIDR; use `none` \
129
                 to trust no proxy"
130
            ),
131
            Self::Unparseable(spec) => write!(
132
                f,
133
                "TRUSTED_PROXIES ({spec:?}) has an entry that is not a CIDR. Every \
134
                 entry must parse: one bad range silently un-trusts an ingress \
135
                 path and puts its users on a shared login budget"
136
            ),
137
        }
138
    }
139
}
140

            
141
impl std::error::Error for TrustedProxyError {}
142

            
143
/// The proxies whose `X-Forwarded-For` we believe.
144
#[derive(Debug, Clone, Default)]
145
pub struct TrustedProxies(Vec<Cidr>);
146

            
147
impl TrustedProxies {
148
    /// Reads a comma-separated list of CIDRs, skipping anything unparseable.
149
    ///
150
    /// Trusts nothing when the list is empty, so the peer address is always
151
    /// used. Prefer [`Self::from_env`], which refuses both to guess and to
152
    /// tolerate a typo.
153
    #[must_use]
154
64
    pub fn from_spec(spec: &str) -> Self {
155
64
        Self(Self::entries(spec).into_iter().flatten().collect())
156
64
    }
157

            
158
    /// Each entry, `None` where it did not parse.
159
74
    fn entries(spec: &str) -> Vec<Option<Cidr>> {
160
74
        spec.split(',')
161
74
            .map(str::trim)
162
88
            .filter(|s| !s.is_empty())
163
74
            .map(Cidr::parse)
164
74
            .collect()
165
74
    }
166

            
167
    /// Reads `TRUSTED_PROXIES`, which must be set explicitly.
168
    ///
169
    /// Neither default is safe to assume, which is why there is none. Trusting
170
    /// nothing while actually behind a proxy keys *every* user on the ingress
171
    /// address, so one attacker exhausts the shared budget and no one can log
172
    /// in. Trusting something while directly exposed lets anyone forge their
173
    /// identity. Set a CIDR list, or the literal `none` for a directly-exposed
174
    /// deployment.
175
    ///
176
    /// EVERY entry must parse. Dropping a bad one with a warning would leave a
177
    /// typo in one range silently un-trusting one ingress path, collapsing that
178
    /// path's users onto a single shared login budget — the failure this
179
    /// variable exists to prevent, reintroduced quietly.
180
    ///
181
    /// # Errors
182
    /// If the variable is unset, has no entries, or any entry does not parse.
183
14
    pub fn from_env() -> Result<Self, TrustedProxyError> {
184
14
        let spec = std::env::var("TRUSTED_PROXIES").map_err(|_| TrustedProxyError::Unset)?;
185

            
186
12
        if spec.trim() == "none" {
187
2
            return Ok(Self(Vec::new()));
188
10
        }
189

            
190
10
        let entries = Self::entries(&spec);
191
10
        if entries.is_empty() {
192
2
            return Err(TrustedProxyError::NoUsableEntry(spec));
193
8
        }
194
8
        if entries.iter().any(Option::is_none) {
195
4
            return Err(TrustedProxyError::Unparseable(spec));
196
4
        }
197

            
198
4
        Ok(Self::from_spec(&spec))
199
14
    }
200

            
201
    #[must_use]
202
6942
    pub fn trusts(&self, addr: IpAddr) -> bool {
203
6942
        let addr = addr.to_canonical();
204
6948
        self.0.iter().any(|cidr| cidr.contains(addr))
205
6942
    }
206

            
207
    /// Resolves the client address for `peer`, given the request's
208
    /// `X-Forwarded-For` value.
209
    ///
210
    /// Walks the hops right-to-left, skipping ones that are themselves trusted,
211
    /// and returns the first address that is not. That entry is the one our own
212
    /// proxy observed; everything to its left was supplied by the client and is
213
    /// worthless. Falls back to the peer whenever the peer is not trusted, the
214
    /// header is absent, or every entry is unusable.
215
    ///
216
    /// `forwarded_for` must carry *every* `X-Forwarded-For` value the request
217
    /// held, in order — see [`forwarded_for`]. Reading only the first would let
218
    /// a client that sends its own header push the proxy's observed hop into a
219
    /// second line this never looks at.
220
    ///
221
    /// Addresses are canonicalized, so `::ffff:203.0.113.7` and `203.0.113.7`
222
    /// are one client rather than two budgets.
223
    #[must_use]
224
6860
    pub fn client_ip(&self, peer: SocketAddr, forwarded_for: Option<&str>) -> IpAddr {
225
6860
        let peer = peer.ip().to_canonical();
226

            
227
6860
        if !self.trusts(peer) {
228
6794
            return peer;
229
66
        }
230

            
231
66
        forwarded_for
232
66
            .unwrap_or_default()
233
66
            .rsplit(',')
234
70
            .filter_map(|hop| hop.trim().parse::<IpAddr>().ok())
235
66
            .map(|hop| hop.to_canonical())
236
66
            .find(|hop| !self.trusts(*hop))
237
66
            .unwrap_or(peer)
238
6860
    }
239
}