Skip to main content

http_extract/
client_ip.rs

1//! Peer extraction and proxy-aware client IP selection.
2//!
3//! [`extract_request_socket_address`] and [`extract_request_socket_ip`] read a
4//! directly stored [`SocketAddr`] request extension. With the `axum` feature,
5//! [`extract_axum_socket_address`] and [`extract_axum_socket_ip`] read Axum's
6//! `ConnectInfo<SocketAddr>` extension. [`extract_socket_ip`] composes those
7//! sources without inspecting HTTP fields.
8//!
9//! Separately, [`extract_client_ip`] uses [`CLIENT_IP_HEADERS`], while
10//! [`extract_client_ip_with_headers`] lets callers choose the fields and their
11//! order explicitly using [`ClientIpHeader`] values. [`extract_proxy_client_ip`]
12//! uses the default Header order and falls back to [`extract_socket_ip`].
13//! The Header selectors cannot authenticate the sender, so Header-derived
14//! results remain raw and untrusted. Applications must establish the relevant
15//! proxy or CDN trust boundary before using a result for authorization, abuse
16//! prevention, or rate limiting.
17//!
18//! RFC 7239 standardizes `Forwarded`; `X-Forwarded-For` and the single-value
19//! provider/proxy fields are de facto conventions rather than IETF standards.
20//! No standard defines precedence between these different field names.
21//!
22//! [RFC 7239]: https://www.rfc-editor.org/rfc/rfc7239.html
23
24use 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        /// A supported client IP Header and its parsing rule.
41        ///
42        /// Choosing a variant does not authenticate the sender or make the
43        /// extracted value trustworthy.
44        #[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    /// `CF-Connecting-IP`.
82    (
83        CfConnectingIp,
84        "cf-connecting-ip",
85        crate::client_ip_headers::extract_header_cf_connecting_ip
86    );
87    /// `X-Real-IP`.
88    (XRealIp, "x-real-ip", crate::client_ip_headers::extract_header_x_real_ip);
89    /// RFC 7239 `Forwarded`.
90    (Forwarded, "forwarded", crate::forwarded::extract_rightmost_forwarded);
91    /// `X-Forwarded-For`.
92    (
93        XForwardedFor,
94        "x-forwarded-for",
95        crate::x_forwarded::extract_rightmost_x_forwarded_for
96    );
97    /// `CloudFront-Viewer-Address`.
98    (
99        CloudFrontViewerAddress,
100        "cloudfront-viewer-address",
101        crate::client_ip_headers::extract_header_cloudfront_viewer_address
102    );
103    /// `Fly-Client-IP`.
104    (
105        FlyClientIp,
106        "fly-client-ip",
107        crate::client_ip_headers::extract_header_fly_client_ip
108    );
109    /// `True-Client-IP`.
110    (
111        TrueClientIp,
112        "true-client-ip",
113        crate::client_ip_headers::extract_header_true_client_ip
114    );
115    /// `X-Envoy-External-Address`.
116    (
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
131/// Extract a `SocketAddr` stored directly in a request extension.
132///
133/// A request does not inherently contain this network fact. This function
134/// only reads a `SocketAddr` previously inserted by a server adapter or
135/// application and returns `None` when the extension is absent. It does not
136/// inspect forwarding Headers or establish that the value is trustworthy.
137pub fn extract_request_socket_address<B>(request: &http::Request<B>) -> Option<SocketAddr> {
138    request.extensions().get::<SocketAddr>().copied()
139}
140
141/// Extract the IP component of a `SocketAddr` request extension.
142///
143/// This delegates to [`extract_request_socket_address`]. It returns `None`
144/// when the extension is absent and does not inspect forwarding Headers or
145/// apply a proxy trust policy.
146pub 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/// Extract the Axum transport peer stored in a request extension.
151///
152/// This reads `axum::extract::ConnectInfo<SocketAddr>` inserted by
153/// `Router::into_make_service_with_connect_info` or explicitly by a test. It
154/// returns `None` when that extension is absent. The returned address is the
155/// socket peer; this function neither parses nor trusts forwarding Headers.
156#[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/// Extract the Axum socket peer IP stored in a request extension.
165///
166/// This returns the [`IpAddr`] from the
167/// `axum::extract::ConnectInfo<SocketAddr>` request extension, or `None` when
168/// that extension is absent. It does not parse `Forwarded`,
169/// `X-Forwarded-For`, or vendor Headers, so it is not a Header-derived or
170/// effective client IP.
171#[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
176/// Extract the request's socket peer IP without inspecting HTTP fields.
177///
178/// With the `axum` feature, Axum's `ConnectInfo<SocketAddr>` extension takes
179/// precedence. If it is absent, this falls back to a directly stored
180/// `SocketAddr` request extension. It returns `None` when neither extension is
181/// present and does not inspect forwarding Headers.
182pub 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
191/// The effective header lookup order used by [`extract_client_ip`].
192///
193/// The standardized `Forwarded` field is checked first, followed by the de
194/// facto `X-Forwarded-For`, `X-Real-IP`, and `CF-Connecting-IP` fields. The
195/// first present source wins. This standard-first order is a library
196/// convention, not an RFC-defined precedence or trust policy.
197pub const CLIENT_IP_HEADERS: &[ClientIpHeader] = &[
198    ClientIpHeader::Forwarded,
199    ClientIpHeader::XForwardedFor,
200    ClientIpHeader::XRealIp,
201    ClientIpHeader::CfConnectingIp,
202];
203
204/// Extract a raw client IP assertion using the effective field order.
205///
206/// This delegates to [`extract_client_ip_with_headers`] with
207/// [`CLIENT_IP_HEADERS`]. A missing value in every header returns `None`. A
208/// malformed, duplicate, or non-text value in the first present header returns
209/// an error without consulting lower-priority headers.
210///
211/// For `Forwarded` and `X-Forwarded-For`, this returns the rightmost address,
212/// which is the assertion nearest the server. The result is still untrusted;
213/// this function has no transport-peer or trusted-proxy configuration.
214pub fn extract_client_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
215    extract_client_ip_with_headers(headers, CLIENT_IP_HEADERS)
216}
217
218/// Extract a raw client IP assertion using caller-defined fields and order.
219///
220/// Sources are checked from left to right and the first present value wins. An
221/// empty order, or no value in any configured source, returns `None`. If a
222/// source is present but malformed, duplicate, or non-text, its error is
223/// returned immediately instead of falling through to another source.
224///
225/// `Forwarded` and `X-Forwarded-For` contribute their rightmost address. All
226/// results remain raw and untrusted regardless of the chosen order.
227pub 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
240/// Extract a proxy-aware client IP, falling back to the socket peer.
241///
242/// This first applies [`extract_client_ip`] to the request Headers. Only when
243/// every Header in [`CLIENT_IP_HEADERS`] is absent does it fall back to
244/// [`extract_socket_ip`]. A malformed, duplicate, or non-text first-present
245/// Header returns an error without consulting the peer.
246///
247/// Header-derived addresses remain raw assertions. Use this function only
248/// when the deployment restricts access to trusted proxies that remove or
249/// overwrite every supported client-IP Header. This function does not verify
250/// trusted proxy addresses or CIDRs.
251pub 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}