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