dig_stun/scope.rs
1//! The single address-scope classifier (`SPEC.md` §5) — the one range table every consumer asks
2//! "could this address be a legitimate reflexive candidate, and could a stranger route to it?"
3//! against.
4//!
5//! Before this crate, two predicates answered overlapping questions with range tables that had
6//! drifted apart: `dig-nat`'s dial guard (`is_usable_reflexive_addr`) and `dig-node`'s on-chain gate
7//! (`is_globally_routable`). [`Scope`] is the one table both are now DERIVED from; §5.4 names the
8//! four places their old tables disagreed, and this table resolves each one toward the SAFER
9//! reading (`SPEC.md` §5.5: a classification error toward [`Scope::NeverDialable`] costs a
10//! candidate; toward [`Scope::GlobalUnicast`] puts an unreachable address into an on-chain
11//! advertisement — so an ambiguous range classifies down).
12
13use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
14
15/// How dialable an address is, as three tiers (`SPEC.md` §5.2). Exhaustive: adding a variant is a
16/// breaking change, since [`is_usable_reflexive_addr`] and [`is_globally_routable`] are both
17/// defined in terms of an exhaustive match over this enum.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Scope {
20 /// Never a destination: reserved, documentation, loopback, link-local, multicast, unspecified,
21 /// benchmarking, discard-only, IETF-assignments, or port `0`.
22 NeverDialable,
23 /// Dialable only from inside the same site or carrier region: RFC 1918, RFC 6598 CGNAT, IPv6
24 /// ULA. A true reading of the node's position, and never something to advertise to strangers.
25 PrivateScope,
26 /// Everything else: an address a stranger on the open internet could route to.
27 GlobalUnicast,
28}
29
30/// Fold an IPv4-mapped (`::ffff:a.b.c.d`) OR deprecated IPv4-**compatible** (`::a.b.c.d`) IPv6
31/// address down to the IPv4 address it embeds. A genuine IPv4 address, or a genuine native IPv6
32/// address, is returned unchanged.
33///
34/// **Fold first, always** (`SPEC.md` §5.3): an on-path STUN server (or a lying peer) fully controls
35/// every decoded byte, so classifying a 16-byte value AS IPv6 without folding first would let it
36/// smuggle any rejected IPv4 range — e.g. `::ffff:127.0.0.1` or the compat form `::7f00:1` for
37/// loopback — past a v6-only classifier. [`Ipv6Addr::to_ipv4`] folds BOTH forms; `to_canonical` is
38/// deliberately NOT used here, since it folds only the mapped form and would miss the compat one.
39///
40/// This is also the reason `0.0.0.0/8` cannot be dropped from the IPv4 table as "redundant" with an
41/// IPv6 unspecified/loopback check: `::` and `::1` both fold to an address in `0.0.0.0/8`
42/// (`0.0.0.0` and `0.0.0.1` respectively), so that row is what actually rejects them post-fold.
43pub(crate) fn fold_ip(ip: IpAddr) -> IpAddr {
44 match ip {
45 IpAddr::V4(_) => ip,
46 IpAddr::V6(v6) => match v6.to_ipv4() {
47 Some(v4) => IpAddr::V4(v4),
48 None => ip,
49 },
50 }
51}
52
53/// Classify a bare IP (`SPEC.md` §5.3). Folds per `fold_ip` first, so a mapped or compat IPv6
54/// value is classified as the IPv4 address it represents.
55///
56/// A caller holding only an [`IpAddr`] (no port) MUST apply the `port == 0` rule of [`scope_of`]
57/// itself if it is relevant to that call site — this function has no port to see.
58pub fn scope_of_ip(ip: IpAddr) -> Scope {
59 match fold_ip(ip) {
60 IpAddr::V4(v4) => scope_of_v4(v4),
61 IpAddr::V6(v6) => scope_of_v6(v6),
62 }
63}
64
65/// Classify a `SocketAddr` (`SPEC.md` §5.3): [`Scope::NeverDialable`] when `addr.port() == 0`,
66/// regardless of the IP, else [`scope_of_ip`] of `addr.ip()`.
67pub fn scope_of(addr: SocketAddr) -> Scope {
68 if addr.port() == 0 {
69 return Scope::NeverDialable;
70 }
71 scope_of_ip(addr.ip())
72}
73
74/// Whether `addr` could ever be a legitimate reflexive dial target — true for [`Scope::PrivateScope`]
75/// AND [`Scope::GlobalUnicast`], false only for [`Scope::NeverDialable`] (`SPEC.md` §5.2). This is
76/// deliberately NOT "is this address public": a LAN or CGNAT address is a genuinely valid dial
77/// target for a peer on the same site or carrier region.
78pub fn is_usable_reflexive_addr(addr: &SocketAddr) -> bool {
79 scope_of(*addr) != Scope::NeverDialable
80}
81
82/// Whether a stranger on the open internet could route to `addr` — true only for
83/// [`Scope::GlobalUnicast`] (`SPEC.md` §5.2). This is the gate for anything written into an
84/// on-chain advertisement: a [`Scope::PrivateScope`] reading is a true reading of the node's
85/// position and is still never something to advertise to strangers.
86pub fn is_globally_routable(addr: &SocketAddr) -> bool {
87 scope_of(*addr) == Scope::GlobalUnicast
88}
89
90/// The IPv4 half of the table (`SPEC.md` §5.3): 11 [`Scope::NeverDialable`] ranges, 4
91/// [`Scope::PrivateScope`] ranges, everything else [`Scope::GlobalUnicast`].
92fn scope_of_v4(v4: Ipv4Addr) -> Scope {
93 let o = v4.octets();
94 let [a, b, ..] = o;
95
96 // 0.0.0.0/8, "this network" (RFC 1122) — also where `::` and `::1` land after folding
97 // (`fold_ip`'s doc comment). Subsumes `is_unspecified` for every v4 input; kept alongside it
98 // below for the same explicit-over-implicit reason both source predicates wrote it out.
99 let is_this_network = a == 0;
100 // 192.0.0.0/24, IETF protocol assignments (RFC 6890) — RECONCILED: dig-node's on-chain gate
101 // already rejected this; dig-nat's dial guard did not (`SPEC.md` §5.4).
102 let is_ietf_protocol_assignment = a == 192 && b == 0 && o[2] == 0;
103 // 192.88.99.0/24, 6to4 relay anycast, deprecated (RFC 7526) — RECONCILED: dig-nat's dial guard
104 // already rejected this; dig-node's on-chain gate did not, and shipped wrong (`SPEC.md` §5.4).
105 let is_6to4_relay_anycast = o[..3] == [192, 88, 99];
106 // 198.18.0.0/15, benchmarking (RFC 2544).
107 let is_benchmarking = a == 198 && (b & 0xfe) == 18;
108 // 240.0.0.0/4, reserved / class E (RFC 1112) — includes 255.255.255.255.
109 let is_reserved_class_e = a >= 240;
110 // 100.64.0.0/10, carrier-grade NAT shared space (RFC 6598) — PrivateScope, not NeverDialable.
111 let is_carrier_grade_nat = a == 100 && (64..128).contains(&b);
112
113 if v4.is_unspecified()
114 || v4.is_loopback()
115 || v4.is_link_local()
116 || v4.is_multicast()
117 || v4.is_broadcast()
118 || v4.is_documentation() // 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 (RFC 5737)
119 || is_this_network
120 || is_ietf_protocol_assignment
121 || is_6to4_relay_anycast
122 || is_benchmarking
123 || is_reserved_class_e
124 {
125 Scope::NeverDialable
126 } else if v4.is_private() || is_carrier_grade_nat {
127 // is_private(): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918).
128 Scope::PrivateScope
129 } else {
130 Scope::GlobalUnicast
131 }
132}
133
134/// The native-IPv6 half of the table (`SPEC.md` §5.3): 7 [`Scope::NeverDialable`] ranges, 1
135/// [`Scope::PrivateScope`] range, everything else [`Scope::GlobalUnicast`]. Only ever sees a
136/// genuine native IPv6 address — a mapped or compat form is folded to IPv4 by `fold_ip` before
137/// [`scope_of_ip`] reaches here.
138fn scope_of_v6(v6: Ipv6Addr) -> Scope {
139 let seg = v6.segments();
140
141 // fe80::/10, link-local unicast (`Ipv6Addr::is_unicast_link_local` is unstable; masked here).
142 let is_link_local = (seg[0] & 0xffc0) == 0xfe80;
143 // 2001:db8::/32, documentation (RFC 3849).
144 let is_documentation = seg[0] == 0x2001 && seg[1] == 0x0db8;
145 // 2001:2::/48, benchmarking (RFC 5180) — RECONCILED: dig-node's on-chain gate already rejected
146 // this; dig-nat's dial guard did not (`SPEC.md` §5.4).
147 let is_benchmarking = seg[0] == 0x2001 && seg[1] == 0x0002 && seg[2] == 0x0000;
148 // 100::/64, discard-only (RFC 6666) — RECONCILED, same direction as benchmarking above.
149 let is_discard_only = seg[0] == 0x0100 && seg[1] == 0 && seg[2] == 0 && seg[3] == 0;
150 // fc00::/7, unique local (RFC 4193) — PrivateScope, the IPv6 analogue of RFC 1918.
151 let is_unique_local = (seg[0] & 0xfe00) == 0xfc00;
152
153 if v6.is_unspecified()
154 || v6.is_loopback()
155 || v6.is_multicast()
156 || is_link_local
157 || is_documentation
158 || is_benchmarking
159 || is_discard_only
160 {
161 Scope::NeverDialable
162 } else if is_unique_local {
163 Scope::PrivateScope
164 } else {
165 Scope::GlobalUnicast
166 }
167}