1use super::IpAddressConfig;
2use std::{collections::HashMap, net::IpAddr};
3
4impl IpAddressConfig {
5 #[must_use]
11 pub fn resolve_ip(&self, headers: &HashMap<String, String>) -> Option<String> {
12 if self.disable_ip_tracking {
13 return None;
14 }
15 let proxies = self
16 .trusted_proxies
17 .iter()
18 .filter_map(|value| Network::parse(value))
19 .collect::<Vec<_>>();
20 self.headers
21 .iter()
22 .filter_map(|name| {
23 headers
24 .iter()
25 .find(|(key, _)| key.eq_ignore_ascii_case(name))
26 .and_then(|(_, value)| self.resolve_header(value, &proxies))
27 })
28 .next()
29 .or_else(|| self.localhost_fallback.then(|| "127.0.0.1".to_owned()))
30 }
31
32 fn resolve_header(&self, value: &str, proxies: &[Network]) -> Option<String> {
33 let hops = value
34 .split(',')
35 .map(|hop| {
36 hop.trim_matches(|character: char| {
37 (character.is_whitespace() && character != '\u{85}') || character == '\u{feff}'
38 })
39 })
40 .filter(|hop| !hop.is_empty())
41 .collect::<Vec<_>>();
42 if proxies.is_empty() {
43 return if hops.len() == 1 {
44 Address::parse(hops.first()?).map(|ip| ip.normalize(self.ipv6_subnet))
45 } else {
46 None
47 };
48 }
49 for hop in hops.iter().rev() {
50 let ip = Address::parse(hop)?;
51 if !proxies.iter().any(|proxy| proxy.contains(&ip.bytes)) {
52 return Some(ip.normalize(self.ipv6_subnet));
53 }
54 }
55 None
56 }
57}
58
59struct Address {
60 bytes: Vec<u8>,
61 ipv6_groups: Option<Vec<String>>,
62}
63
64impl Address {
65 fn parse(value: &str) -> Option<Self> {
66 match value.parse::<IpAddr>().ok()? {
67 IpAddr::V4(ip) => Some(Self {
68 bytes: ip.octets().to_vec(),
69 ipv6_groups: None,
70 }),
71 IpAddr::V6(ip) => {
72 if let Some(mapped) = ip.to_ipv4_mapped().filter(|_| {
76 value.contains('.') || value.split(':').any(|group| group == "ffff")
77 }) {
78 return Some(Self {
79 bytes: mapped.octets().to_vec(),
80 ipv6_groups: None,
81 });
82 }
83 let groups = expand_ipv6(value);
84 let bytes = groups
85 .iter()
86 .flat_map(|group| segment(group).to_be_bytes())
87 .collect();
88 Some(Self {
89 bytes,
90 ipv6_groups: Some(groups),
91 })
92 }
93 }
94 }
95
96 fn normalize(self, prefix: f64) -> String {
97 let Some(groups) = self.ipv6_groups else {
98 return self
99 .bytes
100 .iter()
101 .map(u8::to_string)
102 .collect::<Vec<_>>()
103 .join(".");
104 };
105 if prefix.is_nan() || prefix >= 128.0 {
106 return groups.join(":").to_lowercase();
107 }
108 let mut bits = prefix.floor().max(0.0);
109 groups
110 .into_iter()
111 .map(|group| {
112 let retained = (0..16_u16)
113 .filter(|bit| f64::from(*bit) < bits)
114 .fold(0_u16, |mask, bit| mask | (1 << (15 - bit)));
115 bits -= 16.0;
116 if retained == u16::MAX {
117 group.to_lowercase()
118 } else {
119 format!("{:04x}", segment(&group) & retained)
120 }
121 })
122 .collect::<Vec<_>>()
123 .join(":")
124 }
125}
126
127fn expand_ipv6(value: &str) -> Vec<String> {
128 let raw = if let Some((left, right)) = value.split_once("::") {
129 let mut left = left
130 .split(':')
131 .filter(|group| !group.is_empty())
132 .collect::<Vec<_>>();
133 let right = right
134 .split(':')
135 .filter(|group| !group.is_empty())
136 .collect::<Vec<_>>();
137 left.extend(std::iter::repeat_n("0", 8 - left.len() - right.len()));
138 left.extend(right);
139 left
140 } else {
141 value.split(':').collect()
142 };
143 raw.into_iter()
144 .map(|group| format!("{group:0>4}"))
145 .collect()
146}
147
148fn segment(group: &str) -> u16 {
149 let prefix = group.split_once('.').map_or(group, |(first, _)| first);
150 u16::from_str_radix(prefix, 16).unwrap_or_default()
151}
152
153struct Network {
154 bytes: Vec<u8>,
155 prefix: u8,
156}
157
158impl Network {
159 fn parse(value: &str) -> Option<Self> {
160 let (ip, prefix) = value
161 .rsplit_once('/')
162 .map_or((value, None), |(ip, prefix)| (ip, Some(prefix)));
163 let address = Address::parse(ip)?;
164 let maximum = u8::try_from(address.bytes.len() * 8).ok()?;
165 let prefix = if let Some(prefix) = prefix {
166 if prefix.is_empty() || !prefix.bytes().all(|byte| byte.is_ascii_digit()) {
167 return None;
168 }
169 prefix
170 .parse::<u8>()
171 .ok()
172 .filter(|prefix| *prefix <= maximum)?
173 } else {
174 maximum
175 };
176 Some(Self {
177 bytes: address.bytes,
178 prefix,
179 })
180 }
181
182 fn contains(&self, bytes: &[u8]) -> bool {
183 if bytes.len() != self.bytes.len() {
184 return false;
185 }
186 let mut remaining = self.prefix;
187 for (ip, network) in bytes.iter().zip(&self.bytes) {
188 if remaining == 0 {
189 break;
190 }
191 let mask = u8::MAX << (8 - remaining.min(8));
192 if ip & mask != network & mask {
193 return false;
194 }
195 remaining = remaining.saturating_sub(8);
196 }
197 true
198 }
199}
200
201#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn forwarded_chains_select_the_first_untrusted_hop_from_the_right() {
208 for (proxies, chain, expected) in [
209 (vec![], "198.51.100.7", Some("198.51.100.7")),
210 (vec![], "198.51.100.7, 10.0.0.1", None),
211 (vec!["invalid"], "198.51.100.7, 10.0.0.1", None),
212 (
213 vec!["10.0.0.0/8"],
214 "203.0.113.99, 198.51.100.7, 10.0.0.1",
215 Some("198.51.100.7"),
216 ),
217 (vec!["10.0.0.0/8"], "10.0.0.2, 10.0.0.1", None),
218 (
219 vec!["10.0.0.0/8"],
220 "198.51.100.7, malformed, 10.0.0.1",
221 None,
222 ),
223 ] {
224 let config = IpAddressConfig {
225 trusted_proxies: proxies.into_iter().map(str::to_owned).collect(),
226 localhost_fallback: false,
227 ..Default::default()
228 };
229 let headers = HashMap::from([("X-Forwarded-For".into(), chain.into())]);
230 assert_eq!(config.resolve_ip(&headers).as_deref(), expected, "{chain}");
231 }
232 }
233
234 #[test]
235 fn malformed_header_falls_back_but_tracking_opt_out_never_does() {
236 let mut config = IpAddressConfig {
237 headers: vec!["x-forwarded-for".into(), "x-real-ip".into()],
238 trusted_proxies: vec!["10.0.0.0/8".into()],
239 localhost_fallback: true,
240 ..Default::default()
241 };
242 let headers = HashMap::from([
243 (
244 "x-forwarded-for".into(),
245 "198.51.100.7, bad-hop, 10.0.0.1".into(),
246 ),
247 ("x-real-ip".into(), "203.0.113.8".into()),
248 ]);
249 assert_eq!(config.resolve_ip(&headers).as_deref(), Some("203.0.113.8"));
250 assert_eq!(
251 config.resolve_ip(&HashMap::new()).as_deref(),
252 Some("127.0.0.1")
253 );
254 config.disable_ip_tracking = true;
255 assert_eq!(config.resolve_ip(&headers), None);
256 assert_eq!(config.resolve_ip(&HashMap::new()), None);
257 }
258
259 #[test]
260 fn ipv6_proxy_trust_is_checked_before_grouping_the_client_address() {
261 let config = IpAddressConfig {
262 trusted_proxies: vec!["2001:db8:abcd:12::1/128".into()],
263 ipv6_subnet: 64.0,
264 localhost_fallback: false,
265 ..Default::default()
266 };
267 let headers = HashMap::from([(
268 "x-forwarded-for".into(),
269 "203.0.113.99, 2001:db8:abcd:12::2, 2001:db8:abcd:12::1".into(),
270 )]);
271 assert_eq!(
272 config.resolve_ip(&headers).as_deref(),
273 Some("2001:0db8:abcd:0012:0000:0000:0000:0000")
274 );
275 }
276}
277