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