Skip to main content

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