aster_forge_actix_middleware/
client_ip.rs

1//! Actix helpers for trusted-proxy-aware client IP extraction.
2//!
3//! The product-neutral parsing rules live in `aster_forge_utils::net`. This module only adapts
4//! Actix `HeaderMap` values into those helpers so application services do not need to repeat
5//! `X-Forwarded-For` extraction code.
6
7use std::net::IpAddr;
8
9use actix_web::http::header::HeaderMap;
10use ipnet::IpNet;
11
12/// Resolves the client IP from Actix headers, a direct peer IP, and raw trusted proxy entries.
13///
14/// `X-Forwarded-For` is trusted only when `peer` is covered by the trusted proxy list. Invalid
15/// trusted proxy entries are skipped by `aster_forge_utils::net::parse_trusted_proxies`.
16#[must_use]
17pub fn real_ip_from_headers(
18    headers: &HeaderMap,
19    peer: IpAddr,
20    trusted_proxies: &[String],
21) -> IpAddr {
22    let trusted = aster_forge_utils::net::parse_trusted_proxies(trusted_proxies);
23    real_ip_from_trusted_headers(headers, peer, &trusted)
24}
25
26/// Resolves the client IP from Actix headers, a direct peer IP, and parsed trusted proxy entries.
27#[must_use]
28pub fn real_ip_from_trusted_headers(
29    headers: &HeaderMap,
30    peer: IpAddr,
31    trusted: &[IpNet],
32) -> IpAddr {
33    let x_forwarded_for = headers
34        .get("x-forwarded-for")
35        .and_then(|value| value.to_str().ok());
36    aster_forge_utils::net::real_ip_from_forwarded_for(x_forwarded_for, peer, trusted)
37}
38
39#[cfg(test)]
40mod tests {
41    use std::net::IpAddr;
42
43    use actix_web::test as actix_test;
44    use aster_forge_utils::net::{is_trusted_proxy, parse_trusted_proxies};
45
46    use super::{real_ip_from_headers, real_ip_from_trusted_headers};
47
48    #[test]
49    fn parses_trusted_proxies_as_cidr_and_single_ip() {
50        let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string(), "192.168.1.1".to_string()]);
51
52        assert!(is_trusted_proxy("10.0.0.5".parse().unwrap(), &trusted));
53        assert!(is_trusted_proxy("192.168.1.1".parse().unwrap(), &trusted));
54        assert!(!is_trusted_proxy("203.0.113.1".parse().unwrap(), &trusted));
55    }
56
57    #[test]
58    fn real_ip_uses_leftmost_forwarded_value_only_for_trusted_peer() {
59        let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
60        let req = actix_test::TestRequest::default()
61            .insert_header(("X-Forwarded-For", "203.0.113.10, 198.51.100.2"))
62            .to_srv_request();
63
64        assert_eq!(
65            real_ip_from_trusted_headers(
66                req.headers(),
67                "10.0.0.5".parse::<IpAddr>().unwrap(),
68                &trusted,
69            ),
70            "203.0.113.10".parse::<IpAddr>().unwrap()
71        );
72        assert_eq!(
73            real_ip_from_trusted_headers(
74                req.headers(),
75                "198.51.100.2".parse::<IpAddr>().unwrap(),
76                &trusted,
77            ),
78            "198.51.100.2".parse::<IpAddr>().unwrap()
79        );
80    }
81
82    #[test]
83    fn real_ip_falls_back_to_peer_for_invalid_header() {
84        let req = actix_test::TestRequest::default()
85            .insert_header(("X-Forwarded-For", "not-an-ip"))
86            .to_srv_request();
87
88        assert_eq!(
89            real_ip_from_headers(
90                req.headers(),
91                "10.0.0.5".parse::<IpAddr>().unwrap(),
92                &["10.0.0.0/8".to_string()],
93            ),
94            "10.0.0.5".parse::<IpAddr>().unwrap()
95        );
96    }
97
98    #[test]
99    fn real_ip_accepts_forwarded_values_with_ports() {
100        let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
101
102        for (forwarded, expected) in [
103            ("203.0.113.10:54321, 10.0.0.5", "203.0.113.10"),
104            ("[2001:db8::1]:443, 10.0.0.5", "2001:db8::1"),
105        ] {
106            let req = actix_test::TestRequest::default()
107                .insert_header(("X-Forwarded-For", forwarded))
108                .to_srv_request();
109
110            assert_eq!(
111                real_ip_from_trusted_headers(
112                    req.headers(),
113                    "10.0.0.5".parse::<IpAddr>().unwrap(),
114                    &trusted,
115                ),
116                expected.parse::<IpAddr>().unwrap()
117            );
118        }
119    }
120}