commonware_utils/
net.rs

1//! Utilities for working with IP addresses.
2
3use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4
5/// Bits in an IPv4 address.
6const IPV4_BITS: u8 = 32;
7
8/// Bits in an IPv6 address.
9const IPV6_BITS: u8 = 128;
10
11/// Canonical subnet representation for an IP address.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct Subnet {
14    addr: IpAddr,
15}
16
17/// Prefix lengths (in bits) used to derive canonical subnets for IPv4 and IPv6 addresses.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct SubnetMask {
20    pub ipv4: u32,
21    pub ipv6: u128,
22}
23
24impl SubnetMask {
25    /// Create a new [`SubnetMask`]. Values greater than the address width are clamped when applied.
26    pub const fn new(ipv4_bits: u8, ipv6_bits: u8) -> Self {
27        let ipv4_bits = Self::clamp(ipv4_bits, IPV4_BITS);
28        let ipv6_bits = Self::clamp(ipv6_bits, IPV6_BITS);
29        Self {
30            ipv4: Self::mask_ipv4(ipv4_bits),
31            ipv6: Self::mask_ipv6(ipv6_bits),
32        }
33    }
34
35    /// Clamp the given bits to the maximum value.
36    #[inline]
37    const fn clamp(bits: u8, max: u8) -> u8 {
38        if bits > max {
39            max
40        } else {
41            bits
42        }
43    }
44
45    /// Generate an IPv4 subnet mask that retains the upper `bits`.
46    #[inline]
47    const fn mask_ipv4(bits: u8) -> u32 {
48        if bits == 0 {
49            return 0;
50        }
51
52        (!0u32) << (32 - bits as u32)
53    }
54
55    /// Generate an IPv6 subnet mask that retains the upper `bits`.
56    #[inline]
57    const fn mask_ipv6(bits: u8) -> u128 {
58        if bits == 0 {
59            return 0;
60        }
61
62        (!0u128) << (128 - bits as u32)
63    }
64}
65
66/// Mask an IPv4 address according to the supplied [`SubnetMask`].
67#[inline]
68fn ipv4_subnet(ip: Ipv4Addr, mask: &SubnetMask) -> IpAddr {
69    IpAddr::V4(Ipv4Addr::from(u32::from(ip) & mask.ipv4))
70}
71
72/// Mask an IPv6 address according to the supplied [`SubnetMask`].
73#[inline]
74fn ipv6_subnet(ip: Ipv6Addr, mask: &SubnetMask) -> IpAddr {
75    IpAddr::V6(Ipv6Addr::from(u128::from(ip) & mask.ipv6))
76}
77
78/// Extension trait providing subnet helpers for [`IpAddr`].
79pub trait IpAddrExt {
80    /// Return the [`Subnet`] for the given [`SubnetMask`].
81    fn subnet(&self, mask: &SubnetMask) -> Subnet;
82
83    /// Determine if this IP address is globally routable.
84    // TODO: This mirrors the logic in the unstable `IpAddr::is_global` method from the standard library
85    // and can be removed once that API is stabilized.
86    fn is_global(&self) -> bool;
87}
88
89impl IpAddrExt for IpAddr {
90    fn subnet(&self, mask: &SubnetMask) -> Subnet {
91        match self {
92            IpAddr::V4(v4) => Subnet {
93                addr: ipv4_subnet(*v4, mask),
94            },
95            IpAddr::V6(v6) => {
96                if let Some(v4) = v6.to_ipv4_mapped() {
97                    return Subnet {
98                        addr: ipv4_subnet(v4, mask),
99                    };
100                }
101
102                Subnet {
103                    addr: ipv6_subnet(*v6, mask),
104                }
105            }
106        }
107    }
108
109    fn is_global(&self) -> bool {
110        match self {
111            IpAddr::V4(ip) => is_global_v4(*ip),
112            IpAddr::V6(ip) => is_global_v6(*ip),
113        }
114    }
115}
116
117#[inline]
118const fn is_future_protocol_v4(ip: Ipv4Addr) -> bool {
119    ip.octets()[0] == 192
120        && ip.octets()[1] == 0
121        && ip.octets()[2] == 0
122        && ip.octets()[3] != 9
123        && ip.octets()[3] != 10
124}
125
126#[inline]
127const fn is_shared_v4(ip: Ipv4Addr) -> bool {
128    ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)
129}
130
131#[inline]
132const fn is_benchmarking_v4(ip: Ipv4Addr) -> bool {
133    ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18
134}
135
136#[inline]
137const fn is_reserved_v4(ip: Ipv4Addr) -> bool {
138    ip.octets()[0] & 240 == 240 && !ip.is_broadcast()
139}
140
141#[inline]
142const fn is_global_v4(ip: Ipv4Addr) -> bool {
143    !(ip.octets()[0] == 0 // "This network"
144        || ip.is_private()
145        || is_shared_v4(ip)
146        || ip.is_loopback()
147        || ip.is_link_local()
148        || is_future_protocol_v4(ip)
149        || ip.is_documentation()
150        || is_benchmarking_v4(ip)
151        || is_reserved_v4(ip)
152        || ip.is_broadcast())
153}
154
155#[inline]
156const fn is_documentation_v6(ip: Ipv6Addr) -> bool {
157    (ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)
158}
159
160#[inline]
161const fn is_unique_local_v6(ip: Ipv6Addr) -> bool {
162    (ip.segments()[0] & 0xfe00) == 0xfc00
163}
164
165#[inline]
166const fn is_unicast_link_local_v6(ip: Ipv6Addr) -> bool {
167    (ip.segments()[0] & 0xffc0) == 0xfe80
168}
169
170#[inline]
171const fn is_global_v6(ip: Ipv6Addr) -> bool {
172    !(ip.is_unspecified()
173        || ip.is_loopback()
174        // IPv4-mapped Address (`::ffff:0:0/96`)
175        || matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
176        // IPv4-IPv6 Translation (`64:ff9b:1::/48`)
177        || matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
178        // Discard-Only Address Block (`100::/64`)
179        || matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _])
180        // IETF Protocol Assignments (`2001::/23`)
181        || (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
182            && !(
183                // Port Control Protocol Anycast (`2001:1::1`)
184                u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
185                // Traversal Using Relays around NAT Anycast (`2001:1::2`)
186                || u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
187                // AMT (`2001:3::/32`)
188                || matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _])
189                // AS112-v6 (`2001:4:112::/48`)
190                || matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
191                // ORCHIDv2 (`2001:20::/28`)
192                // Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)
193                || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x3F)
194            ))
195        // 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable,
196        // IANA says N/A.
197        || matches!(ip.segments(), [0x2002, _, _, _, _, _, _, _])
198        || is_documentation_v6(ip)
199        || is_unique_local_v6(ip)
200        || is_unicast_link_local_v6(ip))
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use std::str::FromStr;
207
208    /// Subnet mask using `/24` for IPv4 and `/48` for IPv6 networks.
209    const TEST_MASK: SubnetMask = SubnetMask::new(24, 48);
210
211    #[test]
212    fn ipv4_subnet_zeroes_lower_8_bits() {
213        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 123));
214        assert_eq!(
215            ip.subnet(&TEST_MASK).addr,
216            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))
217        );
218    }
219
220    #[test]
221    fn ipv6_subnet_zeroes_lower_80_bits() {
222        let ip = IpAddr::V6(Ipv6Addr::new(
223            0x2001, 0xdb8, 0x1234, 0x5678, 0x9abc, 0xdef0, 0x1357, 0x2468,
224        ));
225        assert_eq!(
226            ip.subnet(&TEST_MASK).addr,
227            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1234, 0, 0, 0, 0, 0))
228        );
229    }
230
231    #[test]
232    fn ipv4_mapped_ipv6_subnet_uses_ipv4_truncation() {
233        let ip = IpAddr::from_str("::ffff:192.168.1.123").unwrap();
234        assert_eq!(
235            ip.subnet(&TEST_MASK).addr,
236            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))
237        );
238    }
239
240    #[test]
241    fn subnet_mask_max() {
242        let mask = SubnetMask::new(40, 200);
243        assert_eq!(mask.ipv4, u32::MAX);
244        assert_eq!(mask.ipv6, u128::MAX);
245    }
246
247    #[test]
248    fn subnet_mask_min() {
249        let mask = SubnetMask::new(0, 0);
250        assert_eq!(mask.ipv4, 0);
251        assert_eq!(mask.ipv6, 0);
252    }
253
254    #[test]
255    #[allow(unstable_name_collisions)]
256    fn test_is_global_v4() {
257        // Test global IPv4 addresses
258        assert!(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)).is_global()); // Google DNS
259        assert!(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)).is_global()); // Cloudflare DNS
260        assert!(IpAddr::V4(Ipv4Addr::new(123, 45, 67, 89)).is_global()); // Random public address
261
262        // Test private IPv4 addresses
263        assert!(!IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)).is_global()); // 10.0.0.0/8
264        assert!(!IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)).is_global()); // 192.168.0.0/16
265        assert!(!IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)).is_global()); // 172.16.0.0/12
266        assert!(!IpAddr::V4(Ipv4Addr::new(172, 31, 255, 254)).is_global());
267
268        // Test shared address space (100.64.0.0/10)
269        assert!(!IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)).is_global());
270        assert!(!IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)).is_global());
271
272        // Test loopback addresses (127.0.0.0/8)
273        assert!(!IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)).is_global());
274        assert!(!IpAddr::V4(Ipv4Addr::new(127, 255, 255, 254)).is_global());
275
276        // Test link-local addresses (169.254.0.0/16)
277        assert!(!IpAddr::V4(Ipv4Addr::new(169, 254, 0, 1)).is_global());
278        assert!(!IpAddr::V4(Ipv4Addr::new(169, 254, 255, 254)).is_global());
279
280        // Test future use addresses (192.0.0.0/24 except 192.0.0.9 and 192.0.0.10)
281        assert!(!IpAddr::V4(Ipv4Addr::new(192, 0, 0, 1)).is_global());
282        assert!(!IpAddr::V4(Ipv4Addr::new(192, 0, 0, 254)).is_global());
283        // Exception addresses (192.0.0.9 and 192.0.0.10)
284        assert!(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 9)).is_global());
285        assert!(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 10)).is_global());
286
287        // Test documentation addresses
288        assert!(!IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)).is_global()); // 192.0.2.0/24
289        assert!(!IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)).is_global()); // 198.51.100.0/24
290        assert!(!IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)).is_global()); // 203.0.113.0/24
291
292        // Test benchmarking addresses (198.18.0.0/15)
293        assert!(!IpAddr::V4(Ipv4Addr::new(198, 18, 0, 1)).is_global());
294        assert!(!IpAddr::V4(Ipv4Addr::new(198, 19, 255, 254)).is_global());
295
296        // Test reserved addresses (240.0.0.0/4)
297        assert!(!IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)).is_global());
298        assert!(!IpAddr::V4(Ipv4Addr::new(254, 255, 255, 254)).is_global());
299
300        // Test broadcast address
301        assert!(!IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255)).is_global());
302    }
303
304    #[test]
305    #[allow(unstable_name_collisions)]
306    fn test_is_global_v6() {
307        // Test global IPv6 addresses
308        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:4860:4860::8888").unwrap()).is_global()); // Google DNS
309        assert!(IpAddr::V6(Ipv6Addr::from_str("2606:4700:4700::1111").unwrap()).is_global()); // Cloudflare DNS
310        assert!(
311            IpAddr::V6(Ipv6Addr::from_str("2005:1db8:85a3:0000:0000:8a2e:0370:7334").unwrap())
312                .is_global()
313        ); // Random global address
314
315        // Test unspecified address (::)
316        assert!(!IpAddr::V6(Ipv6Addr::UNSPECIFIED).is_global());
317
318        // Test loopback address (::1)
319        assert!(!IpAddr::V6(Ipv6Addr::LOCALHOST).is_global());
320
321        // Test IPv4-mapped addresses (::ffff:0:0/96)
322        assert!(!IpAddr::V6(Ipv6Addr::from_str("::ffff:192.0.2.128").unwrap()).is_global());
323
324        // Test IPv4-IPv6 translation addresses (64:ff9b:1::/48)
325        assert!(!IpAddr::V6(Ipv6Addr::from_str("64:ff9b:1::1").unwrap()).is_global());
326
327        // Test discard-only addresses (100::/64)
328        assert!(!IpAddr::V6(Ipv6Addr::from_str("100::1").unwrap()).is_global());
329
330        // Test IETF protocol assignments (2001::/23)
331        assert!(!IpAddr::V6(Ipv6Addr::from_str("2001:0::1").unwrap()).is_global()); // Within 2001::/23
332        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:1::1").unwrap()).is_global()); // Outside 2001::/23
333
334        // Test exceptions within 2001::/23
335        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:1::1").unwrap()).is_global()); // Port Control Protocol Anycast
336        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:1::2").unwrap()).is_global()); // Traversal Using Relays around NAT
337        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:3::1").unwrap()).is_global()); // AMT
338        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:4:112::1").unwrap()).is_global()); // AS112-v6
339        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:20::1").unwrap()).is_global()); // ORCHIDv2
340        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:30::1").unwrap()).is_global()); // Drone Remote ID
341
342        // Test 6to4 addresses (2002::/16)
343        assert!(!IpAddr::V6(Ipv6Addr::from_str("2002::1").unwrap()).is_global());
344
345        // Test documentation addresses (2001:db8::/32)
346        assert!(!IpAddr::V6(Ipv6Addr::from_str("2001:db8::1").unwrap()).is_global());
347
348        // Test unique local addresses (fc00::/7)
349        assert!(!IpAddr::V6(Ipv6Addr::from_str("fc00::1").unwrap()).is_global()); // fc00::/8
350        assert!(!IpAddr::V6(
351            Ipv6Addr::from_str("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff").unwrap()
352        )
353        .is_global()); // fd00::/8
354
355        // Test link-local unicast addresses (fe80::/10)
356        assert!(!IpAddr::V6(Ipv6Addr::from_str("fe80::1").unwrap()).is_global());
357
358        // Test multicast addresses (ff00::/8)
359        assert!(IpAddr::V6(Ipv6Addr::from_str("ff00::1").unwrap()).is_global());
360
361        // Test global address outside of special ranges
362        assert!(IpAddr::V6(Ipv6Addr::from_str("2003::1").unwrap()).is_global());
363    }
364
365    #[test]
366    #[allow(unstable_name_collisions)]
367    fn test_is_global_ipaddr() {
368        // Test with IpAddr enum
369        // Global IPv4
370        assert!(IpAddr::V4(Ipv4Addr::from_str("1.2.3.4").unwrap()).is_global());
371        // Non-global IPv4
372        assert!(!IpAddr::V4(Ipv4Addr::from_str("10.0.0.1").unwrap()).is_global());
373
374        // Global IPv6
375        assert!(IpAddr::V6(Ipv6Addr::from_str("2001:4860:4860::8888").unwrap()).is_global());
376        // Non-global IPv6
377        assert!(!IpAddr::V6(Ipv6Addr::from_str("fe80::1").unwrap()).is_global());
378    }
379}