1use std::net::IpAddr;
22use std::str::FromStr;
23
24use crate::NetworkConfig;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum IpSource {
29 DirectPeer,
31 TrustedHeader,
33 FallbackMissingHeader,
35 FallbackMalformed,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct ResolvedClientIp {
43 pub ip: IpAddr,
44 pub source: IpSource,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum Cidr {
50 V4(u32, u8),
51 V6(u128, u8),
52}
53
54impl Cidr {
55 fn parse(s: &str) -> Option<Cidr> {
59 let (addr_part, prefix_part) = match s.trim().split_once('/') {
60 Some((a, p)) => (a.trim(), Some(p.trim())),
61 None => (s.trim(), None),
62 };
63 match IpAddr::from_str(addr_part).ok()? {
64 IpAddr::V4(a) => {
65 let prefix = match prefix_part {
66 Some(p) => p.parse().ok().filter(|&p| p <= 32)?,
67 None => 32,
68 };
69 Some(Cidr::V4(u32::from(a), prefix))
70 }
71 IpAddr::V6(a) => {
72 let prefix = match prefix_part {
73 Some(p) => p.parse().ok().filter(|&p| p <= 128)?,
74 None => 128,
75 };
76 Some(Cidr::V6(u128::from(a), prefix))
77 }
78 }
79 }
80
81 fn contains(&self, ip: IpAddr) -> bool {
83 match (self, ip) {
84 (Cidr::V4(net, prefix), IpAddr::V4(addr)) => mask_eq_u32(*net, u32::from(addr), *prefix),
85 (Cidr::V6(net, prefix), IpAddr::V6(addr)) => {
86 mask_eq_u128(*net, u128::from(addr), *prefix)
87 }
88 _ => false,
89 }
90 }
91}
92
93fn mask_eq_u32(a: u32, b: u32, prefix: u8) -> bool {
97 let mask = match prefix {
98 0 => 0,
99 32 => u32::MAX,
100 p => u32::MAX << (32 - p),
101 };
102 (a & mask) == (b & mask)
103}
104
105fn mask_eq_u128(a: u128, b: u128, prefix: u8) -> bool {
107 let mask = match prefix {
108 0 => 0,
109 128 => u128::MAX,
110 p => u128::MAX << (128 - p),
111 };
112 (a & mask) == (b & mask)
113}
114
115pub fn is_valid_cidr(s: &str) -> bool {
119 Cidr::parse(s).is_some()
120}
121
122pub struct ClientIpResolver {
124 trusted: Vec<Cidr>,
125 header: String, hops: usize,
127}
128
129impl ClientIpResolver {
130 pub fn from_config(cfg: &NetworkConfig) -> Self {
133 let trusted = cfg.trusted_proxies.iter().filter_map(|s| Cidr::parse(s)).collect();
134 Self {
135 trusted,
136 header: cfg.client_ip_header.to_ascii_lowercase(),
137 hops: cfg.trusted_hops,
138 }
139 }
140
141 pub fn trusted_count(&self) -> usize {
143 self.trusted.len()
144 }
145
146 pub fn resolve(&self, peer: IpAddr, headers: &[(String, String)]) -> ResolvedClientIp {
148 if !self.trusted.iter().any(|c| c.contains(peer)) {
150 return ResolvedClientIp { ip: peer, source: IpSource::DirectPeer };
151 }
152
153 let Some(value) = self.header_value(headers) else {
155 return ResolvedClientIp { ip: peer, source: IpSource::FallbackMissingHeader };
156 };
157
158 let chain: Vec<&str> =
161 value.split(',').map(str::trim).filter(|s| !s.is_empty()).collect();
162
163 if self.hops == 0 || self.hops > chain.len() {
167 return ResolvedClientIp { ip: peer, source: IpSource::FallbackMalformed };
168 }
169
170 let idx = chain.len() - self.hops;
171 match IpAddr::from_str(chain[idx]) {
172 Ok(ip) => ResolvedClientIp { ip, source: IpSource::TrustedHeader },
173 Err(_) => ResolvedClientIp { ip: peer, source: IpSource::FallbackMalformed },
175 }
176 }
177
178 fn header_value<'a>(&self, headers: &'a [(String, String)]) -> Option<&'a str> {
180 headers
181 .iter()
182 .find(|(k, _)| k.eq_ignore_ascii_case(&self.header))
183 .map(|(_, v)| v.as_str())
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn resolver(trusted: &[&str], hops: usize) -> ClientIpResolver {
192 ClientIpResolver::from_config(&NetworkConfig {
193 trusted_proxies: trusted.iter().map(|s| s.to_string()).collect(),
194 client_ip_header: "x-forwarded-for".to_string(),
195 trusted_hops: hops,
196 })
197 }
198
199 fn ip(s: &str) -> IpAddr {
200 s.parse().unwrap()
201 }
202
203 fn xff(v: &str) -> Vec<(String, String)> {
204 vec![("x-forwarded-for".to_string(), v.to_string())]
205 }
206
207 #[test]
210 fn cidr_v4_prefixes() {
211 assert!(Cidr::parse("10.0.0.0/8").unwrap().contains(ip("10.255.1.2")));
212 assert!(!Cidr::parse("10.0.0.0/8").unwrap().contains(ip("11.0.0.1")));
213 assert!(Cidr::parse("127.0.0.1").unwrap().contains(ip("127.0.0.1")));
215 assert!(!Cidr::parse("127.0.0.1").unwrap().contains(ip("127.0.0.2")));
216 assert!(Cidr::parse("0.0.0.0/0").unwrap().contains(ip("203.0.113.7")));
218 }
219
220 #[test]
221 fn cidr_v6_prefixes() {
222 assert!(Cidr::parse("::1").unwrap().contains(ip("::1")));
223 assert!(!Cidr::parse("::1").unwrap().contains(ip("::2")));
224 assert!(Cidr::parse("fd00::/8").unwrap().contains(ip("fd12:3456::1")));
225 assert!(!Cidr::parse("fd00::/8").unwrap().contains(ip("fe80::1")));
226 assert!(Cidr::parse("::/0").unwrap().contains(ip("2001:db8::1")));
228 }
229
230 #[test]
231 fn cidr_family_mismatch_never_matches() {
232 assert!(!Cidr::parse("0.0.0.0/0").unwrap().contains(ip("::1")));
233 assert!(!Cidr::parse("::/0").unwrap().contains(ip("1.2.3.4")));
234 }
235
236 #[test]
237 fn invalid_cidr_is_skipped() {
238 let r = resolver(&["not-an-ip", "10.0.0.0/8", "10.0.0.0/99"], 1);
239 assert_eq!(r.trusted_count(), 1);
240 }
241
242 #[test]
245 fn untrusted_peer_uses_peer_ignoring_xff() {
246 let r = resolver(&["10.0.0.0/8"], 1);
247 let got = r.resolve(ip("203.0.113.9"), &xff("1.2.3.4"));
248 assert_eq!(got.ip, ip("203.0.113.9"));
249 assert_eq!(got.source, IpSource::DirectPeer);
250 }
251
252 #[test]
253 fn trusted_peer_takes_hop_from_right_not_leftmost() {
254 let r = resolver(&["10.0.0.0/8"], 1);
255 let got = r.resolve(ip("10.0.0.5"), &xff("attacker, 198.51.100.7"));
257 assert_eq!(got.ip, ip("198.51.100.7"));
258 assert_eq!(got.source, IpSource::TrustedHeader);
259 }
260
261 #[test]
262 fn empty_trusted_proxies_always_uses_peer() {
263 let r = resolver(&[], 1);
264 let got = r.resolve(ip("10.0.0.5"), &xff("1.2.3.4"));
265 assert_eq!(got.ip, ip("10.0.0.5"));
266 assert_eq!(got.source, IpSource::DirectPeer);
267 }
268
269 #[test]
270 fn malformed_xff_behind_trusted_falls_back_to_peer() {
271 let r = resolver(&["10.0.0.0/8"], 1);
272 let got = r.resolve(ip("10.0.0.5"), &xff("not-an-ip"));
273 assert_eq!(got.ip, ip("10.0.0.5"));
274 assert_eq!(got.source, IpSource::FallbackMalformed);
275 }
276
277 #[test]
278 fn missing_header_behind_trusted_falls_back_to_peer() {
279 let r = resolver(&["10.0.0.0/8"], 1);
280 let got = r.resolve(ip("10.0.0.5"), &[]);
281 assert_eq!(got.ip, ip("10.0.0.5"));
282 assert_eq!(got.source, IpSource::FallbackMissingHeader);
283 }
284
285 #[test]
286 fn chain_shorter_than_hops_never_picks_leftmost() {
287 let r = resolver(&["10.0.0.0/8"], 2);
290 let got = r.resolve(ip("10.0.0.5"), &xff("attacker"));
291 assert_eq!(got.ip, ip("10.0.0.5"));
292 assert_eq!(got.source, IpSource::FallbackMalformed);
293 }
294
295 #[test]
296 fn ipv6_trusted_peer_reads_xff() {
297 let r = resolver(&["::1"], 1);
298 let got = r.resolve(ip("::1"), &xff("198.51.100.7"));
299 assert_eq!(got.ip, ip("198.51.100.7"));
300 assert_eq!(got.source, IpSource::TrustedHeader);
301 }
302
303 #[test]
304 fn two_hops_selects_correct_position() {
305 let r = resolver(&["10.0.0.0/8"], 2);
306 let got = r.resolve(ip("10.0.0.5"), &xff("9.9.9.9, 198.51.100.7, 10.0.0.9"));
308 assert_eq!(got.ip, ip("198.51.100.7"));
309 assert_eq!(got.source, IpSource::TrustedHeader);
310 }
311}