Skip to main content

http_extract/
client_ip_headers.rs

1//! Direct extraction of common single-value client IP fields.
2//!
3//! Each function parses exactly one provider or proxy field and returns its raw
4//! asserted [`IpAddr`]. `CF-Connecting-IP`, `CloudFront-Viewer-Address`,
5//! `Fly-Client-IP`, `True-Client-IP`, `X-Envoy-External-Address`, and
6//! `X-Real-IP` are vendor or de facto conventions, not IETF standards.
7//!
8//! These values are untrusted: this module does not authenticate a proxy or
9//! apply a trust policy. The feature-gated [`crate::extract_client_ip`]
10//! convenience includes selected fields, but its output remains untrusted.
11//! Applications must decide whether a specific field is trustworthy for their
12//! deployment. Every extractor treats
13//! the field as singular: absence returns `None`, while duplicate, non-text,
14//! empty, or malformed values return a value-redacting error.
15
16use std::net::{IpAddr, SocketAddr};
17
18use http::{HeaderMap, HeaderName, Request};
19
20use crate::{Error, header::extract_single_header_text};
21
22/// The `CF-Connecting-IP` field name.
23pub const CF_CONNECTING_IP: HeaderName = HeaderName::from_static("cf-connecting-ip");
24
25/// The `CloudFront-Viewer-Address` field name.
26pub const CLOUDFRONT_VIEWER_ADDRESS: HeaderName =
27    HeaderName::from_static("cloudfront-viewer-address");
28
29/// The `Fly-Client-IP` field name.
30pub const FLY_CLIENT_IP: HeaderName = HeaderName::from_static("fly-client-ip");
31
32/// The `True-Client-IP` field name.
33pub const TRUE_CLIENT_IP: HeaderName = HeaderName::from_static("true-client-ip");
34
35/// The `X-Envoy-External-Address` field name.
36pub const X_ENVOY_EXTERNAL_ADDRESS: HeaderName =
37    HeaderName::from_static("x-envoy-external-address");
38
39/// The `X-Real-IP` field name.
40pub const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip");
41
42/// Extract the raw, untrusted IP asserted by `CF-Connecting-IP`.
43///
44/// This reads only the [`HeaderMap`]. A missing field returns `None`; duplicate,
45/// non-text, empty, or non-IP values return an error without including the
46/// value. Surrounding whitespace is ignored. This vendor field is not an IETF
47/// standard, and the function does not authenticate Cloudflare or the sender.
48pub fn extract_header_cf_connecting_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
49    extract_single_ip(headers, &CF_CONNECTING_IP)
50}
51
52/// Extract `CF-Connecting-IP` from a complete request.
53///
54/// This reads `request.headers()` and delegates to
55/// [`extract_header_cf_connecting_ip`], preserving its missing and strict error
56/// behavior. The returned assertion remains raw and untrusted.
57pub fn extract_request_cf_connecting_ip<B>(request: &Request<B>) -> Result<Option<IpAddr>, Error> {
58    extract_header_cf_connecting_ip(request.headers())
59}
60
61/// Extract an untrusted client IP from AWS CloudFront's
62/// `CloudFront-Viewer-Address` `IP:port` value.
63///
64/// Both IPv4 and CloudFront's unbracketed IPv6-with-port form are supported;
65/// bracketed IPv6 socket-address syntax is accepted as well. A missing field
66/// returns `None`; duplicate, non-text, empty, missing-port, invalid-port, or
67/// invalid-IP values return an error without including the value. Surrounding
68/// whitespace is ignored. This vendor field is not an IETF standard, and the
69/// result remains raw and untrusted; no sender authentication is performed.
70pub fn extract_header_cloudfront_viewer_address(
71    headers: &HeaderMap,
72) -> Result<Option<IpAddr>, Error> {
73    extract_single_header_text(headers, &CLOUDFRONT_VIEWER_ADDRESS)?
74        .map(parse_cloudfront_viewer_address)
75        .transpose()
76}
77
78/// Extract `CloudFront-Viewer-Address` from a complete request.
79///
80/// This reads `request.headers()` and delegates to
81/// [`extract_header_cloudfront_viewer_address`], preserving its missing and
82/// strict `IP:port` error behavior. The returned assertion remains raw and
83/// untrusted.
84pub fn extract_request_cloudfront_viewer_address<B>(
85    request: &Request<B>,
86) -> Result<Option<IpAddr>, Error> {
87    extract_header_cloudfront_viewer_address(request.headers())
88}
89
90/// Extract the raw, untrusted IP asserted by `Fly-Client-IP`.
91///
92/// This reads only the [`HeaderMap`]. A missing field returns `None`; duplicate,
93/// non-text, empty, or non-IP values return an error without including the
94/// value. Surrounding whitespace is ignored. This vendor field is not an IETF
95/// standard, and the function does not authenticate Fly.io or the sender.
96pub fn extract_header_fly_client_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
97    extract_single_ip(headers, &FLY_CLIENT_IP)
98}
99
100/// Extract `Fly-Client-IP` from a complete request.
101///
102/// This reads `request.headers()` and delegates to
103/// [`extract_header_fly_client_ip`], preserving its missing and strict error
104/// behavior. The returned assertion remains raw and untrusted.
105pub fn extract_request_fly_client_ip<B>(request: &Request<B>) -> Result<Option<IpAddr>, Error> {
106    extract_header_fly_client_ip(request.headers())
107}
108
109/// Extract an untrusted client IP asserted by `True-Client-IP`.
110///
111/// This reads only the [`HeaderMap`]. A missing field returns `None`; duplicate,
112/// non-text, empty, or non-IP values return an error without including the
113/// value. Surrounding whitespace is ignored. This de facto, non-IETF field is
114/// commonly emitted by configured CDN or proxy products; its presence alone
115/// does not authenticate the sender or establish trust.
116pub fn extract_header_true_client_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
117    extract_single_ip(headers, &TRUE_CLIENT_IP)
118}
119
120/// Extract `True-Client-IP` from a complete request.
121///
122/// This reads `request.headers()` and delegates to
123/// [`extract_header_true_client_ip`], preserving its missing and strict error
124/// behavior. The returned assertion remains raw and untrusted.
125pub fn extract_request_true_client_ip<B>(request: &Request<B>) -> Result<Option<IpAddr>, Error> {
126    extract_header_true_client_ip(request.headers())
127}
128
129/// Extract an untrusted client IP asserted by Envoy's
130/// `X-Envoy-External-Address`.
131///
132/// This reads only the [`HeaderMap`]. A missing field returns `None`; duplicate,
133/// non-text, empty, or non-IP values return an error without including the
134/// value. Surrounding whitespace is ignored. This de facto, non-IETF field does
135/// not authenticate Envoy or the sender, so its result remains raw and
136/// untrusted.
137pub fn extract_header_x_envoy_external_address(
138    headers: &HeaderMap,
139) -> Result<Option<IpAddr>, Error> {
140    extract_single_ip(headers, &X_ENVOY_EXTERNAL_ADDRESS)
141}
142
143/// Extract `X-Envoy-External-Address` from a complete request.
144///
145/// This reads `request.headers()` and delegates to
146/// [`extract_header_x_envoy_external_address`], preserving its missing and
147/// strict error behavior. The returned assertion remains raw and untrusted.
148pub fn extract_request_x_envoy_external_address<B>(
149    request: &Request<B>,
150) -> Result<Option<IpAddr>, Error> {
151    extract_header_x_envoy_external_address(request.headers())
152}
153
154/// Extract the raw, untrusted IP asserted by `X-Real-IP`.
155///
156/// This reads only the [`HeaderMap`]. A missing field returns `None`; duplicate,
157/// non-text, empty, or non-IP values return an error without including the
158/// value. Surrounding whitespace is ignored. This de facto field is not an
159/// IETF standard and does not authenticate the sender.
160pub fn extract_header_x_real_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
161    extract_single_ip(headers, &X_REAL_IP)
162}
163
164/// Extract `X-Real-IP` from a complete request.
165///
166/// This reads `request.headers()` and delegates to
167/// [`extract_header_x_real_ip`], preserving its missing and strict error
168/// behavior. The returned assertion remains raw and untrusted.
169pub fn extract_request_x_real_ip<B>(request: &Request<B>) -> Result<Option<IpAddr>, Error> {
170    extract_header_x_real_ip(request.headers())
171}
172
173fn extract_single_ip(headers: &HeaderMap, name: &HeaderName) -> Result<Option<IpAddr>, Error> {
174    extract_single_header_text(headers, name)?
175        .map(|value| parse_ip(value, name))
176        .transpose()
177}
178
179fn parse_ip(value: &str, name: &HeaderName) -> Result<IpAddr, Error> {
180    value
181        .trim()
182        .parse()
183        .map_err(|_| Error::invalid_header(name.clone()))
184}
185
186fn parse_cloudfront_viewer_address(value: &str) -> Result<IpAddr, Error> {
187    let value = value.trim();
188    if let Ok(address) = value.parse::<SocketAddr>() {
189        return Ok(address.ip());
190    }
191
192    let (address, port) = value
193        .rsplit_once(':')
194        .ok_or_else(|| Error::invalid_header(CLOUDFRONT_VIEWER_ADDRESS))?;
195    port.parse::<u16>()
196        .map_err(|_| Error::invalid_header(CLOUDFRONT_VIEWER_ADDRESS))?;
197    address
198        .parse()
199        .map_err(|_| Error::invalid_header(CLOUDFRONT_VIEWER_ADDRESS))
200}
201
202#[cfg(test)]
203mod tests {
204    use http::{HeaderMap, HeaderValue};
205
206    use super::*;
207
208    type Extractor = fn(&HeaderMap) -> Result<Option<IpAddr>, Error>;
209
210    fn ordinary_extractors() -> [(HeaderName, Extractor); 5] {
211        [
212            (CF_CONNECTING_IP, extract_header_cf_connecting_ip),
213            (FLY_CLIENT_IP, extract_header_fly_client_ip),
214            (TRUE_CLIENT_IP, extract_header_true_client_ip),
215            (
216                X_ENVOY_EXTERNAL_ADDRESS,
217                extract_header_x_envoy_external_address,
218            ),
219            (X_REAL_IP, extract_header_x_real_ip),
220        ]
221    }
222
223    #[test]
224    fn single_ip_fields_handle_missing_valid_and_invalid_values() {
225        for (name, extract) in ordinary_extractors() {
226            let mut headers = HeaderMap::new();
227            assert_eq!(extract(&headers).unwrap(), None);
228
229            headers.insert(&name, " 2001:db8::1 ".parse().unwrap());
230            assert_eq!(
231                extract(&headers).unwrap(),
232                Some("2001:db8::1".parse().unwrap())
233            );
234
235            headers.insert(&name, "not-an-ip".parse().unwrap());
236            let error = extract(&headers).unwrap_err();
237            assert!(matches!(error, Error::InvalidHeader { .. }));
238            assert!(!error.to_string().contains("not-an-ip"));
239        }
240    }
241
242    #[test]
243    fn every_single_ip_field_rejects_duplicates() {
244        for (name, extract) in ordinary_extractors() {
245            let mut headers = HeaderMap::new();
246            headers.append(&name, "192.0.2.1".parse().unwrap());
247            headers.append(&name, "198.51.100.2".parse().unwrap());
248            assert!(matches!(
249                extract(&headers),
250                Err(Error::DuplicateHeader { .. })
251            ));
252        }
253    }
254
255    #[test]
256    fn cloudfront_viewer_address_handles_ipv4_and_ipv6_with_ports() {
257        let mut headers = HeaderMap::new();
258        assert_eq!(
259            extract_header_cloudfront_viewer_address(&headers).unwrap(),
260            None
261        );
262
263        for (value, expected) in [
264            ("198.51.100.10:46532", "198.51.100.10"),
265            ("2001:db8::abcd:1234", "2001:db8::abcd"),
266            ("[2001:db8::17]:4711", "2001:db8::17"),
267        ] {
268            headers.insert(&CLOUDFRONT_VIEWER_ADDRESS, value.parse().unwrap());
269            assert_eq!(
270                extract_header_cloudfront_viewer_address(&headers).unwrap(),
271                Some(expected.parse().unwrap())
272            );
273        }
274
275        for value in ["198.51.100.10", "198.51.100.10:not-a-port", "not-an-ip:80"] {
276            headers.insert(&CLOUDFRONT_VIEWER_ADDRESS, value.parse().unwrap());
277            let error = extract_header_cloudfront_viewer_address(&headers).unwrap_err();
278            assert!(matches!(error, Error::InvalidHeader { .. }));
279            assert!(!error.to_string().contains(value));
280        }
281
282        headers.clear();
283        headers.append(
284            &CLOUDFRONT_VIEWER_ADDRESS,
285            "198.51.100.10:443".parse().unwrap(),
286        );
287        headers.append(
288            &CLOUDFRONT_VIEWER_ADDRESS,
289            "198.51.100.11:443".parse().unwrap(),
290        );
291        assert!(matches!(
292            extract_header_cloudfront_viewer_address(&headers),
293            Err(Error::DuplicateHeader { .. })
294        ));
295    }
296
297    #[test]
298    fn all_fields_reject_non_text_without_echoing_values() {
299        let mut cases = ordinary_extractors().to_vec();
300        cases.push((
301            CLOUDFRONT_VIEWER_ADDRESS,
302            extract_header_cloudfront_viewer_address,
303        ));
304
305        for (name, extract) in cases {
306            let mut headers = HeaderMap::new();
307            headers.insert(&name, HeaderValue::from_bytes(&[0xff]).unwrap());
308            let error = extract(&headers).unwrap_err();
309            assert!(matches!(error, Error::InvalidHeader { .. }));
310            assert!(!error.to_string().contains("255"));
311        }
312    }
313
314    #[test]
315    fn request_entry_points_delegate_to_header_extractors() {
316        let request = Request::builder()
317            .header(&CF_CONNECTING_IP, "192.0.2.1")
318            .header(&CLOUDFRONT_VIEWER_ADDRESS, "198.51.100.2:443")
319            .header(&FLY_CLIENT_IP, "203.0.113.3")
320            .header(&TRUE_CLIENT_IP, "192.0.2.4")
321            .header(&X_ENVOY_EXTERNAL_ADDRESS, "198.51.100.5")
322            .header(&X_REAL_IP, "203.0.113.6")
323            .body(())
324            .unwrap();
325
326        assert_eq!(
327            extract_request_cf_connecting_ip(&request).unwrap(),
328            Some("192.0.2.1".parse().unwrap())
329        );
330        assert_eq!(
331            extract_request_cloudfront_viewer_address(&request).unwrap(),
332            Some("198.51.100.2".parse().unwrap())
333        );
334        assert_eq!(
335            extract_request_fly_client_ip(&request).unwrap(),
336            Some("203.0.113.3".parse().unwrap())
337        );
338        assert_eq!(
339            extract_request_true_client_ip(&request).unwrap(),
340            Some("192.0.2.4".parse().unwrap())
341        );
342        assert_eq!(
343            extract_request_x_envoy_external_address(&request).unwrap(),
344            Some("198.51.100.5".parse().unwrap())
345        );
346        assert_eq!(
347            extract_request_x_real_ip(&request).unwrap(),
348            Some("203.0.113.6".parse().unwrap())
349        );
350    }
351}