Skip to main content

xmtp_common/
address.rs

1//! Private address checks for attachment downloads and webhook hosts.
2//!
3//! `address-registry.txt` is a snapshot of the IANA IPv4 and IPv6 registries:
4//! <https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml>
5//! <https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml>
6//! Both `<updated>` values were 2025-10-09 when checked on 2026-09-25.
7//! Refresh the snapshot when either `<updated>` value changes. A longer entry
8//! overrides its parent entry.
9
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
11
12/// RFC 7050 §2.2: a DNS64 synthesizes this name's AAAA under its NAT64 prefix.
13pub const NAT64_DISCOVERY_NAME: &str = "ipv4only.arpa";
14
15fn prefix_matches(address: IpAddr, prefix: IpAddr, bits: u32) -> bool {
16    match (address, prefix) {
17        (IpAddr::V4(address), IpAddr::V4(prefix)) => {
18            let mask = u32::MAX.checked_shl(32 - bits).unwrap_or(0);
19            u32::from(address) & mask == u32::from(prefix) & mask
20        }
21        (IpAddr::V6(address), IpAddr::V6(prefix)) => {
22            let mask = u128::MAX.checked_shl(128 - bits).unwrap_or(0);
23            u128::from(address) & mask == u128::from(prefix) & mask
24        }
25        _ => false,
26    }
27}
28
29fn parse_row(line: &str) -> Option<(IpAddr, u32, bool)> {
30    let mut fields = line.split_whitespace();
31    let block = fields.next()?;
32    let value = fields.next()?;
33    if fields.next().is_some() {
34        return None;
35    }
36    let (prefix, bits) = block.split_once('/')?;
37    let prefix: IpAddr = prefix.parse().ok()?;
38    let bits: u32 = bits.parse().ok()?;
39    let aligned = match prefix {
40        IpAddr::V4(ip) if bits <= 32 => {
41            let mask = u32::MAX.checked_shl(32 - bits).unwrap_or(0);
42            u32::from(ip) & mask == u32::from(ip)
43        }
44        IpAddr::V6(ip) if bits <= 128 => {
45            let mask = u128::MAX.checked_shl(128 - bits).unwrap_or(0);
46            u128::from(ip) & mask == u128::from(ip)
47        }
48        _ => return None,
49    };
50    let reachable = match value {
51        "true" => true,
52        "false" => false,
53        _ => return None,
54    };
55    aligned.then_some((prefix, bits, reachable))
56}
57
58/// Return true for an address that must not be reached by a default client.
59pub fn is_private(address: IpAddr) -> bool {
60    match address {
61        IpAddr::V4(ip) if ip.is_multicast() => return true,
62        IpAddr::V6(ip) if ip.is_multicast() => return true,
63        IpAddr::V6(ip) if embedded_ipv4(ip).is_some_and(|v4| is_private(IpAddr::V4(v4))) => {
64            return true;
65        }
66        _ => {}
67    }
68
69    let mut longest = 0;
70    let mut reachable = true;
71    for line in include_str!("address-registry.txt").lines() {
72        let Some((prefix, bits, row_reachable)) = parse_row(line) else {
73            continue;
74        };
75        if bits >= longest && prefix_matches(address, prefix, bits) {
76            longest = bits;
77            reachable = row_reachable;
78        }
79    }
80    !reachable
81}
82
83/// RFC 6052 §2.2: the IPv4 bytes follow the prefix and skip byte 8.
84fn embedded_at(ip: Ipv6Addr, bits: u8) -> Ipv4Addr {
85    let octets = ip.octets();
86    let mut v4 = (usize::from(bits / 8)..16)
87        .filter(|&i| i != 8)
88        .map(|i| octets[i]);
89    Ipv4Addr::from(std::array::from_fn(|_| v4.next().unwrap_or(0)))
90}
91
92/// RFC 7050 §3: the NAT64 prefixes in the answer for `NAT64_DISCOVERY_NAME`.
93fn nat64_prefixes(answer: impl IntoIterator<Item = IpAddr>) -> Vec<(Ipv6Addr, u8)> {
94    let mut prefixes = vec![];
95    for ip in answer {
96        let IpAddr::V6(ip) = ip else { continue };
97        for bits in [96, 64, 56, 48, 40, 32] {
98            if matches!(embedded_at(ip, bits).octets(), [192, 0, 0, 170 | 171]) {
99                let mask = u128::MAX << (128 - u32::from(bits));
100                prefixes.push((Ipv6Addr::from(u128::from(ip) & mask), bits));
101            }
102        }
103    }
104    prefixes
105}
106
107/// The prefixes in a `NAT64_DISCOVERY_NAME` answer. No lookup, a failed one,
108/// or one slower than 5 seconds adds no prefix.
109pub async fn discover_nat64<A, E>(
110    lookup: Option<impl Future<Output = Result<A, E>>>,
111) -> Vec<(Ipv6Addr, u8)>
112where
113    A: IntoIterator<Item = SocketAddr>,
114{
115    let Some(lookup) = lookup else {
116        return vec![];
117    };
118    match crate::time::timeout(std::time::Duration::from_secs(5), lookup).await {
119        Ok(Ok(answer)) => nat64_prefixes(answer.into_iter().map(|address| address.ip())),
120        _ => vec![],
121    }
122}
123
124/// `is_private`, or under a discovered NAT64 prefix with a private IPv4 address.
125pub fn is_private_behind(address: IpAddr, prefixes: &[(Ipv6Addr, u8)]) -> bool {
126    is_private(address)
127        || matches!(address, IpAddr::V6(ip) if prefixes.iter().any(|&(prefix, bits)| {
128            prefix_matches(address, prefix.into(), bits.into())
129                && is_private(embedded_at(ip, bits).into())
130        }))
131}
132
133fn embedded_ipv4(ip: Ipv6Addr) -> Option<Ipv4Addr> {
134    if let Some(v4) = ip.to_ipv4_mapped() {
135        return Some(v4);
136    }
137    let value = u128::from(ip);
138    let well_known = u128::from("64:ff9b::".parse::<Ipv6Addr>().unwrap());
139    let local_use = u128::from("64:ff9b:1::".parse::<Ipv6Addr>().unwrap());
140    let in_well_known = value >> 32 == well_known >> 32;
141    let in_local_use = value >> 80 == local_use >> 80;
142    (in_well_known || in_local_use).then(|| Ipv4Addr::from(value as u32))
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[xmtp_common::test(unwrap_try = true)]
150    fn registry_rows_are_well_formed() {
151        for (index, line) in include_str!("address-registry.txt").lines().enumerate() {
152            let line = line.trim();
153            if line.is_empty() || line.starts_with('#') {
154                continue;
155            }
156            assert!(parse_row(line).is_some(), "line {}: {line}", index + 1);
157        }
158    }
159
160    #[xmtp_common::test(unwrap_try = true)]
161    fn parse_row_accepts_only_well_formed_rows() {
162        for (row, expected) in [
163            ("10.0.0.0/8\tfalse", Some(("10.0.0.0".parse()?, 8, false))),
164            ("10.0.0.0/8", None),
165            ("10.0.0.0/8 true extra", None),
166            ("bad/8 true", None),
167            ("10.0.0.0 true", None),
168            ("10.0.0.0/33 true", None),
169            ("2001:db8::/129 true", None),
170            ("10.1.0.0/8 true", None),
171            ("2001:db8::1/32 true", None),
172            ("10.0.0.0/8 TRUE", None),
173        ] {
174            assert_eq!(parse_row(row), expected, "{row}");
175        }
176    }
177
178    #[xmtp_common::test(unwrap_try = true)]
179    fn registry_and_embedded_addresses() {
180        for address in [
181            "10.0.0.1",
182            "::ffff:10.0.0.1",
183            "64:ff9b::a00:1",
184            "64:ff9b:1::a00:1",
185            "::ffff:8.8.8.8",
186            "64:ff9b:1::808:808",
187            "ff02::1",
188            "224.0.0.1",
189            "192.0.2.1",
190        ] {
191            assert!(is_private(address.parse()?), "{address}");
192        }
193        for address in [
194            "8.8.8.8",
195            "2606:4700:4700::1111",
196            "192.0.0.9",
197            "64:ff9b::808:808",
198        ] {
199            assert!(!is_private(address.parse()?), "{address}");
200        }
201        let net: Ipv6Addr = "2a00:1450:4001:64::".parse()?;
202        let (net96, net64) = ((net, 96), (net, 64));
203        for (answer, expected) in [
204            ("2a00:1450:4001:64::c000:aa", vec![net96]),
205            ("2a00:1450:4001:64:c0:0:aa00:0", vec![net64]),
206            ("2001:db8:c000:ab::", vec![("2001:db8::".parse()?, 32)]),
207            ("192.0.0.170", vec![]),
208        ] {
209            assert_eq!(nat64_prefixes([answer.parse()?]), expected, "{answer}");
210        }
211        for (address, prefixes, expected) in [
212            ("2a00:1450:4001:64:a:0:100:0", net64, true),
213            ("2a00:1450:4001:64::808:808", net96, false),
214            ("2606:4700:4700::1111", net96, false),
215        ] {
216            assert_eq!(
217                is_private_behind(address.parse()?, &[prefixes]),
218                expected,
219                "{address}"
220            );
221        }
222    }
223}