1use std::net::{IpAddr, SocketAddr};
9
10use ipnet::IpNet;
11
12#[must_use]
14pub fn is_loopback_host(host: &str) -> bool {
15 let trimmed = host.trim();
16 let host = trimmed
17 .strip_prefix('[')
18 .and_then(|value| value.strip_suffix(']'))
19 .unwrap_or(trimmed);
20
21 host.eq_ignore_ascii_case("localhost")
22 || host
23 .parse::<std::net::IpAddr>()
24 .is_ok_and(|ip| ip.is_loopback())
25}
26
27#[must_use]
33pub fn parse_trusted_proxies(trusted_proxies: &[String]) -> Vec<IpNet> {
34 trusted_proxies
35 .iter()
36 .filter_map(|entry| {
37 entry
38 .parse::<IpNet>()
39 .or_else(|_| entry.parse::<IpAddr>().map(IpNet::from))
40 .map_err(|error| tracing::warn!("invalid trusted_proxy entry '{entry}': {error}"))
41 .ok()
42 })
43 .collect()
44}
45
46#[must_use]
48pub fn is_trusted_proxy(ip: IpAddr, trusted: &[IpNet]) -> bool {
49 trusted.iter().any(|net| net.contains(&ip))
50}
51
52pub fn real_ip_from_forwarded_for(
58 x_forwarded_for: Option<&str>,
59 peer: IpAddr,
60 trusted: &[IpNet],
61) -> IpAddr {
62 if !trusted.is_empty() && is_trusted_proxy(peer, trusted) {
63 let ip = x_forwarded_for
64 .and_then(|value| value.split(',').next())
65 .and_then(parse_forwarded_ip);
66 if let Some(ip) = ip {
67 return ip;
68 }
69 }
70 peer
71}
72
73fn parse_forwarded_ip(value: &str) -> Option<IpAddr> {
74 let value = value.trim();
75 value
76 .parse::<IpAddr>()
77 .or_else(|_| value.parse::<SocketAddr>().map(|address| address.ip()))
78 .ok()
79}
80
81#[cfg(test)]
82mod tests {
83 use super::{
84 is_loopback_host, is_trusted_proxy, parse_trusted_proxies, real_ip_from_forwarded_for,
85 };
86 use std::net::IpAddr;
87
88 #[test]
89 fn detects_loopback_hosts() {
90 assert!(is_loopback_host("localhost"));
91 assert!(is_loopback_host("LOCALHOST"));
92 assert!(is_loopback_host("127.0.0.1"));
93 assert!(is_loopback_host("127.0.0.2"));
94 assert!(is_loopback_host("::1"));
95 assert!(is_loopback_host("[::1]"));
96
97 assert!(!is_loopback_host("example.com"));
98 assert!(!is_loopback_host("0.0.0.0"));
99 assert!(!is_loopback_host("192.168.1.10"));
100 }
101
102 #[test]
103 fn parse_trusted_proxies_accepts_cidr_and_single_ip() {
104 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string(), "192.168.1.1".to_string()]);
105
106 assert!(is_trusted_proxy("10.0.0.5".parse().unwrap(), &trusted));
107 assert!(is_trusted_proxy("192.168.1.1".parse().unwrap(), &trusted));
108 assert!(!is_trusted_proxy("203.0.113.1".parse().unwrap(), &trusted));
109 }
110
111 #[test]
112 fn parse_trusted_proxies_skips_invalid_entries() {
113 let trusted = parse_trusted_proxies(&["not-a-proxy".to_string(), "10.0.0.0/8".to_string()]);
114
115 assert_eq!(trusted.len(), 1);
116 assert!(is_trusted_proxy("10.1.2.3".parse().unwrap(), &trusted));
117 }
118
119 #[test]
120 fn real_ip_uses_leftmost_xff_only_for_trusted_peer() {
121 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
122
123 assert_eq!(
124 real_ip_from_forwarded_for(
125 Some("203.0.113.10, 198.51.100.2"),
126 "10.0.0.5".parse::<IpAddr>().unwrap(),
127 &trusted,
128 ),
129 "203.0.113.10".parse::<IpAddr>().unwrap()
130 );
131 assert_eq!(
132 real_ip_from_forwarded_for(
133 Some("203.0.113.10"),
134 "198.51.100.2".parse::<IpAddr>().unwrap(),
135 &trusted,
136 ),
137 "198.51.100.2".parse::<IpAddr>().unwrap()
138 );
139 }
140
141 #[test]
142 fn real_ip_accepts_forwarded_socket_address_forms() {
143 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
144 let peer = "10.0.0.5".parse::<IpAddr>().unwrap();
145
146 assert_eq!(
147 real_ip_from_forwarded_for(Some("203.0.113.10:54321, 10.0.0.5"), peer, &trusted),
148 "203.0.113.10".parse::<IpAddr>().unwrap()
149 );
150 assert_eq!(
151 real_ip_from_forwarded_for(Some("[2001:db8::1]:443, 10.0.0.5"), peer, &trusted),
152 "2001:db8::1".parse::<IpAddr>().unwrap()
153 );
154 }
155
156 #[test]
157 fn real_ip_falls_back_to_peer_for_invalid_or_missing_xff() {
158 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
159 let peer = "10.0.0.5".parse::<IpAddr>().unwrap();
160
161 assert_eq!(
162 real_ip_from_forwarded_for(Some("not-an-ip"), peer, &trusted),
163 peer
164 );
165 assert_eq!(real_ip_from_forwarded_for(None, peer, &trusted), peer);
166 }
167}