ironflow_api/rate_limit/
client_ip.rs1use 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub struct TrustedProxies(Vec<IpNet>);
40
41impl TrustedProxies {
42 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 pub fn is_empty(&self) -> bool {
70 self.0.is_empty()
71 }
72}
73
74#[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 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
110pub(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 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}