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
52#[must_use]
62pub fn real_ip_from_forwarded_for(
63 x_forwarded_for: Option<&str>,
64 peer: IpAddr,
65 trusted: &[IpNet],
66) -> IpAddr {
67 if trusted.is_empty() || !is_trusted_proxy(peer, trusted) {
68 return peer;
69 }
70 let Some(forwarded) = x_forwarded_for else {
71 return peer;
72 };
73 for entry in forwarded.split(',').rev() {
74 let Some(ip) = parse_forwarded_ip(entry) else {
75 return peer;
78 };
79 if !is_trusted_proxy(ip, trusted) {
80 return ip;
81 }
82 }
83 peer
86}
87
88fn parse_forwarded_ip(value: &str) -> Option<IpAddr> {
89 let value = value.trim();
90 value
91 .parse::<IpAddr>()
92 .or_else(|_| value.parse::<SocketAddr>().map(|address| address.ip()))
93 .ok()
94}
95
96#[cfg(test)]
97mod tests {
98 use super::{
99 is_loopback_host, is_trusted_proxy, parse_trusted_proxies, real_ip_from_forwarded_for,
100 };
101 use std::net::IpAddr;
102
103 #[test]
104 fn detects_loopback_hosts() {
105 assert!(is_loopback_host("localhost"));
106 assert!(is_loopback_host("LOCALHOST"));
107 assert!(is_loopback_host("127.0.0.1"));
108 assert!(is_loopback_host("127.0.0.2"));
109 assert!(is_loopback_host("::1"));
110 assert!(is_loopback_host("[::1]"));
111
112 assert!(!is_loopback_host("example.com"));
113 assert!(!is_loopback_host("0.0.0.0"));
114 assert!(!is_loopback_host("192.168.1.10"));
115 }
116
117 #[test]
118 fn parse_trusted_proxies_accepts_cidr_and_single_ip() {
119 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string(), "192.168.1.1".to_string()]);
120
121 assert!(is_trusted_proxy("10.0.0.5".parse().unwrap(), &trusted));
122 assert!(is_trusted_proxy("192.168.1.1".parse().unwrap(), &trusted));
123 assert!(!is_trusted_proxy("203.0.113.1".parse().unwrap(), &trusted));
124 }
125
126 #[test]
127 fn parse_trusted_proxies_skips_invalid_entries() {
128 let trusted = parse_trusted_proxies(&["not-a-proxy".to_string(), "10.0.0.0/8".to_string()]);
129
130 assert_eq!(trusted.len(), 1);
131 assert!(is_trusted_proxy("10.1.2.3".parse().unwrap(), &trusted));
132 }
133
134 #[test]
135 fn real_ip_walks_the_chain_right_to_left_and_stops_at_the_first_untrusted_hop() {
136 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
137 let peer = "10.0.0.5".parse::<IpAddr>().unwrap();
138
139 assert_eq!(
143 real_ip_from_forwarded_for(Some("203.0.113.10, 198.51.100.2"), peer, &trusted,),
144 "198.51.100.2".parse::<IpAddr>().unwrap()
145 );
146
147 assert_eq!(
149 real_ip_from_forwarded_for(Some("203.0.113.10"), peer, &trusted,),
150 "203.0.113.10".parse::<IpAddr>().unwrap()
151 );
152
153 assert_eq!(
155 real_ip_from_forwarded_for(Some("6.6.6.6, 203.0.113.10"), peer, &trusted,),
156 "203.0.113.10".parse::<IpAddr>().unwrap()
157 );
158 }
159
160 #[test]
161 fn real_ip_accepts_forwarded_socket_address_forms() {
162 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
163 let peer = "10.0.0.5".parse::<IpAddr>().unwrap();
164
165 assert_eq!(
166 real_ip_from_forwarded_for(Some("203.0.113.10:54321, 10.0.0.5"), peer, &trusted),
167 "203.0.113.10".parse::<IpAddr>().unwrap()
168 );
169 assert_eq!(
170 real_ip_from_forwarded_for(Some("[2001:db8::1]:443, 10.0.0.5"), peer, &trusted),
171 "2001:db8::1".parse::<IpAddr>().unwrap()
172 );
173 }
174
175 #[test]
176 fn real_ip_stops_at_malformed_entries_and_falls_back_to_peer() {
177 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
178 let peer = "10.0.0.5".parse::<IpAddr>().unwrap();
179
180 assert_eq!(
183 real_ip_from_forwarded_for(Some("203.0.113.10, not-an-ip, 10.0.0.9"), peer, &trusted,),
184 peer
185 );
186 assert_eq!(real_ip_from_forwarded_for(None, peer, &trusted), peer);
187 assert_eq!(real_ip_from_forwarded_for(Some(""), peer, &trusted), peer);
188 }
189
190 #[test]
191 fn real_ip_falls_back_to_peer_for_invalid_or_missing_xff() {
192 let trusted = parse_trusted_proxies(&["10.0.0.0/8".to_string()]);
193 let peer = "10.0.0.5".parse::<IpAddr>().unwrap();
194
195 assert_eq!(
196 real_ip_from_forwarded_for(Some("not-an-ip"), peer, &trusted),
197 peer
198 );
199 assert_eq!(real_ip_from_forwarded_for(None, peer, &trusted), peer);
200 }
201}