Skip to main content

seyd_nat/
addr.rs

1// Copyright 2026 Anton Gravestam
2// SPDX-License-Identifier: Apache-2.0
3//! Address classification.
4//!
5//! `Ipv4Addr::is_global` is unstable in std, and the property we need is
6//! precise: *can a host on the public internet route to this address?* The
7//! trap is carrier-grade NAT space, `100.64.0.0/10` — it is not RFC 1918, so
8//! "is it private?" says no, yet nobody on the internet can reach it. A router
9//! under CGNAT will happily install a port mapping and report such an address;
10//! advertising it only burns a slot in the pilot's candidate race.
11
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
13
14/// Whether `ip` is reachable from the public internet.
15pub fn is_global(ip: IpAddr) -> bool {
16    match ip {
17        IpAddr::V4(v4) => is_global_v4(v4),
18        IpAddr::V6(v6) => is_global_v6(v6),
19    }
20}
21
22fn is_global_v4(ip: Ipv4Addr) -> bool {
23    let o = ip.octets();
24    !(ip.is_private()
25        || ip.is_loopback()
26        || ip.is_link_local()
27        || ip.is_broadcast()
28        || ip.is_documentation()
29        || ip.is_multicast()
30        || ip.is_unspecified()
31        || o[0] == 0
32        || (o[0] == 100 && (o[1] & 0xc0) == 64)          // 100.64.0.0/10 CGNAT
33        || (o[0] == 192 && o[1] == 0 && o[2] == 0)        // 192.0.0.0/24 IETF protocol assignments
34        || (o[0] == 198 && (o[1] & 0xfe) == 18)           // 198.18.0.0/15 benchmarking
35        || o[0] >= 240) // 240.0.0.0/4 reserved
36}
37
38fn is_global_v6(ip: Ipv6Addr) -> bool {
39    if let Some(v4) = ip.to_ipv4_mapped() {
40        return is_global_v4(v4);
41    }
42    let s = ip.segments();
43    !(ip.is_loopback()
44        || ip.is_unspecified()
45        || ip.is_multicast()
46        || (s[0] & 0xffc0) == 0xfe80                       // fe80::/10 link-local
47        || (s[0] & 0xfe00) == 0xfc00                       // fc00::/7 unique local
48        || (s[0] == 0x2001 && s[1] == 0x0db8)              // 2001:db8::/32 documentation
49        || (s[0] == 0x0064 && s[1] == 0xff9b)              // 64:ff9b::/96 NAT64 (not a host address)
50        || (s[0] == 0x2001 && s[1] == 0x0002 && s[2] == 0)) // 2001:2::/48 benchmarking
51}
52
53/// Wrap IPv6 literals in brackets so they can go in a URL authority.
54pub fn format_host(ip: IpAddr) -> String {
55    match ip {
56        IpAddr::V4(v4) => v4.to_string(),
57        IpAddr::V6(v6) => format!("[{v6}]"),
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    fn g(s: &str) -> bool {
66        is_global(s.parse().unwrap())
67    }
68
69    #[test]
70    fn ipv4_classification() {
71        assert!(!g("10.1.2.3"));
72        assert!(!g("172.16.0.1"));
73        assert!(!g("192.168.86.237"));
74        assert!(!g("100.87.8.146"), "CGNAT space must not be global");
75        assert!(!g("100.127.255.255"));
76        assert!(g("100.128.0.1"), "just past the /10");
77        assert!(!g("169.254.1.1"));
78        assert!(!g("127.0.0.1"));
79        assert!(!g("0.0.0.0"));
80        assert!(!g("203.0.113.5"), "documentation space");
81        assert!(!g("198.18.0.1"));
82        assert!(!g("240.0.0.1"));
83        assert!(g("8.8.8.8"));
84        assert!(g("95.216.1.1"));
85    }
86
87    #[test]
88    fn ipv6_classification() {
89        assert!(!g("fc00::1"));
90        assert!(!g("fd12:3456::1"));
91        assert!(!g("fe80::1"));
92        assert!(!g("::1"));
93        assert!(!g("2001:db8::1"));
94        assert!(!g("::ffff:192.168.1.1"), "v4-mapped private");
95        assert!(g("::ffff:8.8.8.8"), "v4-mapped public");
96        assert!(g("2606:4700::1111"));
97        assert!(g("2a00:1450:400f:80d::200e"));
98    }
99
100    #[test]
101    fn host_formatting() {
102        assert_eq!(format_host("1.2.3.4".parse().unwrap()), "1.2.3.4");
103        assert_eq!(
104            format_host("2606:4700::1111".parse().unwrap()),
105            "[2606:4700::1111]"
106        );
107    }
108}