1use std::net::{IpAddr, SocketAddr};
2use std::str::FromStr;
3
4use axum::http::HeaderMap;
5use ip_network::IpNetwork;
6
7#[derive(Debug)]
21pub struct IpExtractor {
22 trusted_proxies: Vec<IpNetwork>,
23}
24
25impl IpExtractor {
26 pub fn new(trusted_proxy_strs: &[String]) -> Result<Self, String> {
32 let mut proxies = Vec::with_capacity(trusted_proxy_strs.len());
33
34 for s in trusted_proxy_strs {
35 if let Ok(net) = s.parse::<IpNetwork>() {
37 proxies.push(net);
38 } else if let Ok(ip) = IpAddr::from_str(s) {
39 proxies.push(IpNetwork::from(ip));
40 } else {
41 tracing::warn!(entry = %s, "trusted_proxies entry is not a valid IP or CIDR range -- skipped");
42 }
43 }
44
45 Ok(Self {
46 trusted_proxies: proxies,
47 })
48 }
49
50 #[must_use]
52 pub fn is_empty(&self) -> bool {
53 self.trusted_proxies.is_empty()
54 }
55
56 pub fn extract(&self, headers: &HeaderMap, peer_addr: SocketAddr) -> IpAddr {
66 if self.trusted_proxies.is_empty() {
67 return peer_addr.ip();
68 }
69
70 if !self.is_trusted(peer_addr.ip()) {
71 return peer_addr.ip();
72 }
73
74 self.extract_cf_connecting_ip(headers)
75 .or_else(|| self.extract_x_real_ip(headers))
76 .or_else(|| self.extract_x_forwarded_for(headers))
77 .unwrap_or_else(|| peer_addr.ip())
78 }
79
80 fn is_trusted(&self, ip: IpAddr) -> bool {
81 self.trusted_proxies.iter().any(|net| net.contains(ip))
82 }
83
84 fn extract_cf_connecting_ip(&self, headers: &HeaderMap) -> Option<IpAddr> {
85 headers
86 .get("cf-connecting-ip")
87 .and_then(|v| v.to_str().ok())
88 .and_then(|s| IpAddr::from_str(s.trim()).ok())
89 }
90
91 fn extract_x_real_ip(&self, headers: &HeaderMap) -> Option<IpAddr> {
92 headers
93 .get("x-real-ip")
94 .and_then(|v| v.to_str().ok())
95 .and_then(|s| IpAddr::from_str(s.trim()).ok())
96 }
97
98 fn extract_x_forwarded_for(&self, headers: &HeaderMap) -> Option<IpAddr> {
101 let value = headers.get("x-forwarded-for")?.to_str().ok()?;
102 value
103 .rsplit(',')
104 .filter_map(|s| IpAddr::from_str(s.trim()).ok())
105 .find(|ip| !self.is_trusted(*ip))
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use axum::http::HeaderValue;
113
114 fn peer(addr: &str) -> SocketAddr {
115 addr.parse().unwrap()
116 }
117
118 fn extractor(proxies: &[&str]) -> IpExtractor {
119 IpExtractor::new(&proxies.iter().map(|s| s.to_string()).collect::<Vec<_>>()).unwrap()
120 }
121
122 #[test]
123 fn no_proxies_returns_peer_ip() {
124 let ext = extractor(&[]);
125 let headers = HeaderMap::new();
126 assert_eq!(
127 ext.extract(&headers, peer("1.2.3.4:12345")),
128 "1.2.3.4".parse::<IpAddr>().unwrap()
129 );
130 }
131
132 #[test]
133 fn no_proxies_ignores_all_headers() {
134 let ext = extractor(&[]);
135 let mut headers = HeaderMap::new();
136 headers.insert("cf-connecting-ip", HeaderValue::from_static("5.6.7.8"));
137 headers.insert("x-real-ip", HeaderValue::from_static("9.10.11.12"));
138 headers.insert("x-forwarded-for", HeaderValue::from_static("13.14.15.16"));
139
140 assert_eq!(
141 ext.extract(&headers, peer("1.2.3.4:12345")),
142 "1.2.3.4".parse::<IpAddr>().unwrap()
143 );
144 }
145
146 #[test]
147 fn untrusted_peer_returns_peer_ip() {
148 let ext = extractor(&["10.0.0.1"]);
149 let mut headers = HeaderMap::new();
150 headers.insert("cf-connecting-ip", HeaderValue::from_static("5.6.7.8"));
151
152 assert_eq!(
153 ext.extract(&headers, peer("1.2.3.4:12345")),
154 "1.2.3.4".parse::<IpAddr>().unwrap()
155 );
156 }
157
158 #[test]
159 fn trusted_peer_uses_cf_connecting_ip() {
160 let ext = extractor(&["10.0.0.1"]);
161 let mut headers = HeaderMap::new();
162 headers.insert("cf-connecting-ip", HeaderValue::from_static("203.0.114.50"));
163
164 assert_eq!(
165 ext.extract(&headers, peer("10.0.0.1:443")),
166 "203.0.114.50".parse::<IpAddr>().unwrap()
167 );
168 }
169
170 #[test]
171 fn cf_connecting_ip_with_whitespace() {
172 let ext = extractor(&["10.0.0.1"]);
173 let mut headers = HeaderMap::new();
174 headers.insert(
175 "cf-connecting-ip",
176 HeaderValue::from_static(" 203.0.114.50 "),
177 );
178
179 assert_eq!(
180 ext.extract(&headers, peer("10.0.0.1:443")),
181 "203.0.114.50".parse::<IpAddr>().unwrap()
182 );
183 }
184
185 #[test]
186 fn cf_connecting_ip_invalid_falls_through() {
187 let ext = extractor(&["10.0.0.1"]);
188 let mut headers = HeaderMap::new();
189 headers.insert("cf-connecting-ip", HeaderValue::from_static("not-an-ip"));
190 headers.insert("x-real-ip", HeaderValue::from_static("5.6.7.8"));
191
192 assert_eq!(
193 ext.extract(&headers, peer("10.0.0.1:443")),
194 "5.6.7.8".parse::<IpAddr>().unwrap()
195 );
196 }
197
198 #[test]
199 fn trusted_peer_uses_x_real_ip() {
200 let ext = extractor(&["10.0.0.1"]);
201 let mut headers = HeaderMap::new();
202 headers.insert("x-real-ip", HeaderValue::from_static("5.6.7.8"));
203
204 assert_eq!(
205 ext.extract(&headers, peer("10.0.0.1:443")),
206 "5.6.7.8".parse::<IpAddr>().unwrap()
207 );
208 }
209
210 #[test]
211 fn cf_connecting_ip_takes_priority_over_x_real_ip() {
212 let ext = extractor(&["10.0.0.1"]);
213 let mut headers = HeaderMap::new();
214 headers.insert("cf-connecting-ip", HeaderValue::from_static("1.1.1.1"));
215 headers.insert("x-real-ip", HeaderValue::from_static("2.2.2.2"));
216
217 assert_eq!(
218 ext.extract(&headers, peer("10.0.0.1:443")),
219 "1.1.1.1".parse::<IpAddr>().unwrap()
220 );
221 }
222
223 #[test]
224 fn x_forwarded_for_single_ip() {
225 let ext = extractor(&["10.0.0.1"]);
226 let mut headers = HeaderMap::new();
227 headers.insert("x-forwarded-for", HeaderValue::from_static("203.0.114.50"));
228
229 assert_eq!(
230 ext.extract(&headers, peer("10.0.0.1:443")),
231 "203.0.114.50".parse::<IpAddr>().unwrap()
232 );
233 }
234
235 #[test]
236 fn x_forwarded_for_rightmost_untrusted() {
237 let ext = extractor(&["10.0.0.1", "10.0.0.2"]);
238 let mut headers = HeaderMap::new();
239 headers.insert(
240 "x-forwarded-for",
241 HeaderValue::from_static("99.99.99.99, 5.6.7.8, 10.0.0.2"),
242 );
243
244 assert_eq!(
245 ext.extract(&headers, peer("10.0.0.1:443")),
246 "5.6.7.8".parse::<IpAddr>().unwrap()
247 );
248 }
249
250 #[test]
251 fn x_forwarded_for_all_trusted_returns_peer() {
252 let ext = extractor(&["10.0.0.1", "10.0.0.2", "10.0.0.3"]);
253 let mut headers = HeaderMap::new();
254 headers.insert(
255 "x-forwarded-for",
256 HeaderValue::from_static("10.0.0.3, 10.0.0.2"),
257 );
258
259 assert_eq!(
260 ext.extract(&headers, peer("10.0.0.1:443")),
261 "10.0.0.1".parse::<IpAddr>().unwrap()
262 );
263 }
264
265 #[test]
266 fn x_forwarded_for_with_whitespace() {
267 let ext = extractor(&["10.0.0.1"]);
268 let mut headers = HeaderMap::new();
269 headers.insert(
270 "x-forwarded-for",
271 HeaderValue::from_static(" 5.6.7.8 , 10.0.0.1 "),
272 );
273
274 assert_eq!(
275 ext.extract(&headers, peer("10.0.0.1:443")),
276 "5.6.7.8".parse::<IpAddr>().unwrap()
277 );
278 }
279
280 #[test]
281 fn x_forwarded_for_with_invalid_entries() {
282 let ext = extractor(&["10.0.0.1"]);
283 let mut headers = HeaderMap::new();
284 headers.insert(
285 "x-forwarded-for",
286 HeaderValue::from_static("5.6.7.8, garbage, not-ip"),
287 );
288
289 assert_eq!(
290 ext.extract(&headers, peer("10.0.0.1:443")),
291 "5.6.7.8".parse::<IpAddr>().unwrap()
292 );
293 }
294
295 #[test]
296 fn no_headers_returns_peer() {
297 let ext = extractor(&["10.0.0.1"]);
298 let headers = HeaderMap::new();
299
300 assert_eq!(
301 ext.extract(&headers, peer("10.0.0.1:443")),
302 "10.0.0.1".parse::<IpAddr>().unwrap()
303 );
304 }
305
306 #[test]
307 fn ipv6_peer_and_header() {
308 let ext = extractor(&["::1"]);
309 let mut headers = HeaderMap::new();
310 headers.insert(
311 "x-real-ip",
312 HeaderValue::from_static("2001:4860:4860::8888"),
313 );
314
315 assert_eq!(
316 ext.extract(&headers, peer("[::1]:443")),
317 "2001:4860:4860::8888".parse::<IpAddr>().unwrap()
318 );
319 }
320
321 #[test]
322 fn ipv6_in_x_forwarded_for() {
323 let ext = extractor(&["::1"]);
324 let mut headers = HeaderMap::new();
325 headers.insert(
326 "x-forwarded-for",
327 HeaderValue::from_static("2606:4700::1, ::1"),
328 );
329
330 assert_eq!(
331 ext.extract(&headers, peer("[::1]:443")),
332 "2606:4700::1".parse::<IpAddr>().unwrap()
333 );
334 }
335
336 #[test]
337 fn invalid_proxy_strings_are_skipped() {
338 let ext = IpExtractor::new(&[
339 "10.0.0.1".to_string(),
340 "not-an-ip".to_string(),
341 "".to_string(),
342 "10.0.0.2".to_string(),
343 ])
344 .unwrap();
345 assert_eq!(ext.trusted_proxies.len(), 2);
346 }
347
348 #[test]
349 fn cidr_trusted_proxy_matches_subnet() {
350 let ext = extractor(&["10.0.0.0/8"]);
351 let mut headers = HeaderMap::new();
352 headers.insert("x-real-ip", HeaderValue::from_static("1.2.3.4"));
353
354 assert_eq!(
355 ext.extract(&headers, peer("10.0.0.5:443")),
356 "1.2.3.4".parse::<IpAddr>().unwrap()
357 );
358 }
359
360 #[test]
361 fn cidr_xff_skips_trusted_ranges() {
362 let ext = extractor(&["10.0.0.0/8", "172.16.0.0/12"]);
363 let mut headers = HeaderMap::new();
364 headers.insert(
365 "x-forwarded-for",
366 HeaderValue::from_static("8.8.8.8, 10.0.0.1, 172.16.0.1"),
367 );
368
369 assert_eq!(
370 ext.extract(&headers, peer("172.16.0.1:443")),
371 "8.8.8.8".parse::<IpAddr>().unwrap()
372 );
373 }
374
375 #[test]
376 fn cidr_mixed_exact_and_range() {
377 let ext = extractor(&["10.0.0.0/8", "192.168.1.1"]);
378 let mut headers = HeaderMap::new();
379 headers.insert("x-real-ip", HeaderValue::from_static("5.6.7.8"));
380
381 assert_eq!(
383 ext.extract(&headers, peer("192.168.1.1:443")),
384 "5.6.7.8".parse::<IpAddr>().unwrap()
385 );
386 assert_eq!(
388 ext.extract(&headers, peer("10.99.99.99:443")),
389 "5.6.7.8".parse::<IpAddr>().unwrap()
390 );
391 }
392
393 #[test]
394 fn is_empty_true_when_no_proxies() {
395 let ext = extractor(&[]);
396 assert!(ext.is_empty());
397 }
398
399 #[test]
400 fn is_empty_false_when_proxies_configured() {
401 let ext = extractor(&["10.0.0.1"]);
402 assert!(!ext.is_empty());
403 }
404
405 #[test]
406 fn untrusted_peer_ignores_xff() {
407 let ext = extractor(&["10.0.0.1"]);
408 let mut headers = HeaderMap::new();
409 headers.insert("x-forwarded-for", HeaderValue::from_static("5.6.7.8, 9.10.11.12"));
410 headers.insert("cf-connecting-ip", HeaderValue::from_static("5.6.7.8"));
411
412 assert_eq!(
413 ext.extract(&headers, peer("1.2.3.4:12345")),
414 "1.2.3.4".parse::<IpAddr>().unwrap()
415 );
416 }
417
418 #[test]
419 fn bare_ip_auto_promotes_to_host_network() {
420 let ext = extractor(&["10.0.0.1"]);
422 let mut headers = HeaderMap::new();
423 headers.insert("x-real-ip", HeaderValue::from_static("1.2.3.4"));
424
425 assert_eq!(
427 ext.extract(&headers, peer("10.0.0.1:443")),
428 "1.2.3.4".parse::<IpAddr>().unwrap()
429 );
430 assert_eq!(
432 ext.extract(&headers, peer("10.0.0.2:443")),
433 "10.0.0.2".parse::<IpAddr>().unwrap()
434 );
435 }
436}