1use std::net::{Ipv4Addr, Ipv6Addr};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum HostClass {
17 Loopback,
19 LinkLocal,
21 Private,
23 Cgnat,
25 Unspecified,
28 Public,
30}
31
32impl HostClass {
33 pub fn is_internal(self) -> bool {
36 !matches!(self, HostClass::Public)
37 }
38
39 pub fn is_loopback(self) -> bool {
44 matches!(self, HostClass::Loopback)
45 }
46}
47
48pub fn classify_host(host: &str) -> HostClass {
51 let h = host
52 .trim_start_matches('[')
53 .trim_end_matches(']')
54 .trim_end_matches('.')
55 .to_ascii_lowercase();
56 if h == "localhost" || h.ends_with(".localhost") {
57 return HostClass::Loopback;
58 }
59 if let Ok(ip) = h.parse::<Ipv4Addr>() {
60 return classify_ipv4(ip);
61 }
62 if let Ok(ip) = h.parse::<Ipv6Addr>() {
63 if let Some(v4) = ip.to_ipv4_mapped() {
67 return classify_ipv4(v4);
68 }
69 if ip.is_loopback() {
70 return HostClass::Loopback;
71 }
72 if ip.is_unspecified() {
73 return HostClass::Unspecified;
74 }
75 if (ip.segments()[0] & 0xfe00) == 0xfc00 {
76 return HostClass::Private; }
78 if (ip.segments()[0] & 0xffc0) == 0xfe80 {
79 return HostClass::LinkLocal; }
81 return if is_global_ipv6(ip) {
82 HostClass::Public
83 } else {
84 HostClass::Unspecified
85 };
86 }
87 HostClass::Public
88}
89
90fn classify_ipv4(ip: Ipv4Addr) -> HostClass {
91 if ip.is_loopback() {
92 return HostClass::Loopback;
93 }
94 if ip.is_unspecified() || ip.is_broadcast() {
95 return HostClass::Unspecified;
96 }
97 if ip.is_link_local() {
98 return HostClass::LinkLocal;
99 }
100 if ip.is_private() {
101 return HostClass::Private;
102 }
103 let o = ip.octets();
104 if o[0] == 100 && (64..=127).contains(&o[1]) {
105 return HostClass::Cgnat; }
107 if is_global_ipv4(ip) {
108 HostClass::Public
109 } else {
110 HostClass::Unspecified
111 }
112}
113
114fn is_global_ipv4(ip: Ipv4Addr) -> bool {
115 let [a, b, c, d] = ip.octets();
116
117 if [a, b, c, d] == [192, 0, 0, 9] || [a, b, c, d] == [192, 0, 0, 10] {
120 return true;
121 }
122
123 !(a == 0 || a == 10
125 || a == 127
126 || (a == 100 && (64..=127).contains(&b))
127 || (a == 169 && b == 254)
128 || (a == 172 && (16..=31).contains(&b))
129 || (a == 192 && b == 0 && c == 0) || (a == 192 && b == 0 && c == 2) || (a == 192 && b == 88 && c == 99) || (a == 192 && b == 168)
133 || (a == 198 && (b == 18 || b == 19)) || (a == 198 && b == 51 && c == 100) || (a == 203 && b == 0 && c == 113) || a >= 224) }
138
139fn is_global_ipv6(ip: Ipv6Addr) -> bool {
140 let value = u128::from(ip);
141 let globally_reachable_protocol_assignment = ip == Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 1)
142 || ip == Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 2)
143 || ip == Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 3)
144 || in_ipv6_prefix(
145 value,
146 u128::from(Ipv6Addr::new(0x2001, 3, 0, 0, 0, 0, 0, 0)),
147 32,
148 )
149 || in_ipv6_prefix(
150 value,
151 u128::from(Ipv6Addr::new(0x2001, 4, 0x0112, 0, 0, 0, 0, 0)),
152 48,
153 )
154 || in_ipv6_prefix(
155 value,
156 u128::from(Ipv6Addr::new(0x2001, 0x20, 0, 0, 0, 0, 0, 0)),
157 28,
158 )
159 || in_ipv6_prefix(
160 value,
161 u128::from(Ipv6Addr::new(0x2001, 0x30, 0, 0, 0, 0, 0, 0)),
162 28,
163 );
164
165 globally_reachable_protocol_assignment
172 || (in_ipv6_prefix(
173 value,
174 u128::from(Ipv6Addr::new(0x2000, 0, 0, 0, 0, 0, 0, 0)),
175 3,
176 ) && !in_ipv6_prefix(
177 value,
178 u128::from(Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0)),
179 23,
180 ) && !in_ipv6_prefix(
181 value,
182 u128::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)),
183 32,
184 ) && !in_ipv6_prefix(
185 value,
186 u128::from(Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0)),
187 16,
188 ) && !in_ipv6_prefix(
189 value,
190 u128::from(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0)),
191 20,
192 ))
193}
194
195fn in_ipv6_prefix(value: u128, network: u128, prefix_len: u32) -> bool {
196 let mask = u128::MAX << (128 - prefix_len);
197 value & mask == network & mask
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn loopback_forms() {
206 for h in [
207 "localhost",
208 "localhost.",
209 "127.0.0.1",
210 "127.1.2.3",
211 "[::1]",
212 "[::ffff:127.0.0.1]",
213 "app.localhost",
214 ] {
215 assert_eq!(classify_host(h), HostClass::Loopback, "{h}");
216 assert!(classify_host(h).is_internal());
217 assert!(classify_host(h).is_loopback());
218 }
219 }
220
221 #[test]
222 fn internal_but_not_loopback() {
223 for h in [
225 "10.0.0.5",
226 "192.168.1.1",
227 "172.16.0.1",
228 "169.254.169.254",
229 "[::ffff:169.254.169.254]", "[fc00::1]", "[fe80::1]", "100.100.100.200", "0.0.0.0",
234 "0.1.2.3", "192.0.0.1", "192.0.2.1", "198.18.0.1", "198.51.100.1", "203.0.113.1", "224.0.0.1", "240.0.0.1", "[64:ff9b::7f00:1]", "[2001:db8::1]", "[2002:7f00:1::]", "[3fff::1]", "[ff02::1]", ] {
248 assert!(classify_host(h).is_internal(), "{h} should be internal");
249 assert!(!classify_host(h).is_loopback(), "{h} must not be loopback");
250 }
251 }
252
253 #[test]
254 fn public_hosts() {
255 for h in [
256 "example.com",
257 "8.8.8.8",
258 "1.1.1.1",
259 "192.0.0.9",
260 "192.0.0.10",
261 "192.31.196.1",
262 "192.52.193.1",
263 "192.175.48.1",
264 "[2606:4700:4700::1111]",
265 "[2001:4860:4860::8888]",
266 "[2001:1::1]",
267 "[2001:1::2]",
268 "[2001:1::3]",
269 "[2001:3::1]",
270 "[2001:4:112::1]",
271 "[2001:20::1]",
272 "[2001:30::1]",
273 "api.openai.com",
274 ] {
275 assert_eq!(classify_host(h), HostClass::Public, "{h}");
276 assert!(!classify_host(h).is_internal(), "{h}");
277 }
278 }
279
280 #[test]
281 fn special_purpose_literals_are_never_public() {
282 for host in [
283 "0.1.2.3",
284 "10.0.0.1",
285 "198.18.1.1",
286 "224.0.0.1",
287 "64:ff9b::7f00:1",
288 "2001:db8::1",
289 "2002:7f00:1::",
290 ] {
291 assert!(classify_host(host).is_internal(), "{host}");
292 }
293 for host in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"] {
294 assert_eq!(classify_host(host), HostClass::Public, "{host}");
295 }
296 }
297
298 #[test]
299 fn generated_ipv4_special_purpose_ranges_are_never_public() {
300 let ranges = [
304 (Ipv4Addr::new(0, 0, 0, 0), 8),
305 (Ipv4Addr::new(10, 0, 0, 0), 8),
306 (Ipv4Addr::new(100, 64, 0, 0), 10),
307 (Ipv4Addr::new(127, 0, 0, 0), 8),
308 (Ipv4Addr::new(169, 254, 0, 0), 16),
309 (Ipv4Addr::new(172, 16, 0, 0), 12),
310 (Ipv4Addr::new(192, 0, 0, 0), 24),
311 (Ipv4Addr::new(192, 0, 2, 0), 24),
312 (Ipv4Addr::new(192, 88, 99, 0), 24),
313 (Ipv4Addr::new(192, 168, 0, 0), 16),
314 (Ipv4Addr::new(198, 18, 0, 0), 15),
315 (Ipv4Addr::new(198, 51, 100, 0), 24),
316 (Ipv4Addr::new(203, 0, 113, 0), 24),
317 (Ipv4Addr::new(224, 0, 0, 0), 4),
318 (Ipv4Addr::new(240, 0, 0, 0), 4),
319 ];
320 let globally_reachable_exceptions = [
321 u32::from(Ipv4Addr::new(192, 0, 0, 9)),
322 u32::from(Ipv4Addr::new(192, 0, 0, 10)),
323 ];
324
325 let mut state = 0x9e37_79b9_u32;
326 for (network, prefix_len) in ranges {
327 let mask = u32::MAX << (32 - prefix_len);
328 let network = u32::from(network) & mask;
329 for _ in 0..2048 {
330 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
331 let candidate = network | (state & !mask);
332 if globally_reachable_exceptions.contains(&candidate) {
333 continue;
334 }
335 let host = Ipv4Addr::from(candidate).to_string();
336 assert!(
337 classify_host(&host).is_internal(),
338 "special-purpose IPv4 escaped policy: {host}/{prefix_len}"
339 );
340 }
341 }
342 }
343
344 #[test]
345 fn generated_ipv6_special_purpose_ranges_are_never_public() {
346 let ranges = [
351 (Ipv6Addr::new(0x0064, 0xff9b, 0, 0, 0, 0, 0, 0), 96),
352 (Ipv6Addr::new(0x0064, 0xff9b, 1, 0, 0, 0, 0, 0), 48),
353 (Ipv6Addr::new(0x0100, 0, 0, 0, 0, 0, 0, 0), 64),
354 (Ipv6Addr::new(0x0100, 0, 0, 1, 0, 0, 0, 0), 64),
355 (Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0), 32),
356 (Ipv6Addr::new(0x2001, 2, 0, 0, 0, 0, 0, 0), 48),
357 (Ipv6Addr::new(0x2001, 0x10, 0, 0, 0, 0, 0, 0), 28),
358 (Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 32),
359 (Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0), 16),
360 (Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0), 20),
361 (Ipv6Addr::new(0x5f00, 0, 0, 0, 0, 0, 0, 0), 16),
362 (Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7),
363 (Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), 10),
364 (Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 8),
365 ];
366
367 let mut state = 0x6a09_e667_f3bc_c909_bb67_ae85_84ca_a73b_u128;
368 for (network, prefix_len) in ranges {
369 let mask = u128::MAX << (128 - prefix_len);
370 let network = u128::from(network) & mask;
371 for _ in 0..2048 {
372 state = state
373 .wrapping_mul(0x2360_ed05_1fc6_5da4_4385_df64_9fcc_f645)
374 .wrapping_add(0x9e37_79b9_7f4a_7c15_6a09_e667_f3bc_c909);
375 let host = Ipv6Addr::from(network | (state & !mask)).to_string();
376 assert!(
377 classify_host(&host).is_internal(),
378 "special-purpose IPv6 escaped policy: {host}/{prefix_len}"
379 );
380 }
381 }
382 }
383}