Skip to main content

ironflow_api/rate_limit/
client_ip.rs

1//! Client IP resolution for rate limiting.
2//!
3//! The client is the TCP peer, unless that peer is a trusted reverse proxy:
4//! then only the configured forwarding header is read. `X-Forwarded-For` is
5//! read from the right, each trusted hop skipped, and the first untrusted hop
6//! is the client; hops further left were written by the client itself and
7//! are never read. `X-Real-IP` is read as is.
8
9use std::net::IpAddr;
10use std::str::FromStr;
11use std::sync::Once;
12
13use axum::http::HeaderMap;
14use ipnet::IpNet;
15use thiserror::Error;
16use tracing::warn;
17
18/// The forwarding header a trusted proxy reports the client IP in.
19///
20/// Only this header is read: falling back to another one would let a client
21/// write the header the proxy does not set.
22///
23/// # Examples
24///
25/// ```
26/// use ironflow_api::rate_limit::ClientIpHeader;
27///
28/// assert_eq!(ClientIpHeader::default(), ClientIpHeader::XForwardedFor);
29/// assert_eq!("X-Real-IP".parse::<ClientIpHeader>()?, ClientIpHeader::XRealIp);
30/// # Ok::<(), Box<dyn std::error::Error>>(())
31/// ```
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub enum ClientIpHeader {
34    /// `X-Forwarded-For`, read from the right, trusted hops skipped.
35    #[default]
36    XForwardedFor,
37    /// `X-Real-IP`, a single address set by the proxy.
38    XRealIp,
39}
40
41/// A [`ClientIpHeader`] name that is neither `x-forwarded-for` nor
42/// `x-real-ip`.
43///
44/// # Examples
45///
46/// ```
47/// use ironflow_api::rate_limit::ClientIpHeader;
48///
49/// let err = "forwarded".parse::<ClientIpHeader>().unwrap_err();
50/// assert_eq!(err.0, "forwarded");
51/// ```
52#[derive(Debug, Clone, PartialEq, Eq, Error)]
53#[error("invalid client IP header {0:?}: expected x-forwarded-for or x-real-ip")]
54pub struct InvalidClientIpHeader(pub String);
55
56impl FromStr for ClientIpHeader {
57    type Err = InvalidClientIpHeader;
58
59    /// Parse a header name, ignoring case and surrounding blanks.
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        match s.trim().to_ascii_lowercase().as_str() {
62            "x-forwarded-for" => Ok(Self::XForwardedFor),
63            "x-real-ip" => Ok(Self::XRealIp),
64            _ => Err(InvalidClientIpHeader(s.to_string())),
65        }
66    }
67}
68
69/// Reverse proxies whose forwarding header is believed, and which header
70/// that is.
71///
72/// Empty by default: the client IP is then always the TCP peer. Behind a
73/// reverse proxy, list its address (or range), otherwise every client shares
74/// the proxy's bucket. Only the configured [`ClientIpHeader`] is read,
75/// `X-Forwarded-For` unless [`TrustedProxies::with_header`] says otherwise.
76///
77/// Parsed from a comma-separated list of IP addresses and CIDR ranges.
78///
79/// # Examples
80///
81/// ```
82/// use ironflow_api::rate_limit::TrustedProxies;
83///
84/// let proxies: TrustedProxies = "10.0.0.0/8, 192.168.1.10".parse()?;
85/// assert!(proxies.contains("10.1.2.3".parse()?));
86/// assert!(proxies.contains("192.168.1.10".parse()?));
87/// assert!(!proxies.contains("203.0.113.7".parse()?));
88/// assert!(TrustedProxies::default().is_empty());
89/// # Ok::<(), Box<dyn std::error::Error>>(())
90/// ```
91#[derive(Debug, Clone, Default, PartialEq, Eq)]
92pub struct TrustedProxies {
93    nets: Vec<IpNet>,
94    header: ClientIpHeader,
95}
96
97impl TrustedProxies {
98    /// Whether `ip` belongs to one of the trusted proxies.
99    ///
100    /// An IPv4-mapped IPv6 address (`::ffff:10.0.0.1`) matches its IPv4 form.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use ironflow_api::rate_limit::TrustedProxies;
106    ///
107    /// let proxies: TrustedProxies = "10.0.0.1".parse()?;
108    /// assert!(proxies.contains("::ffff:10.0.0.1".parse()?));
109    /// # Ok::<(), Box<dyn std::error::Error>>(())
110    /// ```
111    pub fn contains(&self, ip: IpAddr) -> bool {
112        let ip = ip.to_canonical();
113        self.nets.iter().any(|net| net.contains(&ip))
114    }
115
116    /// Whether no proxy is trusted.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use ironflow_api::rate_limit::TrustedProxies;
122    ///
123    /// assert!(TrustedProxies::default().is_empty());
124    /// ```
125    pub fn is_empty(&self) -> bool {
126        self.nets.is_empty()
127    }
128
129    /// Read the client IP from `header` instead of the default
130    /// `X-Forwarded-For`.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use ironflow_api::rate_limit::{ClientIpHeader, TrustedProxies};
136    ///
137    /// let proxies = "10.0.0.1"
138    ///     .parse::<TrustedProxies>()?
139    ///     .with_header(ClientIpHeader::XRealIp);
140    /// assert_eq!(proxies.header(), ClientIpHeader::XRealIp);
141    /// # Ok::<(), Box<dyn std::error::Error>>(())
142    /// ```
143    pub fn with_header(mut self, header: ClientIpHeader) -> Self {
144        self.header = header;
145        self
146    }
147
148    /// The forwarding header read from a trusted proxy.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use ironflow_api::rate_limit::{ClientIpHeader, TrustedProxies};
154    ///
155    /// assert_eq!(TrustedProxies::default().header(), ClientIpHeader::XForwardedFor);
156    /// ```
157    pub fn header(&self) -> ClientIpHeader {
158        self.header
159    }
160}
161
162/// A [`TrustedProxies`] entry that is neither an IP address nor a CIDR range.
163///
164/// # Examples
165///
166/// ```
167/// use ironflow_api::rate_limit::TrustedProxies;
168///
169/// let err = "10.0.0.0/8, proxy.local".parse::<TrustedProxies>().unwrap_err();
170/// assert_eq!(err.0, "proxy.local");
171/// ```
172#[derive(Debug, Clone, PartialEq, Eq, Error)]
173#[error("invalid trusted proxy {0:?}: expected an IP address or a CIDR range")]
174pub struct InvalidTrustedProxy(pub String);
175
176impl FromStr for TrustedProxies {
177    type Err = InvalidTrustedProxy;
178
179    /// Parse a comma-separated list. Blank entries are skipped, so an empty
180    /// string trusts no proxy.
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        s.split(',')
183            .map(str::trim)
184            .filter(|entry| !entry.is_empty())
185            .map(|entry| {
186                entry
187                    .parse::<IpNet>()
188                    .or_else(|_| entry.parse::<IpAddr>().map(IpNet::from))
189                    .map_err(|_| InvalidTrustedProxy(entry.to_string()))
190            })
191            .collect::<Result<Vec<_>, _>>()
192            .map(|nets| TrustedProxies {
193                nets,
194                header: ClientIpHeader::default(),
195            })
196    }
197}
198
199static MISSING_PEER_WARNING: Once = Once::new();
200
201/// The IP address a request is rate limited under.
202///
203/// `peer` is the TCP peer, `None` when the server was not built with
204/// `into_make_service_with_connect_info`. There is then no client IP and the
205/// IP limit is skipped: one bucket shared by every client would let any of
206/// them lock out all others, and a header cannot be believed without a
207/// trusted peer.
208pub(super) fn client_ip(
209    peer: Option<IpAddr>,
210    headers: &HeaderMap,
211    trusted: &TrustedProxies,
212) -> Option<IpAddr> {
213    let Some(peer) = peer.map(|ip| ip.to_canonical()) else {
214        MISSING_PEER_WARNING.call_once(|| {
215            warn!(
216                "rate limiter cannot see the client address: the IP limit is skipped \
217                 until the router is served with \
218                 into_make_service_with_connect_info::<SocketAddr>()"
219            );
220        });
221        return None;
222    };
223    if !trusted.contains(peer) {
224        return Some(peer);
225    }
226
227    let client = match trusted.header() {
228        ClientIpHeader::XForwardedFor => forwarded_for_client(peer, headers, trusted),
229        ClientIpHeader::XRealIp => headers
230            .get("x-real-ip")
231            .and_then(|value| value.to_str().ok())
232            .and_then(|value| value.trim().parse::<IpAddr>().ok())
233            .map_or(peer, |ip| ip.to_canonical()),
234    };
235    Some(client)
236}
237
238/// The client `X-Forwarded-For` names behind the trusted `peer`, or the peer
239/// itself when the header is absent.
240fn forwarded_for_client(peer: IpAddr, headers: &HeaderMap, trusted: &TrustedProxies) -> IpAddr {
241    let hops: Vec<&str> = headers
242        .get_all("x-forwarded-for")
243        .iter()
244        .filter_map(|value| value.to_str().ok())
245        .flat_map(|value| value.split(','))
246        .map(str::trim)
247        .collect();
248
249    let mut client = peer;
250    for hop in hops.iter().rev() {
251        // A trusted proxy always writes a valid address: an unparsable hop
252        // comes from beyond the trusted chain, so the last trusted hop wins.
253        let Ok(ip) = hop.parse::<IpAddr>() else {
254            return client;
255        };
256        let ip = ip.to_canonical();
257        if !trusted.contains(ip) {
258            return ip;
259        }
260        client = ip;
261    }
262    client
263}
264
265#[cfg(test)]
266mod tests {
267    use axum::http::HeaderValue;
268
269    use super::*;
270
271    fn ip(s: &str) -> IpAddr {
272        s.parse().unwrap()
273    }
274
275    fn headers(pairs: &[(&'static str, &str)]) -> HeaderMap {
276        let mut map = HeaderMap::new();
277        for (name, value) in pairs {
278            map.append(*name, HeaderValue::from_str(value).unwrap());
279        }
280        map
281    }
282
283    fn proxies(s: &str) -> TrustedProxies {
284        s.parse().unwrap()
285    }
286
287    fn real_ip_proxies(s: &str) -> TrustedProxies {
288        proxies(s).with_header(ClientIpHeader::XRealIp)
289    }
290
291    #[test]
292    fn untrusted_peer_ignores_forwarding_headers() {
293        let h = headers(&[("x-forwarded-for", "9.9.9.9"), ("x-real-ip", "8.8.8.8")]);
294        assert_eq!(
295            client_ip(Some(ip("203.0.113.7")), &h, &TrustedProxies::default()),
296            Some(ip("203.0.113.7"))
297        );
298        assert_eq!(
299            client_ip(Some(ip("203.0.113.7")), &h, &proxies("10.0.0.0/8")),
300            Some(ip("203.0.113.7"))
301        );
302        assert_eq!(
303            client_ip(Some(ip("203.0.113.7")), &h, &real_ip_proxies("10.0.0.0/8")),
304            Some(ip("203.0.113.7"))
305        );
306    }
307
308    #[test]
309    fn trusted_peer_uses_the_last_forwarded_hop() {
310        let h = headers(&[("x-forwarded-for", "6.6.6.6, 198.51.100.4")]);
311        assert_eq!(
312            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
313            Some(ip("198.51.100.4"))
314        );
315    }
316
317    #[test]
318    fn trusted_hops_are_skipped_from_the_right() {
319        let h = headers(&[("x-forwarded-for", "6.6.6.6, 198.51.100.4, 10.0.0.9")]);
320        assert_eq!(
321            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
322            Some(ip("198.51.100.4"))
323        );
324    }
325
326    #[test]
327    fn all_hops_trusted_gives_the_leftmost() {
328        let h = headers(&[("x-forwarded-for", "10.0.0.8, 10.0.0.9")]);
329        assert_eq!(
330            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
331            Some(ip("10.0.0.8"))
332        );
333    }
334
335    #[test]
336    fn unparsable_hop_stops_at_the_last_trusted_hop() {
337        let h = headers(&[("x-forwarded-for", "198.51.100.4, garbage, 10.0.0.9")]);
338        assert_eq!(
339            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
340            Some(ip("10.0.0.9"))
341        );
342    }
343
344    #[test]
345    fn several_forwarded_for_headers_are_read_in_order() {
346        let h = headers(&[
347            ("x-forwarded-for", "6.6.6.6"),
348            ("x-forwarded-for", "198.51.100.4"),
349        ]);
350        assert_eq!(
351            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
352            Some(ip("198.51.100.4"))
353        );
354    }
355
356    #[test]
357    fn x_forwarded_for_mode_ignores_x_real_ip() {
358        let h = headers(&[("x-real-ip", "198.51.100.4")]);
359        assert_eq!(
360            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
361            Some(ip("10.0.0.2"))
362        );
363    }
364
365    #[test]
366    fn x_real_ip_mode_reads_x_real_ip() {
367        let h = headers(&[("x-real-ip", " 198.51.100.4 ")]);
368        assert_eq!(
369            client_ip(Some(ip("10.0.0.2")), &h, &real_ip_proxies("10.0.0.0/8")),
370            Some(ip("198.51.100.4"))
371        );
372    }
373
374    #[test]
375    fn x_real_ip_mode_ignores_x_forwarded_for() {
376        let h = headers(&[("x-forwarded-for", "198.51.100.4")]);
377        assert_eq!(
378            client_ip(Some(ip("10.0.0.2")), &h, &real_ip_proxies("10.0.0.0/8")),
379            Some(ip("10.0.0.2"))
380        );
381        let h = headers(&[
382            ("x-forwarded-for", "198.51.100.4"),
383            ("x-real-ip", "198.51.100.5"),
384        ]);
385        assert_eq!(
386            client_ip(Some(ip("10.0.0.2")), &h, &real_ip_proxies("10.0.0.0/8")),
387            Some(ip("198.51.100.5"))
388        );
389    }
390
391    #[test]
392    fn x_real_ip_mode_with_an_unparsable_value_gives_the_peer() {
393        let h = headers(&[("x-real-ip", "not-an-ip")]);
394        assert_eq!(
395            client_ip(Some(ip("10.0.0.2")), &h, &real_ip_proxies("10.0.0.0/8")),
396            Some(ip("10.0.0.2"))
397        );
398    }
399
400    #[test]
401    fn trusted_peer_without_headers_is_the_client() {
402        assert_eq!(
403            client_ip(
404                Some(ip("10.0.0.2")),
405                &HeaderMap::new(),
406                &proxies("10.0.0.0/8")
407            ),
408            Some(ip("10.0.0.2"))
409        );
410    }
411
412    #[test]
413    fn ipv4_mapped_peer_is_canonicalized() {
414        let h = headers(&[("x-forwarded-for", "198.51.100.4")]);
415        assert_eq!(
416            client_ip(Some(ip("::ffff:10.0.0.2")), &h, &proxies("10.0.0.2")),
417            Some(ip("198.51.100.4"))
418        );
419        assert_eq!(
420            client_ip(
421                Some(ip("::ffff:203.0.113.7")),
422                &h,
423                &TrustedProxies::default()
424            ),
425            Some(ip("203.0.113.7"))
426        );
427    }
428
429    #[test]
430    fn missing_peer_has_no_client_ip() {
431        let h = headers(&[("x-forwarded-for", "9.9.9.9"), ("x-real-ip", "8.8.8.8")]);
432        assert_eq!(client_ip(None, &h, &proxies("0.0.0.0/0")), None);
433        assert_eq!(client_ip(None, &h, &real_ip_proxies("0.0.0.0/0")), None);
434    }
435
436    #[test]
437    fn parses_addresses_and_ranges() {
438        let parsed = proxies(" 10.0.0.0/8 ,192.168.1.10, fd00::/8 ,");
439        assert!(parsed.contains(ip("10.255.0.1")));
440        assert!(parsed.contains(ip("192.168.1.10")));
441        assert!(!parsed.contains(ip("192.168.1.11")));
442        assert!(parsed.contains(ip("fd12::1")));
443        assert_eq!(parsed.header(), ClientIpHeader::XForwardedFor);
444    }
445
446    #[test]
447    fn empty_string_trusts_no_proxy() {
448        assert!(proxies("").is_empty());
449        assert!(proxies(" , ").is_empty());
450    }
451
452    #[test]
453    fn invalid_entry_is_reported() {
454        assert_eq!(
455            "10.0.0.0/8,10.0.0.0/33".parse::<TrustedProxies>(),
456            Err(InvalidTrustedProxy("10.0.0.0/33".to_string()))
457        );
458        assert_eq!(
459            "localhost".parse::<TrustedProxies>(),
460            Err(InvalidTrustedProxy("localhost".to_string()))
461        );
462    }
463
464    #[test]
465    fn client_ip_header_parses_case_insensitively() {
466        for (raw, expected) in [
467            ("x-forwarded-for", ClientIpHeader::XForwardedFor),
468            (" X-Forwarded-For ", ClientIpHeader::XForwardedFor),
469            ("x-real-ip", ClientIpHeader::XRealIp),
470            ("X-REAL-IP", ClientIpHeader::XRealIp),
471        ] {
472            assert_eq!(raw.parse::<ClientIpHeader>(), Ok(expected), "{raw:?}");
473        }
474    }
475
476    #[test]
477    fn invalid_client_ip_header_is_reported() {
478        for raw in ["", "forwarded", "x-forwarded-for,x-real-ip"] {
479            assert_eq!(
480                raw.parse::<ClientIpHeader>(),
481                Err(InvalidClientIpHeader(raw.to_string()))
482            );
483        }
484    }
485}