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 `X-Forwarded-For` is read from the right, each trusted hop skipped,
5//! and the first untrusted hop is the client. Hops further left were written
6//! by the client itself and are never read.
7
8use std::net::{IpAddr, Ipv4Addr};
9use std::str::FromStr;
10use std::sync::Once;
11
12use axum::http::HeaderMap;
13use ipnet::IpNet;
14use thiserror::Error;
15use tracing::warn;
16
17/// Reverse proxies whose `X-Forwarded-For` and `X-Real-IP` headers are
18/// believed.
19///
20/// Empty by default: the client IP is then always the TCP peer. Behind a
21/// reverse proxy, list its address (or range), otherwise every client shares
22/// the proxy's bucket.
23///
24/// Parsed from a comma-separated list of IP addresses and CIDR ranges.
25///
26/// # Examples
27///
28/// ```
29/// use ironflow_api::rate_limit::TrustedProxies;
30///
31/// let proxies: TrustedProxies = "10.0.0.0/8, 192.168.1.10".parse()?;
32/// assert!(proxies.contains("10.1.2.3".parse()?));
33/// assert!(proxies.contains("192.168.1.10".parse()?));
34/// assert!(!proxies.contains("203.0.113.7".parse()?));
35/// assert!(TrustedProxies::default().is_empty());
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub struct TrustedProxies(Vec<IpNet>);
40
41impl TrustedProxies {
42    /// Whether `ip` belongs to one of the trusted proxies.
43    ///
44    /// An IPv4-mapped IPv6 address (`::ffff:10.0.0.1`) matches its IPv4 form.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use ironflow_api::rate_limit::TrustedProxies;
50    ///
51    /// let proxies: TrustedProxies = "10.0.0.1".parse()?;
52    /// assert!(proxies.contains("::ffff:10.0.0.1".parse()?));
53    /// # Ok::<(), Box<dyn std::error::Error>>(())
54    /// ```
55    pub fn contains(&self, ip: IpAddr) -> bool {
56        let ip = ip.to_canonical();
57        self.0.iter().any(|net| net.contains(&ip))
58    }
59
60    /// Whether no proxy is trusted.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use ironflow_api::rate_limit::TrustedProxies;
66    ///
67    /// assert!(TrustedProxies::default().is_empty());
68    /// ```
69    pub fn is_empty(&self) -> bool {
70        self.0.is_empty()
71    }
72}
73
74/// A [`TrustedProxies`] entry that is neither an IP address nor a CIDR range.
75///
76/// # Examples
77///
78/// ```
79/// use ironflow_api::rate_limit::TrustedProxies;
80///
81/// let err = "10.0.0.0/8, proxy.local".parse::<TrustedProxies>().unwrap_err();
82/// assert_eq!(err.0, "proxy.local");
83/// ```
84#[derive(Debug, Clone, PartialEq, Eq, Error)]
85#[error("invalid trusted proxy {0:?}: expected an IP address or a CIDR range")]
86pub struct InvalidTrustedProxy(pub String);
87
88impl FromStr for TrustedProxies {
89    type Err = InvalidTrustedProxy;
90
91    /// Parse a comma-separated list. Blank entries are skipped, so an empty
92    /// string trusts no proxy.
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        s.split(',')
95            .map(str::trim)
96            .filter(|entry| !entry.is_empty())
97            .map(|entry| {
98                entry
99                    .parse::<IpNet>()
100                    .or_else(|_| entry.parse::<IpAddr>().map(IpNet::from))
101                    .map_err(|_| InvalidTrustedProxy(entry.to_string()))
102            })
103            .collect::<Result<Vec<_>, _>>()
104            .map(TrustedProxies)
105    }
106}
107
108static MISSING_PEER_WARNING: Once = Once::new();
109
110/// The IP address a request is rate limited under.
111///
112/// `peer` is the TCP peer, `None` when the server was not built with
113/// `into_make_service_with_connect_info`. Every such request then shares one
114/// bucket: limiting everyone together beats letting a header pick the
115/// bucket.
116pub(super) fn client_ip(
117    peer: Option<IpAddr>,
118    headers: &HeaderMap,
119    trusted: &TrustedProxies,
120) -> IpAddr {
121    let Some(peer) = peer.map(|ip| ip.to_canonical()) else {
122        MISSING_PEER_WARNING.call_once(|| {
123            warn!(
124                "rate limiter cannot see the client address: serve the router with \
125                 into_make_service_with_connect_info::<SocketAddr>(); all clients \
126                 share one bucket until then"
127            );
128        });
129        return IpAddr::V4(Ipv4Addr::UNSPECIFIED);
130    };
131    if !trusted.contains(peer) {
132        return peer;
133    }
134
135    let hops: Vec<&str> = headers
136        .get_all("x-forwarded-for")
137        .iter()
138        .filter_map(|value| value.to_str().ok())
139        .flat_map(|value| value.split(','))
140        .map(str::trim)
141        .collect();
142
143    if hops.is_empty() {
144        return headers
145            .get("x-real-ip")
146            .and_then(|value| value.to_str().ok())
147            .and_then(|value| value.trim().parse::<IpAddr>().ok())
148            .map_or(peer, |ip| ip.to_canonical());
149    }
150
151    let mut client = peer;
152    for hop in hops.iter().rev() {
153        // A trusted proxy always writes a valid address: an unparsable hop
154        // comes from beyond the trusted chain, so the last trusted hop wins.
155        let Ok(ip) = hop.parse::<IpAddr>() else {
156            return client;
157        };
158        let ip = ip.to_canonical();
159        if !trusted.contains(ip) {
160            return ip;
161        }
162        client = ip;
163    }
164    client
165}
166
167#[cfg(test)]
168mod tests {
169    use axum::http::HeaderValue;
170
171    use super::*;
172
173    fn ip(s: &str) -> IpAddr {
174        s.parse().unwrap()
175    }
176
177    fn headers(pairs: &[(&'static str, &str)]) -> HeaderMap {
178        let mut map = HeaderMap::new();
179        for (name, value) in pairs {
180            map.append(*name, HeaderValue::from_str(value).unwrap());
181        }
182        map
183    }
184
185    fn proxies(s: &str) -> TrustedProxies {
186        s.parse().unwrap()
187    }
188
189    #[test]
190    fn untrusted_peer_ignores_forwarding_headers() {
191        let h = headers(&[("x-forwarded-for", "9.9.9.9"), ("x-real-ip", "8.8.8.8")]);
192        assert_eq!(
193            client_ip(Some(ip("203.0.113.7")), &h, &TrustedProxies::default()),
194            ip("203.0.113.7")
195        );
196        assert_eq!(
197            client_ip(Some(ip("203.0.113.7")), &h, &proxies("10.0.0.0/8")),
198            ip("203.0.113.7")
199        );
200    }
201
202    #[test]
203    fn trusted_peer_uses_the_last_forwarded_hop() {
204        let h = headers(&[("x-forwarded-for", "6.6.6.6, 198.51.100.4")]);
205        assert_eq!(
206            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
207            ip("198.51.100.4")
208        );
209    }
210
211    #[test]
212    fn trusted_hops_are_skipped_from_the_right() {
213        let h = headers(&[("x-forwarded-for", "6.6.6.6, 198.51.100.4, 10.0.0.9")]);
214        assert_eq!(
215            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
216            ip("198.51.100.4")
217        );
218    }
219
220    #[test]
221    fn all_hops_trusted_gives_the_leftmost() {
222        let h = headers(&[("x-forwarded-for", "10.0.0.8, 10.0.0.9")]);
223        assert_eq!(
224            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
225            ip("10.0.0.8")
226        );
227    }
228
229    #[test]
230    fn unparsable_hop_stops_at_the_last_trusted_hop() {
231        let h = headers(&[("x-forwarded-for", "198.51.100.4, garbage, 10.0.0.9")]);
232        assert_eq!(
233            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
234            ip("10.0.0.9")
235        );
236    }
237
238    #[test]
239    fn several_forwarded_for_headers_are_read_in_order() {
240        let h = headers(&[
241            ("x-forwarded-for", "6.6.6.6"),
242            ("x-forwarded-for", "198.51.100.4"),
243        ]);
244        assert_eq!(
245            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
246            ip("198.51.100.4")
247        );
248    }
249
250    #[test]
251    fn trusted_peer_without_forwarded_for_uses_x_real_ip() {
252        let h = headers(&[("x-real-ip", "198.51.100.4")]);
253        assert_eq!(
254            client_ip(Some(ip("10.0.0.2")), &h, &proxies("10.0.0.0/8")),
255            ip("198.51.100.4")
256        );
257    }
258
259    #[test]
260    fn trusted_peer_without_headers_is_the_client() {
261        assert_eq!(
262            client_ip(
263                Some(ip("10.0.0.2")),
264                &HeaderMap::new(),
265                &proxies("10.0.0.0/8")
266            ),
267            ip("10.0.0.2")
268        );
269    }
270
271    #[test]
272    fn ipv4_mapped_peer_is_canonicalized() {
273        let h = headers(&[("x-forwarded-for", "198.51.100.4")]);
274        assert_eq!(
275            client_ip(Some(ip("::ffff:10.0.0.2")), &h, &proxies("10.0.0.2")),
276            ip("198.51.100.4")
277        );
278        assert_eq!(
279            client_ip(
280                Some(ip("::ffff:203.0.113.7")),
281                &h,
282                &TrustedProxies::default()
283            ),
284            ip("203.0.113.7")
285        );
286    }
287
288    #[test]
289    fn missing_peer_falls_back_to_one_shared_address() {
290        let h = headers(&[("x-forwarded-for", "9.9.9.9")]);
291        assert_eq!(
292            client_ip(None, &h, &proxies("0.0.0.0/0")),
293            IpAddr::V4(Ipv4Addr::UNSPECIFIED)
294        );
295    }
296
297    #[test]
298    fn parses_addresses_and_ranges() {
299        let parsed = proxies(" 10.0.0.0/8 ,192.168.1.10, fd00::/8 ,");
300        assert!(parsed.contains(ip("10.255.0.1")));
301        assert!(parsed.contains(ip("192.168.1.10")));
302        assert!(!parsed.contains(ip("192.168.1.11")));
303        assert!(parsed.contains(ip("fd12::1")));
304    }
305
306    #[test]
307    fn empty_string_trusts_no_proxy() {
308        assert!(proxies("").is_empty());
309        assert!(proxies(" , ").is_empty());
310    }
311
312    #[test]
313    fn invalid_entry_is_reported() {
314        assert_eq!(
315            "10.0.0.0/8,10.0.0.0/33".parse::<TrustedProxies>(),
316            Err(InvalidTrustedProxy("10.0.0.0/33".to_string()))
317        );
318        assert_eq!(
319            "localhost".parse::<TrustedProxies>(),
320            Err(InvalidTrustedProxy("localhost".to_string()))
321        );
322    }
323}