Skip to main content

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`.
16pub fn real_ip_from_headers(
17    headers: &HeaderMap,
18    peer: IpAddr,
19    trusted_proxies: &[String],
20) -> IpAddr {
21    let trusted = aster_forge_utils::net::parse_trusted_proxies(trusted_proxies);
22    real_ip_from_trusted_headers(headers, peer, &trusted)
23}
24
25/// Resolves the client IP from Actix headers, a direct peer IP, and parsed trusted proxy entries.
26pub fn real_ip_from_trusted_headers(
27    headers: &HeaderMap,
28    peer: IpAddr,
29    trusted: &[IpNet],
30) -> IpAddr {
31    let x_forwarded_for = headers
32        .get("x-forwarded-for")
33        .and_then(|value| value.to_str().ok());
34    aster_forge_utils::net::real_ip_from_forwarded_for(x_forwarded_for, peer, trusted)
35}
36
37#[cfg(test)]
38mod tests {
39    use std::net::IpAddr;
40
41    use actix_web::test as actix_test;
42    use aster_forge_utils::net::{is_trusted_proxy, parse_trusted_proxies};
43
44    use super::{real_ip_from_headers, real_ip_from_trusted_headers};
45
46    #[test]
47    fn parses_trusted_proxies_as_cidr_and_single_ip() {
48        let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string(), "192.168.1.1".to_string()]);
49
50        assert!(is_trusted_proxy("10.0.0.5".parse().unwrap(), &trusted));
51        assert!(is_trusted_proxy("192.168.1.1".parse().unwrap(), &trusted));
52        assert!(!is_trusted_proxy("203.0.113.1".parse().unwrap(), &trusted));
53    }
54
55    #[test]
56    fn real_ip_uses_leftmost_forwarded_value_only_for_trusted_peer() {
57        let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
58        let req = actix_test::TestRequest::default()
59            .insert_header(("X-Forwarded-For", "203.0.113.10, 198.51.100.2"))
60            .to_srv_request();
61
62        assert_eq!(
63            real_ip_from_trusted_headers(
64                req.headers(),
65                "10.0.0.5".parse::<IpAddr>().unwrap(),
66                &trusted,
67            ),
68            "203.0.113.10".parse::<IpAddr>().unwrap()
69        );
70        assert_eq!(
71            real_ip_from_trusted_headers(
72                req.headers(),
73                "198.51.100.2".parse::<IpAddr>().unwrap(),
74                &trusted,
75            ),
76            "198.51.100.2".parse::<IpAddr>().unwrap()
77        );
78    }
79
80    #[test]
81    fn real_ip_falls_back_to_peer_for_invalid_header() {
82        let req = actix_test::TestRequest::default()
83            .insert_header(("X-Forwarded-For", "not-an-ip"))
84            .to_srv_request();
85
86        assert_eq!(
87            real_ip_from_headers(
88                req.headers(),
89                "10.0.0.5".parse::<IpAddr>().unwrap(),
90                &["10.0.0.0/8".to_string()],
91            ),
92            "10.0.0.5".parse::<IpAddr>().unwrap()
93        );
94    }
95
96    #[test]
97    fn real_ip_accepts_forwarded_values_with_ports() {
98        let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
99
100        for (forwarded, expected) in [
101            ("203.0.113.10:54321, 10.0.0.5", "203.0.113.10"),
102            ("[2001:db8::1]:443, 10.0.0.5", "2001:db8::1"),
103        ] {
104            let req = actix_test::TestRequest::default()
105                .insert_header(("X-Forwarded-For", forwarded))
106                .to_srv_request();
107
108            assert_eq!(
109                real_ip_from_trusted_headers(
110                    req.headers(),
111                    "10.0.0.5".parse::<IpAddr>().unwrap(),
112                    &trusted,
113                ),
114                expected.parse::<IpAddr>().unwrap()
115            );
116        }
117    }
118}