Skip to main content

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}