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