Skip to main content

dig_nat/
stun.rs

1//! Minimal STUN (RFC 5389) client — discover this node's *reflexive* (public) transport address.
2//!
3//! A NAT'd node cannot see the `ip:port` the outside world dials it on. STUN answers that: the node
4//! sends a **Binding request** to a STUN server (the DIG relay runs one; any RFC-5389 STUN server
5//! also works) and the server replies with a **Binding success response** carrying the node's
6//! reflexive address in an `XOR-MAPPED-ADDRESS` attribute. That reflexive `ip:port` is the
7//! **server-reflexive candidate** dig-nat advertises so a remote peer can attempt a direct dial or
8//! a coordinated hole-punch.
9//!
10//! We implement the small datagram directly (RFC 5389 §6, §15.2) rather than pulling a STUN crate:
11//! it is a fixed 20-byte header + TLV attributes, so encoding/parsing is tiny and every branch is
12//! unit-testable against the RFC byte layout with no network. The relay's STUN server is expected
13//! to speak this exact wire; if the sibling agent's dig-relay STUN implementation diverges, this is
14//! the module to reconcile (see the crate-level reconciliation note).
15
16use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
17use std::time::Duration;
18
19use tokio::net::UdpSocket;
20
21/// STUN magic cookie (RFC 5389 §6). Always the first 4 bytes after the message type + length.
22pub const MAGIC_COOKIE: u32 = 0x2112_A442;
23
24/// Binding request message type (RFC 5389 §6 — method Binding = 0x001, class Request = 0b00).
25pub const BINDING_REQUEST: u16 = 0x0001;
26/// Binding success response message type (method Binding, class Success = 0b10).
27pub const BINDING_SUCCESS: u16 = 0x0101;
28
29/// `XOR-MAPPED-ADDRESS` attribute type (RFC 5389 §15.2).
30pub const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
31/// Legacy `MAPPED-ADDRESS` attribute type (RFC 5389 §15.1) — some servers still emit it.
32pub const ATTR_MAPPED_ADDRESS: u16 = 0x0001;
33
34/// Address family markers inside a (XOR-)MAPPED-ADDRESS attribute.
35const FAMILY_IPV4: u8 = 0x01;
36const FAMILY_IPV6: u8 = 0x02;
37
38/// Errors decoding a STUN response or performing a Binding transaction.
39#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40pub enum StunError {
41    /// The datagram was shorter than a valid STUN message / attribute.
42    #[error("STUN message truncated")]
43    Truncated,
44    /// The magic cookie did not match — not a STUN (RFC 5389) message.
45    #[error("bad STUN magic cookie")]
46    BadMagicCookie,
47    /// The transaction id in the response did not match the request (possible spoof / stale reply).
48    #[error("STUN transaction id mismatch")]
49    TransactionIdMismatch,
50    /// The message parsed but carried no usable mapped address: either no (XOR-)MAPPED-ADDRESS
51    /// attribute at all, OR a parsed address that failed the reflexive-usability guard
52    /// ([`is_usable_reflexive_addr`] — e.g. a non-global/reserved address such as loopback,
53    /// link-local, multicast, a documentation range, or `port == 0`).
54    #[error("no usable mapped address in STUN response")]
55    NoMappedAddress,
56    /// The message type was not a Binding success response.
57    #[error("unexpected STUN message type: {0:#06x}")]
58    UnexpectedType(u16),
59    /// Underlying socket I/O error (stringified so [`StunError`] stays `Clone`/`Eq`).
60    #[error("STUN io: {0}")]
61    Io(String),
62    /// The transaction did not complete within the deadline.
63    #[error("STUN request timed out")]
64    Timeout,
65}
66
67/// A STUN Binding request: 20-byte header (type, length=0, cookie, 96-bit transaction id) and no
68/// attributes. `transaction_id` is caller-supplied so the response can be matched to the request.
69pub fn encode_binding_request(transaction_id: &[u8; 12]) -> Vec<u8> {
70    let mut msg = Vec::with_capacity(20);
71    msg.extend_from_slice(&BINDING_REQUEST.to_be_bytes()); // message type
72    msg.extend_from_slice(&0u16.to_be_bytes()); // message length (no attributes)
73    msg.extend_from_slice(&MAGIC_COOKIE.to_be_bytes()); // magic cookie
74    msg.extend_from_slice(transaction_id); // 96-bit transaction id
75    msg
76}
77
78/// Whether a parsed reflexive [`SocketAddr`] is usable as a server-reflexive candidate — a
79/// defense-in-depth guard against a malicious or misconfigured STUN server (the relay runs one)
80/// returning a bogus address that a consumer would then advertise (#1387).
81///
82/// Returns `false` (reject) for addresses that can never be a legitimate reflexive candidate,
83/// across BOTH families: unspecified, loopback, link-local, multicast, the RFC-5737/3849
84/// documentation ranges, the IPv4 limited-broadcast address, the never-dialable IPv4 ranges
85/// (`0.0.0.0/8` "this-network", `192.88.99.0/24` 6to4-relay anycast, `198.18.0.0/15` benchmarking,
86/// `240.0.0.0/4` reserved/class-E), and `port == 0`.
87///
88/// # IPv4-mapped / -compatible IPv6 cannot smuggle a rejected IPv4 range
89///
90/// The address is folded to IPv4 BEFORE classification, so an IPv4-mapped (`::ffff:a.b.c.d`) or
91/// deprecated IPv4-compatible (`::a.b.c.d`) form is evaluated as the IPv4 address it represents and
92/// hits the IPv4 predicate. Without this, a malicious STUN server (which fully controls the 16
93/// decoded bytes) could smuggle e.g. `::ffff:127.0.0.1` or the compat form `::7f00:1` past a V6-only
94/// classifier and induce a peer to self-dial loopback / the same LAN. [`Ipv6Addr::to_ipv4`] folds
95/// BOTH forms (stable since Rust 1.0), unlike `to_canonical` which folds only the mapped form.
96///
97/// # Why this is NOT a blanket `is_global`
98///
99/// PRIVATE (RFC 1918 `10/8`, `172.16/12`, `192.168/16`), CGNAT (RFC 6598 `100.64/10`), and IPv6
100/// ULA (`fc00::/7`) addresses are deliberately ACCEPTED. They are genuinely valid reflexive
101/// addresses on a LAN or behind carrier-grade NAT — a peer on the same LAN/CGNAT region reaches
102/// them directly. Rejecting them would break LAN and test-network reflexive discovery, including
103/// the #1062 EC2 e2e where nodes learn a private VPC reflexive address. This guard removes only
104/// the addresses that are *never* a valid dial target, not merely non-public ones.
105fn is_usable_reflexive_addr(addr: &SocketAddr) -> bool {
106    if addr.port() == 0 {
107        return false;
108    }
109    // Fold IPv4-mapped/-compatible IPv6 to V4 first so those forms cannot bypass the V4 predicate
110    // (an on-path STUN server controls every decoded byte — see the doc-comment). `to_ipv4` folds
111    // both `::ffff:a.b.c.d` and the deprecated `::a.b.c.d`; a genuine native V6 yields `None`.
112    match addr.ip() {
113        IpAddr::V4(v4) => is_usable_reflexive_v4(v4),
114        IpAddr::V6(v6) => match v6.to_ipv4() {
115            Some(v4) => is_usable_reflexive_v4(v4),
116            None => is_usable_reflexive_v6(v6),
117        },
118    }
119}
120
121/// The IPv4 reflexive predicate: reject only the ranges that are *never* a valid dial target,
122/// keeping private/CGNAT accepted (see [`is_usable_reflexive_addr`]).
123fn is_usable_reflexive_v4(v4: Ipv4Addr) -> bool {
124    let [a, b, _, _] = v4.octets();
125    // `0.0.0.0/8` "this-network" (RFC 1122) — non-zero hosts here are not dialable either.
126    let is_this_network = a == 0;
127    // `192.88.99.0/24` — 6to4 relay anycast (RFC 7526), not a unicast dial target.
128    let is_6to4_relay_anycast = v4.octets()[..3] == [192, 88, 99];
129    // `198.18.0.0/15` — benchmarking (RFC 2544).
130    let is_benchmarking = a == 198 && (b & 0xfe) == 18;
131    // `240.0.0.0/4` — reserved / class-E (RFC 1112), incl. the 255.255.255.255 broadcast.
132    let is_reserved_class_e = a >= 240;
133    !v4.is_unspecified()
134        && !v4.is_loopback()
135        && !v4.is_link_local()
136        && !v4.is_multicast()
137        && !v4.is_broadcast()
138        && !v4.is_documentation()
139        && !is_this_network
140        && !is_6to4_relay_anycast
141        && !is_benchmarking
142        && !is_reserved_class_e
143}
144
145/// The genuine-native-IPv6 reflexive predicate. Only ever sees real V6 addresses — mapped/compat
146/// forms are folded to V4 by [`is_usable_reflexive_addr`] before this is reached.
147fn is_usable_reflexive_v6(v6: Ipv6Addr) -> bool {
148    let seg = v6.segments();
149    // fe80::/10 — link-local unicast (`is_unicast_link_local` is unstable, check manually).
150    let is_link_local = (seg[0] & 0xffc0) == 0xfe80;
151    // 2001:db8::/32 — documentation (`is_documentation` is unstable, check manually).
152    let is_documentation = seg[0] == 0x2001 && seg[1] == 0x0db8;
153    !v6.is_unspecified()
154        && !v6.is_loopback()
155        && !v6.is_multicast()
156        && !is_link_local
157        && !is_documentation
158}
159
160/// Parse a STUN **Binding success response**, returning the reflexive [`SocketAddr`] from its
161/// `XOR-MAPPED-ADDRESS` (preferred) or legacy `MAPPED-ADDRESS` attribute.
162///
163/// Validates the magic cookie and (when `expected_txid` is `Some`) the transaction id, so a stale
164/// or spoofed datagram is rejected. Implements the XOR de-obfuscation of RFC 5389 §15.2.
165///
166/// This is a PURE parser: it does NOT check whether the returned address is a usable
167/// (non-reserved, globally/LAN-dialable) reflexive candidate. A caller wanting a usable
168/// server-reflexive candidate should use [`query_reflexive_address`], which applies the
169/// [`is_usable_reflexive_addr`] guard on top of parsing.
170pub fn parse_binding_response(
171    msg: &[u8],
172    expected_txid: Option<&[u8; 12]>,
173) -> Result<SocketAddr, StunError> {
174    if msg.len() < 20 {
175        return Err(StunError::Truncated);
176    }
177    let msg_type = u16::from_be_bytes([msg[0], msg[1]]);
178    let msg_len = u16::from_be_bytes([msg[2], msg[3]]) as usize;
179    let cookie = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
180    if cookie != MAGIC_COOKIE {
181        return Err(StunError::BadMagicCookie);
182    }
183    if msg_type != BINDING_SUCCESS {
184        return Err(StunError::UnexpectedType(msg_type));
185    }
186    let txid: [u8; 12] = msg[8..20].try_into().map_err(|_| StunError::Truncated)?;
187    if let Some(expected) = expected_txid {
188        if &txid != expected {
189            return Err(StunError::TransactionIdMismatch);
190        }
191    }
192    if msg.len() < 20 + msg_len {
193        return Err(StunError::Truncated);
194    }
195
196    // Walk the TLV attributes. Prefer XOR-MAPPED-ADDRESS; fall back to MAPPED-ADDRESS.
197    let mut fallback: Option<SocketAddr> = None;
198    let mut off = 20usize;
199    let end = 20 + msg_len;
200    while off + 4 <= end {
201        let attr_type = u16::from_be_bytes([msg[off], msg[off + 1]]);
202        let attr_len = u16::from_be_bytes([msg[off + 2], msg[off + 3]]) as usize;
203        let val_start = off + 4;
204        let val_end = val_start + attr_len;
205        if val_end > end {
206            return Err(StunError::Truncated);
207        }
208        let value = &msg[val_start..val_end];
209        match attr_type {
210            ATTR_XOR_MAPPED_ADDRESS => {
211                return decode_mapped_address(value, &txid, true);
212            }
213            ATTR_MAPPED_ADDRESS if fallback.is_none() => {
214                fallback = decode_mapped_address(value, &txid, false).ok();
215            }
216            _ => {}
217        }
218        // Attributes are padded to a 4-byte boundary (RFC 5389 §15).
219        off = val_end + ((4 - (attr_len % 4)) % 4);
220    }
221    fallback.ok_or(StunError::NoMappedAddress)
222}
223
224/// Decode a (XOR-)MAPPED-ADDRESS attribute value into a [`SocketAddr`].
225///
226/// Layout (RFC 5389 §15.1/§15.2): `[reserved:1][family:1][port:2][address:4 or 16]`. When `xor` is
227/// set, the port is XORed with the top 16 bits of the magic cookie and the address is XORed with the
228/// full cookie (IPv4) or cookie‖transaction-id (IPv6).
229fn decode_mapped_address(
230    value: &[u8],
231    txid: &[u8; 12],
232    xor: bool,
233) -> Result<SocketAddr, StunError> {
234    if value.len() < 4 {
235        return Err(StunError::Truncated);
236    }
237    let family = value[1];
238    let raw_port = u16::from_be_bytes([value[2], value[3]]);
239    let cookie_be = MAGIC_COOKIE.to_be_bytes();
240    let port = if xor {
241        raw_port ^ ((MAGIC_COOKIE >> 16) as u16)
242    } else {
243        raw_port
244    };
245
246    match family {
247        FAMILY_IPV4 => {
248            if value.len() < 8 {
249                return Err(StunError::Truncated);
250            }
251            let mut octets = [value[4], value[5], value[6], value[7]];
252            if xor {
253                for (i, o) in octets.iter_mut().enumerate() {
254                    *o ^= cookie_be[i];
255                }
256            }
257            Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(octets)), port))
258        }
259        FAMILY_IPV6 => {
260            if value.len() < 20 {
261                return Err(StunError::Truncated);
262            }
263            let mut octets = [0u8; 16];
264            octets.copy_from_slice(&value[4..20]);
265            if xor {
266                // XOR key is the 32-bit cookie followed by the 96-bit transaction id.
267                let mut key = [0u8; 16];
268                key[..4].copy_from_slice(&cookie_be);
269                key[4..].copy_from_slice(txid);
270                for (o, k) in octets.iter_mut().zip(key.iter()) {
271                    *o ^= *k;
272                }
273            }
274            Ok(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), port))
275        }
276        other => Err(StunError::UnexpectedType(other as u16)),
277    }
278}
279
280/// Perform a single STUN Binding transaction against `server` over `socket`, returning the
281/// discovered reflexive (public) [`SocketAddr`] of `socket`. Bounded by `timeout`; a lost datagram
282/// surfaces as [`StunError::Timeout`] (the caller retries or falls through to the next method).
283///
284/// This is **THE API to obtain a DIALABLE server-reflexive candidate**: it returns the reflexive
285/// `ip:port` mapping of the caller's OWN listen socket, so the port is the real external binding a
286/// remote peer can dial (unlike [`discover_reflexive_address`], which learns the public IP over a
287/// throwaway ephemeral socket — see its `## Port caveat`). Connectivity-core (dig-node) should STUN
288/// from its actual listen socket via this function to advertise a dialable candidate.
289///
290/// The `socket` should be the very UDP socket whose external mapping the caller wants to learn —
291/// the reflexive address is specific to the NAT binding created by *that* socket.
292///
293/// The returned address is checked against [`is_usable_reflexive_addr`]: a parsed-but-unusable
294/// (non-global/reserved) reflexive address is rejected as [`StunError::NoMappedAddress`] (#1387).
295///
296/// ## Anti-spoof: source address validation (#179 finding 2)
297///
298/// A UDP reply's source address is easy to check and hard for an off-path attacker to spoof
299/// (spoofing the source AND getting the reply routed back requires being on-path or the same
300/// network). This function therefore accepts a datagram only when it actually originates from
301/// `server`; anything else (a stray reply, a scan, an attacker racing a forged response) is
302/// discarded and the receive loop continues within the overall `timeout` deadline — a single
303/// mismatched-source datagram must not fail the whole transaction, since the real reply may still be
304/// in flight. This is independent, defense-in-depth hygiene alongside the transaction-id check
305/// ([`new_transaction_id`]); neither replaces the other.
306pub async fn query_reflexive_address(
307    socket: &UdpSocket,
308    server: SocketAddr,
309    timeout: Duration,
310) -> Result<SocketAddr, StunError> {
311    let txid = new_transaction_id();
312    let req = encode_binding_request(&txid);
313    socket
314        .send_to(&req, server)
315        .await
316        .map_err(|e| StunError::Io(e.to_string()))?;
317
318    let mut buf = [0u8; 512];
319    let deadline = tokio::time::Instant::now() + timeout;
320    loop {
321        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
322        if remaining.is_zero() {
323            return Err(StunError::Timeout);
324        }
325        let (n, from) = match tokio::time::timeout(remaining, socket.recv_from(&mut buf)).await {
326            Ok(Ok(x)) => x,
327            Ok(Err(e)) => return Err(StunError::Io(e.to_string())),
328            Err(_) => return Err(StunError::Timeout),
329        };
330        if from != server {
331            // Not from the queried server — ignore and keep waiting for the genuine reply.
332            continue;
333        }
334        let addr = parse_binding_response(&buf[..n], Some(&txid))?;
335        // Defense-in-depth (#1387): a malicious/misconfigured STUN server can return a bogus
336        // reflexive address (loopback, multicast, a documentation range, port 0, …) that we would
337        // otherwise advertise. Reject any address that is not a usable reflexive candidate; the
338        // caller (or `discover_reflexive_address`) then falls through to the next candidate.
339        if !is_usable_reflexive_addr(&addr) {
340            return Err(StunError::NoMappedAddress);
341        }
342        return Ok(addr);
343    }
344}
345
346/// Discover this node's server-reflexive (public) address via STUN, IPv6-first with IPv4 FALLBACK
347/// (CLAUDE.md §5.2). `stun_servers` are the resolved STUN endpoints across BOTH families (e.g. every
348/// A + AAAA record of `<relay-host>:3478` — the caller MUST NOT pre-collapse to one family). The STUN
349/// Binding transaction is raced over the local∩server family intersection via [`dig_ip::connect`]:
350/// IPv6 is attempted first and IPv4 is used as a fallback when the IPv6 STUN server is unreachable —
351/// the reflexive address is NEVER nulled just because the IPv6 STUN server did not respond. Returns
352/// the discovered reflexive [`SocketAddr`], or `None` when no family's STUN server answered.
353///
354/// This is the canonical front-door fix for the #1062 gap: consumers (dig-node) MUST call this
355/// instead of hand-rolling a family sort or collapsing `to_socket_addrs()` to a single family — the
356/// happy-eyeballs racer and the local∩server intersection live here, in ONE place, per the dig-ip
357/// charter ("NO repo hand-rolls a family sort or happy-eyeballs racer").
358///
359/// ## Port caveat
360///
361/// Each candidate is STUNed over a THROWAWAY ephemeral UDP socket bound just for that transaction,
362/// so the returned IP is the node's stable public IP but the **PORT is that throwaway socket's NAT
363/// binding — NOT reliably dialable** under most NAT types (a remote peer dialing it will usually
364/// fail). Use this to learn the public IP; for a DIALABLE server-reflexive candidate, STUN from
365/// your ACTUAL listen socket via [`query_reflexive_address`] instead.
366pub async fn discover_reflexive_address(
367    stun_servers: &[SocketAddr],
368    local: dig_ip::LocalStack,
369    timeout: Duration,
370) -> Option<SocketAddr> {
371    if stun_servers.is_empty() {
372        return None;
373    }
374
375    let mut candidates = dig_ip::PeerCandidates::new();
376    candidates.extend(
377        stun_servers.iter().copied(),
378        dig_ip::CandidateSource::StunReflexive,
379    );
380
381    let config = dig_ip::DialConfig {
382        per_attempt_timeout: timeout,
383        ..Default::default()
384    };
385
386    // One "dial" == one full STUN Binding transaction against a candidate server. We bind an
387    // ephemeral UDP socket in the SERVER's family (dig-ip only hands us a family the local host can
388    // originate on) and learn that socket's reflexive mapping. The racer returns the first family's
389    // successful reflexive address, preferring IPv6.
390    let winner = dig_ip::connect(&local, &candidates, config, |stun_addr| async move {
391        let bind: SocketAddr = if stun_addr.is_ipv6() {
392            (Ipv6Addr::UNSPECIFIED, 0).into()
393        } else {
394            (Ipv4Addr::UNSPECIFIED, 0).into()
395        };
396        let socket = UdpSocket::bind(bind)
397            .await
398            .map_err(|e| format!("bind {bind}: {e}"))?;
399        query_reflexive_address(&socket, stun_addr, timeout)
400            .await
401            .map_err(|e| e.to_string())
402    })
403    .await;
404
405    match winner {
406        Ok(w) => Some(w.conn),
407        Err(_) => None,
408    }
409}
410
411/// Generate a 96-bit STUN transaction id from a CSPRNG (RFC 5389 §10.1: "It primarily serves to
412/// correlate requests with responses... **and MUST be uniformly and randomly chosen from the
413/// interval 0 .. 2**96 - 1, and SHOULD be cryptographically random").
414///
415/// The transaction id is the ONLY anti-spoof mechanism [`query_reflexive_address`] applies to a
416/// Binding response (the datagram source is not validated in isolation — see
417/// [`query_reflexive_address`]'s source check): a predictable id (e.g. one derived from wall-clock
418/// time) lets an off-path attacker who can approximate the send instant forge a `BINDING_SUCCESS`
419/// carrying a poisoned reflexive address before the real STUN server's reply arrives. Sourcing every
420/// bit from [`ring::rand::SystemRandom`] (already in the dependency tree via rustls) closes that.
421///
422/// `pub` so tests can assert directly on the id's statistical properties (see `tests/stun.rs`)
423/// without re-running the full network transaction.
424pub fn new_transaction_id() -> [u8; 12] {
425    use ring::rand::{SecureRandom, SystemRandom};
426
427    let mut id = [0u8; 12];
428    // `SystemRandom::fill` only fails on catastrophic RNG unavailability (e.g. no OS entropy
429    // source) — there is no sane fallback in that case, so we panic rather than silently degrade
430    // back to a predictable id (which would reopen exactly the vulnerability this fixes).
431    SystemRandom::new()
432        .fill(&mut id)
433        .expect("OS CSPRNG must be available to generate a STUN transaction id");
434    id
435}
436
437#[cfg(test)]
438mod reflexive_guard_tests {
439    //! Unit tests for the private [`is_usable_reflexive_addr`] guard (#1387). Covers every reject
440    //! category across BOTH families, and asserts private/CGNAT/ULA are ACCEPTED (not a blanket
441    //! `is_global` — see the function's doc-comment).
442    use super::is_usable_reflexive_addr;
443    use std::net::SocketAddr;
444
445    fn addr(s: &str) -> SocketAddr {
446        s.parse().expect("valid SocketAddr literal")
447    }
448
449    #[test]
450    fn accepts_genuinely_global_addresses() {
451        assert!(is_usable_reflexive_addr(&addr("1.1.1.1:443")));
452        assert!(is_usable_reflexive_addr(&addr("8.8.8.8:53")));
453        assert!(is_usable_reflexive_addr(&addr(
454            "[2606:4700:4700::1111]:443"
455        )));
456    }
457
458    #[test]
459    fn accepts_private_cgnat_and_ula() {
460        // NOT rejected: legitimate reflexive addresses on a LAN / behind CGNAT (#1062 e2e).
461        assert!(is_usable_reflexive_addr(&addr("192.168.1.5:9000")));
462        assert!(is_usable_reflexive_addr(&addr("10.0.0.7:9000")));
463        assert!(is_usable_reflexive_addr(&addr("172.16.5.5:9000")));
464        assert!(is_usable_reflexive_addr(&addr("100.64.0.1:9000"))); // CGNAT (RFC 6598)
465        assert!(is_usable_reflexive_addr(&addr("[fd00::1]:9000"))); // ULA (fc00::/7)
466    }
467
468    #[test]
469    fn rejects_port_zero() {
470        assert!(!is_usable_reflexive_addr(&addr("1.1.1.1:0")));
471        assert!(!is_usable_reflexive_addr(&addr("[2606:4700:4700::1111]:0")));
472    }
473
474    #[test]
475    fn rejects_reserved_ipv4() {
476        assert!(!is_usable_reflexive_addr(&addr("0.0.0.0:1234"))); // unspecified
477        assert!(!is_usable_reflexive_addr(&addr("127.0.0.1:1234"))); // loopback
478        assert!(!is_usable_reflexive_addr(&addr("169.254.1.1:1234"))); // link-local
479        assert!(!is_usable_reflexive_addr(&addr("224.0.0.1:1234"))); // multicast
480        assert!(!is_usable_reflexive_addr(&addr("255.255.255.255:1234"))); // broadcast
481        assert!(!is_usable_reflexive_addr(&addr("192.0.2.1:1234"))); // TEST-NET-1
482        assert!(!is_usable_reflexive_addr(&addr("198.51.100.1:1234"))); // TEST-NET-2
483        assert!(!is_usable_reflexive_addr(&addr("203.0.113.1:1234"))); // TEST-NET-3
484    }
485
486    #[test]
487    fn rejects_reserved_ipv6() {
488        assert!(!is_usable_reflexive_addr(&addr("[::]:1234"))); // unspecified
489        assert!(!is_usable_reflexive_addr(&addr("[::1]:1234"))); // loopback
490        assert!(!is_usable_reflexive_addr(&addr("[fe80::1]:1234"))); // link-local fe80::/10
491        assert!(!is_usable_reflexive_addr(&addr("[febf::1]:1234"))); // link-local upper edge
492        assert!(!is_usable_reflexive_addr(&addr("[ff02::1]:1234"))); // multicast ff00::/8
493        assert!(!is_usable_reflexive_addr(&addr("[2001:db8::1]:1234"))); // documentation 2001:db8::/32
494    }
495
496    #[test]
497    fn rejects_ipv4_mapped_and_compat_smuggling_reserved_ranges() {
498        // Bug 1: an on-path STUN server controls the 16 decoded bytes and could smuggle any rejected
499        // IPv4 range as an IPv4-mapped (`::ffff:a.b.c.d`) or deprecated IPv4-compat (`::a.b.c.d`)
500        // address. After to_canonical folding these MUST hit the V4 predicate and be rejected.
501        assert!(!is_usable_reflexive_addr(&addr("[::ffff:127.0.0.1]:1234"))); // mapped loopback
502        assert!(!is_usable_reflexive_addr(&addr(
503            "[::ffff:169.254.1.1]:1234"
504        ))); // mapped link-local
505        assert!(!is_usable_reflexive_addr(&addr("[::ffff:224.0.0.1]:1234"))); // mapped multicast
506        assert!(!is_usable_reflexive_addr(&addr("[::ffff:192.0.2.1]:1234"))); // mapped TEST-NET-1
507        assert!(!is_usable_reflexive_addr(&addr(
508            "[::ffff:255.255.255.255]:1234"
509        ))); // mapped broadcast
510        assert!(!is_usable_reflexive_addr(&addr("[::ffff:0.0.0.0]:1234"))); // mapped unspecified
511        assert!(!is_usable_reflexive_addr(&addr("[::7f00:1]:1234"))); // compat 127.0.0.1
512    }
513
514    #[test]
515    fn accepts_ipv4_mapped_private() {
516        // The accept-private design survives folding: a mapped private address is still ACCEPTED.
517        assert!(is_usable_reflexive_addr(&addr("[::ffff:10.0.0.1]:9000")));
518    }
519
520    #[test]
521    fn rejects_never_dialable_ipv4_ranges() {
522        // Bug 2: never-dialable ranges the stdlib predicates miss (their unstable helpers can't be
523        // called, so the masks are hand-rolled).
524        assert!(!is_usable_reflexive_addr(&addr("198.18.0.1:1234"))); // benchmarking 198.18.0.0/15
525        assert!(!is_usable_reflexive_addr(&addr("198.19.0.1:1234"))); // benchmarking upper half
526        assert!(!is_usable_reflexive_addr(&addr("240.0.0.1:1234"))); // reserved/class-E 240.0.0.0/4
527        assert!(!is_usable_reflexive_addr(&addr("0.1.2.3:1234"))); // this-network 0.0.0.0/8 non-zero host
528        assert!(!is_usable_reflexive_addr(&addr("192.88.99.1:1234"))); // 6to4 relay anycast
529    }
530}