1use std::{
25 net::{IpAddr, SocketAddr},
26 str::FromStr,
27};
28
29use http::{HeaderMap, HeaderName};
30
31use crate::Error;
32
33macro_rules! client_ip_headers {
34 (
35 $(
36 $(#[$docs:meta])*
37 ($variant:ident, $name:literal, $extractor:path);
38 )+
39 ) => {
40 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
45 pub enum ClientIpHeader {
46 $(
47 $(#[$docs])*
48 $variant,
49 )+
50 }
51
52 impl FromStr for ClientIpHeader {
53 type Err = Error;
54
55 fn from_str(source: &str) -> Result<Self, Self::Err> {
56 let name = HeaderName::from_bytes(source.as_bytes())
57 .map_err(|_| Error::unsupported_header_name(source))?;
58
59 match name.as_str() {
60 $(
61 $name => Ok(Self::$variant),
62 )+
63 _ => Err(Error::unsupported_header_name(source)),
64 }
65 }
66 }
67
68 impl ClientIpHeader {
69 fn extract(self, headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
70 match self {
71 $(
72 Self::$variant => ($extractor)(headers),
73 )+
74 }
75 }
76 }
77 };
78}
79
80client_ip_headers! {
81 (
83 CfConnectingIp,
84 "cf-connecting-ip",
85 crate::client_ip_headers::extract_header_cf_connecting_ip
86 );
87 (XRealIp, "x-real-ip", crate::client_ip_headers::extract_header_x_real_ip);
89 (Forwarded, "forwarded", crate::forwarded::extract_rightmost_forwarded);
91 (
93 XForwardedFor,
94 "x-forwarded-for",
95 crate::x_forwarded::extract_rightmost_x_forwarded_for
96 );
97 (
99 CloudFrontViewerAddress,
100 "cloudfront-viewer-address",
101 crate::client_ip_headers::extract_header_cloudfront_viewer_address
102 );
103 (
105 FlyClientIp,
106 "fly-client-ip",
107 crate::client_ip_headers::extract_header_fly_client_ip
108 );
109 (
111 TrueClientIp,
112 "true-client-ip",
113 crate::client_ip_headers::extract_header_true_client_ip
114 );
115 (
117 XEnvoyExternalAddress,
118 "x-envoy-external-address",
119 crate::client_ip_headers::extract_header_x_envoy_external_address
120 );
121}
122
123impl TryFrom<&str> for ClientIpHeader {
124 type Error = Error;
125
126 fn try_from(source: &str) -> Result<Self, Self::Error> {
127 source.parse()
128 }
129}
130
131pub fn extract_request_socket_address<B>(request: &http::Request<B>) -> Option<SocketAddr> {
138 request.extensions().get::<SocketAddr>().copied()
139}
140
141pub fn extract_request_socket_ip<B>(request: &http::Request<B>) -> Option<IpAddr> {
147 extract_request_socket_address(request).map(|address| address.ip())
148}
149
150#[cfg(feature = "axum")]
157pub fn extract_axum_socket_address<B>(request: &http::Request<B>) -> Option<SocketAddr> {
158 request
159 .extensions()
160 .get::<axum::extract::ConnectInfo<SocketAddr>>()
161 .map(|info| info.0)
162}
163
164#[cfg(feature = "axum")]
172pub fn extract_axum_socket_ip<B>(request: &http::Request<B>) -> Option<IpAddr> {
173 extract_axum_socket_address(request).map(|peer| peer.ip())
174}
175
176pub fn extract_socket_ip<B>(request: &http::Request<B>) -> Option<IpAddr> {
183 #[cfg(feature = "axum")]
184 if let Some(ip) = extract_axum_socket_ip(request) {
185 return Some(ip);
186 }
187
188 extract_request_socket_ip(request)
189}
190
191pub const CLIENT_IP_HEADERS: &[ClientIpHeader] = &[
198 ClientIpHeader::Forwarded,
199 ClientIpHeader::XForwardedFor,
200 ClientIpHeader::XRealIp,
201 ClientIpHeader::CfConnectingIp,
202];
203
204pub fn extract_client_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
215 extract_client_ip_with_headers(headers, CLIENT_IP_HEADERS)
216}
217
218pub fn extract_client_ip_with_headers(
228 headers: &HeaderMap,
229 sources: &[ClientIpHeader],
230) -> Result<Option<IpAddr>, Error> {
231 for source in sources {
232 if let Some(ip) = source.extract(headers)? {
233 return Ok(Some(ip));
234 }
235 }
236
237 Ok(None)
238}
239
240pub fn extract_proxy_client_ip<B>(request: &http::Request<B>) -> Result<Option<IpAddr>, Error> {
252 Ok(extract_client_ip(request.headers())?.or_else(|| extract_socket_ip(request)))
253}
254
255#[cfg(test)]
256mod tests {
257 use http::HeaderMap;
258
259 use crate::{forwarded::FORWARDED, x_forwarded::X_FORWARDED_FOR};
260
261 use super::*;
262 #[test]
263 fn request_socket_functions_read_the_socket_addr_extension() {
264 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
265 let mut request = http::Request::new(());
266 request.extensions_mut().insert(peer);
267
268 assert_eq!(extract_request_socket_address(&request), Some(peer));
269 assert_eq!(extract_request_socket_ip(&request), Some(peer.ip()));
270 }
271
272 #[test]
273 fn request_socket_functions_return_none_without_the_extension() {
274 let request = http::Request::new(());
275
276 assert_eq!(extract_request_socket_address(&request), None);
277 assert_eq!(extract_request_socket_ip(&request), None);
278 }
279
280 #[test]
281 fn socket_ip_reads_the_direct_socket_addr_extension() {
282 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
283 let mut request = http::Request::new(());
284 request.extensions_mut().insert(peer);
285
286 assert_eq!(extract_socket_ip(&request), Some(peer.ip()));
287 }
288
289 #[test]
290 fn socket_ip_returns_none_without_a_peer_extension() {
291 assert_eq!(extract_socket_ip(&http::Request::new(())), None);
292 }
293
294 #[cfg(feature = "axum")]
295 #[test]
296 fn request_socket_and_axum_socket_extractors_are_independent() {
297 let request_peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
298 let axum_peer: SocketAddr = "198.51.100.10:8080".parse().unwrap();
299 let mut request = http::Request::new(());
300
301 request.extensions_mut().insert(request_peer);
302 request
303 .extensions_mut()
304 .insert(axum::extract::ConnectInfo(axum_peer));
305
306 assert_eq!(extract_request_socket_address(&request), Some(request_peer));
307 assert_eq!(extract_request_socket_ip(&request), Some(request_peer.ip()));
308 assert_eq!(extract_axum_socket_address(&request), Some(axum_peer));
309 assert_eq!(extract_axum_socket_ip(&request), Some(axum_peer.ip()));
310 assert_eq!(extract_socket_ip(&request), Some(axum_peer.ip()));
311 }
312
313 #[test]
314 fn proxy_client_ip_prefers_a_header_over_the_peer() {
315 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
316 let mut request = http::Request::new(());
317 request.extensions_mut().insert(peer);
318 request
319 .headers_mut()
320 .insert(FORWARDED, "for=198.51.100.10".parse().unwrap());
321
322 assert_eq!(
323 extract_proxy_client_ip(&request).unwrap(),
324 Some("198.51.100.10".parse().unwrap())
325 );
326 }
327
328 #[test]
329 fn proxy_client_ip_falls_back_to_the_peer_when_headers_are_absent() {
330 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
331 let mut request = http::Request::new(());
332 request.extensions_mut().insert(peer);
333
334 assert_eq!(extract_proxy_client_ip(&request).unwrap(), Some(peer.ip()));
335 }
336
337 #[cfg(feature = "axum")]
338 #[test]
339 fn proxy_client_ip_falls_back_to_the_axum_peer() {
340 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
341 let mut request = http::Request::new(());
342 request
343 .extensions_mut()
344 .insert(axum::extract::ConnectInfo(peer));
345
346 assert_eq!(extract_proxy_client_ip(&request).unwrap(), Some(peer.ip()));
347 }
348
349 #[test]
350 fn proxy_client_ip_does_not_fall_back_after_an_invalid_header() {
351 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
352 let mut request = http::Request::new(());
353 request.extensions_mut().insert(peer);
354 request
355 .headers_mut()
356 .insert(FORWARDED, "for=not-an-ip".parse().unwrap());
357
358 assert!(extract_proxy_client_ip(&request).is_err());
359 }
360
361 #[test]
362 fn proxy_client_ip_returns_none_without_headers_or_peer() {
363 assert_eq!(
364 extract_proxy_client_ip(&http::Request::new(())).unwrap(),
365 None
366 );
367 }
368
369 #[cfg(feature = "axum")]
370 #[test]
371 fn axum_socket_address_reads_connect_info_extension() {
372 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
373 let mut request = http::Request::new(());
374 request
375 .extensions_mut()
376 .insert(axum::extract::ConnectInfo(peer));
377
378 assert_eq!(extract_axum_socket_address(&request), Some(peer));
379 }
380
381 #[cfg(feature = "axum")]
382 #[test]
383 fn axum_socket_address_returns_none_without_connect_info() {
384 let request = http::Request::new(());
385
386 assert_eq!(extract_axum_socket_address(&request), None);
387 }
388
389 #[cfg(feature = "axum")]
390 #[test]
391 fn axum_socket_ip_reads_connect_info_extension() {
392 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
393 let mut request = http::Request::new(());
394 request
395 .extensions_mut()
396 .insert(axum::extract::ConnectInfo(peer));
397
398 assert_eq!(extract_axum_socket_ip(&request), Some(peer.ip()));
399 }
400
401 #[cfg(feature = "axum")]
402 #[test]
403 fn axum_socket_ip_returns_none_without_connect_info() {
404 let request = http::Request::new(());
405
406 assert_eq!(extract_axum_socket_ip(&request), None);
407 }
408
409 #[test]
410 fn default_order_is_stable_and_first_present_header_wins() {
411 assert_eq!(
412 CLIENT_IP_HEADERS,
413 &[
414 ClientIpHeader::Forwarded,
415 ClientIpHeader::XForwardedFor,
416 ClientIpHeader::XRealIp,
417 ClientIpHeader::CfConnectingIp,
418 ]
419 );
420
421 let mut headers = HeaderMap::new();
422 headers.insert("cf-connecting-ip", "192.0.2.1".parse().unwrap());
423 headers.insert("x-real-ip", "192.0.2.2".parse().unwrap());
424 headers.insert(&FORWARDED, "for=192.0.2.3".parse().unwrap());
425 headers.insert(&X_FORWARDED_FOR, "192.0.2.4".parse().unwrap());
426
427 assert_eq!(
428 extract_client_ip(&headers).unwrap(),
429 Some("192.0.2.3".parse().unwrap())
430 );
431 }
432
433 #[test]
434 fn default_order_falls_through_only_when_a_header_is_absent() {
435 let mut headers = HeaderMap::new();
436 headers.insert(&FORWARDED, "for=192.0.2.3".parse().unwrap());
437 headers.insert(&X_FORWARDED_FOR, "192.0.2.4".parse().unwrap());
438
439 assert_eq!(
440 extract_client_ip(&headers).unwrap(),
441 Some("192.0.2.3".parse().unwrap())
442 );
443
444 headers.insert("x-real-ip", "not-an-ip".parse().unwrap());
445 assert_eq!(
446 extract_client_ip(&headers).unwrap(),
447 Some("192.0.2.3".parse().unwrap())
448 );
449
450 headers.insert(&FORWARDED, "for=unknown".parse().unwrap());
451 assert!(matches!(
452 extract_client_ip(&headers),
453 Err(Error::InvalidHeader { .. })
454 ));
455 }
456
457 #[test]
458 fn chain_headers_return_the_rightmost_address() {
459 let mut headers = HeaderMap::new();
460 headers.insert(
461 &FORWARDED,
462 "for=192.0.2.1, for=198.51.100.2".parse().unwrap(),
463 );
464 assert_eq!(
465 extract_client_ip_with_headers(&headers, &[ClientIpHeader::Forwarded]).unwrap(),
466 Some("198.51.100.2".parse().unwrap())
467 );
468
469 headers.remove(&FORWARDED);
470 headers.insert(&X_FORWARDED_FOR, "192.0.2.1, 198.51.100.3".parse().unwrap());
471 assert_eq!(
472 extract_client_ip_with_headers(&headers, &[ClientIpHeader::XForwardedFor]).unwrap(),
473 Some("198.51.100.3".parse().unwrap())
474 );
475 }
476
477 #[test]
478 fn custom_order_changes_precedence() {
479 let mut headers = HeaderMap::new();
480 headers.insert("cf-connecting-ip", "192.0.2.1".parse().unwrap());
481 headers.insert("x-real-ip", "192.0.2.2".parse().unwrap());
482 let custom_headers = [ClientIpHeader::XRealIp, ClientIpHeader::CfConnectingIp];
483
484 assert_eq!(
485 extract_client_ip_with_headers(&headers, &custom_headers).unwrap(),
486 Some("192.0.2.2".parse().unwrap())
487 );
488 assert_eq!(extract_client_ip_with_headers(&headers, &[]).unwrap(), None);
489 }
490
491 #[test]
492 fn custom_order_supports_every_documented_single_value_header() {
493 for (source, header) in [
494 (ClientIpHeader::CfConnectingIp, "cf-connecting-ip"),
495 (ClientIpHeader::XRealIp, "x-real-ip"),
496 (ClientIpHeader::FlyClientIp, "fly-client-ip"),
497 (ClientIpHeader::TrueClientIp, "true-client-ip"),
498 (
499 ClientIpHeader::XEnvoyExternalAddress,
500 "x-envoy-external-address",
501 ),
502 ] {
503 let mut headers = HeaderMap::new();
504 headers.insert(header, "192.0.2.10".parse().unwrap());
505
506 assert_eq!(
507 extract_client_ip_with_headers(&headers, &[source]).unwrap(),
508 Some("192.0.2.10".parse().unwrap()),
509 "source {header}",
510 );
511 }
512
513 let mut headers = HeaderMap::new();
514 headers.insert(
515 "cloudfront-viewer-address",
516 "192.0.2.10:443".parse().unwrap(),
517 );
518 assert_eq!(
519 extract_client_ip_with_headers(&headers, &[ClientIpHeader::CloudFrontViewerAddress],)
520 .unwrap(),
521 Some("192.0.2.10".parse().unwrap())
522 );
523 }
524
525 #[test]
526 fn parses_supported_header_names() {
527 for (name, expected) in [
528 ("cf-connecting-ip", ClientIpHeader::CfConnectingIp),
529 ("X-Real-IP", ClientIpHeader::XRealIp),
530 ("forwarded", ClientIpHeader::Forwarded),
531 ("x-forwarded-for", ClientIpHeader::XForwardedFor),
532 (
533 "cloudfront-viewer-address",
534 ClientIpHeader::CloudFrontViewerAddress,
535 ),
536 ("fly-client-ip", ClientIpHeader::FlyClientIp),
537 ("true-client-ip", ClientIpHeader::TrueClientIp),
538 (
539 "x-envoy-external-address",
540 ClientIpHeader::XEnvoyExternalAddress,
541 ),
542 ] {
543 assert_eq!(ClientIpHeader::try_from(name).unwrap(), expected);
544 assert_eq!(name.parse::<ClientIpHeader>().unwrap(), expected);
545 }
546
547 for header in ["forwarded-for", "not a header"] {
548 let error = header.parse::<ClientIpHeader>().unwrap_err();
549 assert!(matches!(error, Error::UnsupportedHeaderName { .. }));
550 assert!(error.to_string().contains(header));
551 }
552 }
553
554 #[test]
555 fn malformed_selected_source_does_not_fall_through() {
556 let mut headers = HeaderMap::new();
557 headers.insert(&FORWARDED, "for=unknown".parse().unwrap());
558 headers.insert(&X_FORWARDED_FOR, "192.0.2.4".parse().unwrap());
559
560 assert!(matches!(
561 extract_client_ip_with_headers(
562 &headers,
563 &[ClientIpHeader::Forwarded, ClientIpHeader::XForwardedFor,],
564 ),
565 Err(Error::InvalidHeader { .. })
566 ));
567 }
568}