Skip to main content

fluidattacks_core_git/
local_address.rs

1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3use ipnet::{Ipv4Net, Ipv6Net};
4
5/// Why an address is not one to reach from outside.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
7pub enum LocalAddress {
8    #[error("Requests to loopback addresses are not allowed")]
9    Loopback,
10    #[error("Requests to the local network are not allowed")]
11    LocalNetwork,
12}
13
14const LOOPBACK_V4: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(127, 0, 0, 0), 8);
15
16// IANA's special-purpose ranges, as Python's ipaddress reads them; std's is_global, which covers them, is nightly-only.
17const LOCAL_NETWORK_V4: [Ipv4Net; 14] = [
18    Ipv4Net::new_assert(Ipv4Addr::UNSPECIFIED, 8),
19    Ipv4Net::new_assert(Ipv4Addr::new(10, 0, 0, 0), 8),
20    Ipv4Net::new_assert(Ipv4Addr::new(100, 64, 0, 0), 10),
21    Ipv4Net::new_assert(Ipv4Addr::new(169, 254, 0, 0), 16),
22    Ipv4Net::new_assert(Ipv4Addr::new(172, 16, 0, 0), 12),
23    Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 0), 24),
24    Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 170), 31),
25    Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 2, 0), 24),
26    Ipv4Net::new_assert(Ipv4Addr::new(192, 168, 0, 0), 16),
27    Ipv4Net::new_assert(Ipv4Addr::new(198, 18, 0, 0), 15),
28    Ipv4Net::new_assert(Ipv4Addr::new(198, 51, 100, 0), 24),
29    Ipv4Net::new_assert(Ipv4Addr::new(203, 0, 113, 0), 24),
30    Ipv4Net::new_assert(Ipv4Addr::new(240, 0, 0, 0), 4),
31    Ipv4Net::new_assert(Ipv4Addr::BROADCAST, 32),
32];
33
34const GLOBAL_V4: [Ipv4Addr; 2] = [Ipv4Addr::new(192, 0, 0, 9), Ipv4Addr::new(192, 0, 0, 10)];
35
36const LOCAL_NETWORK_V6: [Ipv6Net; 9] = [
37    Ipv6Net::new_assert(Ipv6Addr::UNSPECIFIED, 128),
38    Ipv6Net::new_assert(Ipv6Addr::new(0x64, 0xff9b, 1, 0, 0, 0, 0, 0), 48),
39    Ipv6Net::new_assert(Ipv6Addr::new(0x100, 0, 0, 0, 0, 0, 0, 0), 64),
40    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0), 23),
41    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32),
42    Ipv6Net::new_assert(Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0), 16),
43    Ipv6Net::new_assert(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0), 20),
44    Ipv6Net::new_assert(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7),
45    Ipv6Net::new_assert(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), 10),
46];
47
48const GLOBAL_V6: [Ipv6Net; 6] = [
49    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 1), 128),
50    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 2), 128),
51    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 3, 0, 0, 0, 0, 0, 0), 32),
52    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 4, 0x112, 0, 0, 0, 0, 0), 48),
53    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0x20, 0, 0, 0, 0, 0, 0), 28),
54    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0x30, 0, 0, 0, 0, 0, 0), 28),
55];
56
57/// `None` for an address reachable from outside; an IPv4 mapped into IPv6 counts as its IPv4.
58#[must_use]
59pub fn local_address(ip: IpAddr) -> Option<LocalAddress> {
60    match ip {
61        IpAddr::V4(ip) => local_v4(ip),
62        IpAddr::V6(ip) => ip.to_ipv4_mapped().map_or_else(|| local_v6(ip), local_v4),
63    }
64}
65
66fn local_v4(ip: Ipv4Addr) -> Option<LocalAddress> {
67    if LOOPBACK_V4.contains(&ip) {
68        Some(LocalAddress::Loopback)
69    } else if LOCAL_NETWORK_V4.iter().any(|net| net.contains(&ip)) && !GLOBAL_V4.contains(&ip) {
70        Some(LocalAddress::LocalNetwork)
71    } else {
72        None
73    }
74}
75
76fn local_v6(ip: Ipv6Addr) -> Option<LocalAddress> {
77    if ip.is_loopback() {
78        Some(LocalAddress::Loopback)
79    } else if LOCAL_NETWORK_V6.iter().any(|net| net.contains(&ip))
80        && !GLOBAL_V6.iter().any(|net| net.contains(&ip))
81    {
82        Some(LocalAddress::LocalNetwork)
83    } else {
84        None
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use std::net::IpAddr;
91
92    use super::{local_address, LocalAddress};
93
94    fn kind(ip: &str) -> Option<LocalAddress> {
95        local_address(ip.parse::<IpAddr>().unwrap())
96    }
97
98    // Each address read by Python 3.13's ipaddress: is_loopback, or is_private,
99    // is_link_local, 100.64.0.0/10 or 255.255.255.255.
100    #[test]
101    fn a_loopback_address_is_loopback() {
102        for ip in ["127.0.0.1", "127.255.0.1", "::1", "::ffff:127.0.0.1"] {
103            assert_eq!(kind(ip), Some(LocalAddress::Loopback), "{ip}");
104        }
105    }
106
107    #[test]
108    fn a_special_purpose_address_is_local_network() {
109        for ip in [
110            "0.0.0.0",
111            "10.0.0.1",
112            "100.64.0.1",
113            "169.254.1.1",
114            "172.16.0.1",
115            "192.0.0.1",
116            "192.0.0.171",
117            "192.0.2.1",
118            "192.168.0.1",
119            "198.19.0.1",
120            "198.51.100.1",
121            "203.0.113.1",
122            "240.0.0.1",
123            "255.255.255.255",
124            "::",
125            "::ffff:10.0.0.1",
126            "64:ff9b:1::1",
127            "100::1",
128            "2001:10::1",
129            "2001:db8::1",
130            "2002::1",
131            "3fff::1",
132            "fc00::1",
133            "fe80::1",
134        ] {
135            assert_eq!(kind(ip), Some(LocalAddress::LocalNetwork), "{ip}");
136        }
137    }
138
139    // Python reads 100.64.0.0/10 on the plain IPv4 only, and lets the mapped one through.
140    #[test]
141    fn a_mapped_ipv4_is_read_as_its_ipv4() {
142        assert_eq!(kind("::ffff:100.64.0.1"), Some(LocalAddress::LocalNetwork));
143    }
144
145    #[test]
146    fn a_global_address_is_not_local() {
147        for ip in [
148            "8.8.8.8",
149            "192.0.0.9",
150            "192.0.0.10",
151            "224.0.0.1",
152            "::ffff:8.8.8.8",
153            "::127.0.0.1",
154            "64:ff9b::a00:1",
155            "2001:1::1",
156            "2001:3::1",
157            "2001:4:112::1",
158            "2001:20::1",
159            "2001:30::1",
160            "2606:4700::1111",
161        ] {
162            assert_eq!(kind(ip), None, "{ip}");
163        }
164    }
165}