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 // NOTE: `::1` (IPv6 loopback) never reaches here — `to_ipv4()` folds it to `0.0.0.1`, which the
149 // v4 `0.0.0.0/8` "this-network" rule in `is_usable_reflexive_v4` rejects. That v4 rule is
150 // therefore LOAD-BEARING for this predicate; do NOT drop it as "redundant" in a future cleanup.
151 let seg = v6.segments();
152 // fe80::/10 — link-local unicast (`is_unicast_link_local` is unstable, check manually).
153 let is_link_local = (seg[0] & 0xffc0) == 0xfe80;
154 // 2001:db8::/32 — documentation (`is_documentation` is unstable, check manually).
155 let is_documentation = seg[0] == 0x2001 && seg[1] == 0x0db8;
156 !v6.is_unspecified()
157 && !v6.is_loopback()
158 && !v6.is_multicast()
159 && !is_link_local
160 && !is_documentation
161}
162
163/// Parse a STUN **Binding success response**, returning the reflexive [`SocketAddr`] from its
164/// `XOR-MAPPED-ADDRESS` (preferred) or legacy `MAPPED-ADDRESS` attribute.
165///
166/// Validates the magic cookie and (when `expected_txid` is `Some`) the transaction id, so a stale
167/// or spoofed datagram is rejected. Implements the XOR de-obfuscation of RFC 5389 §15.2.
168///
169/// This is a PURE parser: it does NOT check whether the returned address is a usable
170/// (non-reserved, globally/LAN-dialable) reflexive candidate. A caller wanting a usable
171/// server-reflexive candidate should use [`query_reflexive_address`], which applies the
172/// [`is_usable_reflexive_addr`] guard on top of parsing.
173pub fn parse_binding_response(
174 msg: &[u8],
175 expected_txid: Option<&[u8; 12]>,
176) -> Result<SocketAddr, StunError> {
177 if msg.len() < 20 {
178 return Err(StunError::Truncated);
179 }
180 let msg_type = u16::from_be_bytes([msg[0], msg[1]]);
181 let msg_len = u16::from_be_bytes([msg[2], msg[3]]) as usize;
182 let cookie = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
183 if cookie != MAGIC_COOKIE {
184 return Err(StunError::BadMagicCookie);
185 }
186 if msg_type != BINDING_SUCCESS {
187 return Err(StunError::UnexpectedType(msg_type));
188 }
189 let txid: [u8; 12] = msg[8..20].try_into().map_err(|_| StunError::Truncated)?;
190 if let Some(expected) = expected_txid {
191 if &txid != expected {
192 return Err(StunError::TransactionIdMismatch);
193 }
194 }
195 if msg.len() < 20 + msg_len {
196 return Err(StunError::Truncated);
197 }
198
199 // Walk the TLV attributes. Prefer XOR-MAPPED-ADDRESS; fall back to MAPPED-ADDRESS.
200 let mut fallback: Option<SocketAddr> = None;
201 let mut off = 20usize;
202 let end = 20 + msg_len;
203 while off + 4 <= end {
204 let attr_type = u16::from_be_bytes([msg[off], msg[off + 1]]);
205 let attr_len = u16::from_be_bytes([msg[off + 2], msg[off + 3]]) as usize;
206 let val_start = off + 4;
207 let val_end = val_start + attr_len;
208 if val_end > end {
209 return Err(StunError::Truncated);
210 }
211 let value = &msg[val_start..val_end];
212 match attr_type {
213 ATTR_XOR_MAPPED_ADDRESS => {
214 return decode_mapped_address(value, &txid, true);
215 }
216 ATTR_MAPPED_ADDRESS if fallback.is_none() => {
217 fallback = decode_mapped_address(value, &txid, false).ok();
218 }
219 _ => {}
220 }
221 // Attributes are padded to a 4-byte boundary (RFC 5389 §15).
222 off = val_end + ((4 - (attr_len % 4)) % 4);
223 }
224 fallback.ok_or(StunError::NoMappedAddress)
225}
226
227/// Decode a (XOR-)MAPPED-ADDRESS attribute value into a [`SocketAddr`].
228///
229/// Layout (RFC 5389 §15.1/§15.2): `[reserved:1][family:1][port:2][address:4 or 16]`. When `xor` is
230/// set, the port is XORed with the top 16 bits of the magic cookie and the address is XORed with the
231/// full cookie (IPv4) or cookie‖transaction-id (IPv6).
232fn decode_mapped_address(
233 value: &[u8],
234 txid: &[u8; 12],
235 xor: bool,
236) -> Result<SocketAddr, StunError> {
237 if value.len() < 4 {
238 return Err(StunError::Truncated);
239 }
240 let family = value[1];
241 let raw_port = u16::from_be_bytes([value[2], value[3]]);
242 let cookie_be = MAGIC_COOKIE.to_be_bytes();
243 let port = if xor {
244 raw_port ^ ((MAGIC_COOKIE >> 16) as u16)
245 } else {
246 raw_port
247 };
248
249 match family {
250 FAMILY_IPV4 => {
251 if value.len() < 8 {
252 return Err(StunError::Truncated);
253 }
254 let mut octets = [value[4], value[5], value[6], value[7]];
255 if xor {
256 for (i, o) in octets.iter_mut().enumerate() {
257 *o ^= cookie_be[i];
258 }
259 }
260 Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(octets)), port))
261 }
262 FAMILY_IPV6 => {
263 if value.len() < 20 {
264 return Err(StunError::Truncated);
265 }
266 let mut octets = [0u8; 16];
267 octets.copy_from_slice(&value[4..20]);
268 if xor {
269 // XOR key is the 32-bit cookie followed by the 96-bit transaction id.
270 let mut key = [0u8; 16];
271 key[..4].copy_from_slice(&cookie_be);
272 key[4..].copy_from_slice(txid);
273 for (o, k) in octets.iter_mut().zip(key.iter()) {
274 *o ^= *k;
275 }
276 }
277 Ok(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), port))
278 }
279 other => Err(StunError::UnexpectedType(other as u16)),
280 }
281}
282
283/// Perform a single STUN Binding transaction against `server` over `socket`, returning the
284/// discovered reflexive (public) [`SocketAddr`] of `socket`. Bounded by `timeout`; a lost datagram
285/// surfaces as [`StunError::Timeout`] (the caller retries or falls through to the next method).
286///
287/// This is **THE API to obtain a DIALABLE server-reflexive candidate**: it returns the reflexive
288/// `ip:port` mapping of the caller's OWN listen socket, so the port is the real external binding a
289/// remote peer can dial (unlike [`discover_reflexive_address`], which learns the public IP over a
290/// throwaway ephemeral socket — see its `## Port caveat`). Connectivity-core (dig-node) should STUN
291/// from its actual listen socket via this function to advertise a dialable candidate.
292///
293/// The `socket` should be the very UDP socket whose external mapping the caller wants to learn —
294/// the reflexive address is specific to the NAT binding created by *that* socket.
295///
296/// The returned address is checked against [`is_usable_reflexive_addr`]: a parsed-but-unusable
297/// (non-global/reserved) reflexive address is rejected as [`StunError::NoMappedAddress`] (#1387).
298///
299/// ## Anti-spoof: source address validation (#179 finding 2)
300///
301/// A UDP reply's source address is easy to check and hard for an off-path attacker to spoof
302/// (spoofing the source AND getting the reply routed back requires being on-path or the same
303/// network). This function therefore accepts a datagram only when it actually originates from
304/// `server`; anything else (a stray reply, a scan, an attacker racing a forged response) is
305/// discarded and the receive loop continues within the overall `timeout` deadline — a single
306/// mismatched-source datagram must not fail the whole transaction, since the real reply may still be
307/// in flight. This is independent, defense-in-depth hygiene alongside the transaction-id check
308/// ([`new_transaction_id`]); neither replaces the other.
309pub async fn query_reflexive_address(
310 socket: &UdpSocket,
311 server: SocketAddr,
312 timeout: Duration,
313) -> Result<SocketAddr, StunError> {
314 let txid = new_transaction_id();
315 let req = encode_binding_request(&txid);
316 socket
317 .send_to(&req, server)
318 .await
319 .map_err(|e| StunError::Io(e.to_string()))?;
320
321 let mut buf = [0u8; 512];
322 let deadline = tokio::time::Instant::now() + timeout;
323 loop {
324 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
325 if remaining.is_zero() {
326 return Err(StunError::Timeout);
327 }
328 let (n, from) = match tokio::time::timeout(remaining, socket.recv_from(&mut buf)).await {
329 Ok(Ok(x)) => x,
330 Ok(Err(e)) => return Err(StunError::Io(e.to_string())),
331 Err(_) => return Err(StunError::Timeout),
332 };
333 if from != server {
334 // Not from the queried server — ignore and keep waiting for the genuine reply.
335 continue;
336 }
337 let addr = parse_binding_response(&buf[..n], Some(&txid))?;
338 // Defense-in-depth (#1387): a malicious/misconfigured STUN server can return a bogus
339 // reflexive address (loopback, multicast, a documentation range, port 0, …) that we would
340 // otherwise advertise. Reject any address that is not a usable reflexive candidate; the
341 // caller (or `discover_reflexive_address`) then falls through to the next candidate.
342 if !is_usable_reflexive_addr(&addr) {
343 return Err(StunError::NoMappedAddress);
344 }
345 return Ok(addr);
346 }
347}
348
349/// Discover this node's server-reflexive (public) address via STUN, IPv6-first with IPv4 FALLBACK
350/// (CLAUDE.md §5.2). `stun_servers` are the resolved STUN endpoints across BOTH families (e.g. every
351/// A + AAAA record of `<relay-host>:3478` — the caller MUST NOT pre-collapse to one family). The STUN
352/// Binding transaction is raced over the local∩server family intersection via [`dig_ip::connect`]:
353/// IPv6 is attempted first and IPv4 is used as a fallback when the IPv6 STUN server is unreachable —
354/// the reflexive address is NEVER nulled just because the IPv6 STUN server did not respond. Returns
355/// the discovered reflexive [`SocketAddr`], or `None` when no family's STUN server answered.
356///
357/// This is the canonical front-door fix for the #1062 gap: consumers (dig-node) MUST call this
358/// instead of hand-rolling a family sort or collapsing `to_socket_addrs()` to a single family — the
359/// happy-eyeballs racer and the local∩server intersection live here, in ONE place, per the dig-ip
360/// charter ("NO repo hand-rolls a family sort or happy-eyeballs racer").
361///
362/// ## Port caveat
363///
364/// Each candidate is STUNed over a THROWAWAY ephemeral UDP socket bound just for that transaction,
365/// so the returned IP is the node's stable public IP but the **PORT is that throwaway socket's NAT
366/// binding — NOT reliably dialable** under most NAT types (a remote peer dialing it will usually
367/// fail). Use this to learn the public IP; for a DIALABLE server-reflexive candidate, STUN from
368/// your ACTUAL listen socket via [`query_reflexive_address`] instead.
369pub async fn discover_reflexive_address(
370 stun_servers: &[SocketAddr],
371 local: dig_ip::LocalStack,
372 timeout: Duration,
373) -> Option<SocketAddr> {
374 if stun_servers.is_empty() {
375 return None;
376 }
377
378 let mut candidates = dig_ip::PeerCandidates::new();
379 candidates.extend(
380 stun_servers.iter().copied(),
381 dig_ip::CandidateSource::StunReflexive,
382 );
383
384 let config = dig_ip::DialConfig {
385 per_attempt_timeout: timeout,
386 ..Default::default()
387 };
388
389 // One "dial" == one full STUN Binding transaction against a candidate server. We bind an
390 // ephemeral UDP socket in the SERVER's family (dig-ip only hands us a family the local host can
391 // originate on) and learn that socket's reflexive mapping. The racer returns the first family's
392 // successful reflexive address, preferring IPv6.
393 let winner = dig_ip::connect(&local, &candidates, config, |stun_addr| async move {
394 let bind: SocketAddr = if stun_addr.is_ipv6() {
395 (Ipv6Addr::UNSPECIFIED, 0).into()
396 } else {
397 (Ipv4Addr::UNSPECIFIED, 0).into()
398 };
399 let socket = UdpSocket::bind(bind)
400 .await
401 .map_err(|e| format!("bind {bind}: {e}"))?;
402 query_reflexive_address(&socket, stun_addr, timeout)
403 .await
404 .map_err(|e| e.to_string())
405 })
406 .await;
407
408 match winner {
409 Ok(w) => Some(w.conn),
410 Err(_) => None,
411 }
412}
413
414/// Generate a 96-bit STUN transaction id from a CSPRNG (RFC 5389 §10.1: "It primarily serves to
415/// correlate requests with responses... **and MUST be uniformly and randomly chosen from the
416/// interval 0 .. 2**96 - 1, and SHOULD be cryptographically random").
417///
418/// The transaction id is the ONLY anti-spoof mechanism [`query_reflexive_address`] applies to a
419/// Binding response (the datagram source is not validated in isolation — see
420/// [`query_reflexive_address`]'s source check): a predictable id (e.g. one derived from wall-clock
421/// time) lets an off-path attacker who can approximate the send instant forge a `BINDING_SUCCESS`
422/// carrying a poisoned reflexive address before the real STUN server's reply arrives. Sourcing every
423/// bit from [`ring::rand::SystemRandom`] (already in the dependency tree via rustls) closes that.
424///
425/// `pub` so tests can assert directly on the id's statistical properties (see `tests/stun.rs`)
426/// without re-running the full network transaction.
427pub fn new_transaction_id() -> [u8; 12] {
428 use ring::rand::{SecureRandom, SystemRandom};
429
430 let mut id = [0u8; 12];
431 // `SystemRandom::fill` only fails on catastrophic RNG unavailability (e.g. no OS entropy
432 // source) — there is no sane fallback in that case, so we panic rather than silently degrade
433 // back to a predictable id (which would reopen exactly the vulnerability this fixes).
434 SystemRandom::new()
435 .fill(&mut id)
436 .expect("OS CSPRNG must be available to generate a STUN transaction id");
437 id
438}
439
440#[cfg(test)]
441mod reflexive_guard_tests {
442 //! Unit tests for the private [`is_usable_reflexive_addr`] guard (#1387). Covers every reject
443 //! category across BOTH families, and asserts private/CGNAT/ULA are ACCEPTED (not a blanket
444 //! `is_global` — see the function's doc-comment).
445 use super::is_usable_reflexive_addr;
446 use std::net::SocketAddr;
447
448 fn addr(s: &str) -> SocketAddr {
449 s.parse().expect("valid SocketAddr literal")
450 }
451
452 #[test]
453 fn accepts_genuinely_global_addresses() {
454 assert!(is_usable_reflexive_addr(&addr("1.1.1.1:443")));
455 assert!(is_usable_reflexive_addr(&addr("8.8.8.8:53")));
456 assert!(is_usable_reflexive_addr(&addr(
457 "[2606:4700:4700::1111]:443"
458 )));
459 }
460
461 #[test]
462 fn accepts_private_cgnat_and_ula() {
463 // NOT rejected: legitimate reflexive addresses on a LAN / behind CGNAT (#1062 e2e).
464 assert!(is_usable_reflexive_addr(&addr("192.168.1.5:9000")));
465 assert!(is_usable_reflexive_addr(&addr("10.0.0.7:9000")));
466 assert!(is_usable_reflexive_addr(&addr("172.16.5.5:9000")));
467 assert!(is_usable_reflexive_addr(&addr("100.64.0.1:9000"))); // CGNAT (RFC 6598)
468 assert!(is_usable_reflexive_addr(&addr("[fd00::1]:9000"))); // ULA (fc00::/7)
469 }
470
471 #[test]
472 fn rejects_port_zero() {
473 assert!(!is_usable_reflexive_addr(&addr("1.1.1.1:0")));
474 assert!(!is_usable_reflexive_addr(&addr("[2606:4700:4700::1111]:0")));
475 }
476
477 #[test]
478 fn rejects_reserved_ipv4() {
479 assert!(!is_usable_reflexive_addr(&addr("0.0.0.0:1234"))); // unspecified
480 assert!(!is_usable_reflexive_addr(&addr("127.0.0.1:1234"))); // loopback
481 assert!(!is_usable_reflexive_addr(&addr("169.254.1.1:1234"))); // link-local
482 assert!(!is_usable_reflexive_addr(&addr("224.0.0.1:1234"))); // multicast
483 assert!(!is_usable_reflexive_addr(&addr("255.255.255.255:1234"))); // broadcast
484 assert!(!is_usable_reflexive_addr(&addr("192.0.2.1:1234"))); // TEST-NET-1
485 assert!(!is_usable_reflexive_addr(&addr("198.51.100.1:1234"))); // TEST-NET-2
486 assert!(!is_usable_reflexive_addr(&addr("203.0.113.1:1234"))); // TEST-NET-3
487 }
488
489 #[test]
490 fn rejects_reserved_ipv6() {
491 assert!(!is_usable_reflexive_addr(&addr("[::]:1234"))); // unspecified
492 assert!(!is_usable_reflexive_addr(&addr("[::1]:1234"))); // loopback
493 assert!(!is_usable_reflexive_addr(&addr("[fe80::1]:1234"))); // link-local fe80::/10
494 assert!(!is_usable_reflexive_addr(&addr("[febf::1]:1234"))); // link-local upper edge
495 assert!(!is_usable_reflexive_addr(&addr("[ff02::1]:1234"))); // multicast ff00::/8
496 assert!(!is_usable_reflexive_addr(&addr("[2001:db8::1]:1234"))); // documentation 2001:db8::/32
497 }
498
499 #[test]
500 fn rejects_ipv4_mapped_and_compat_smuggling_reserved_ranges() {
501 // Bug 1: an on-path STUN server controls the 16 decoded bytes and could smuggle any rejected
502 // IPv4 range as an IPv4-mapped (`::ffff:a.b.c.d`) or deprecated IPv4-compat (`::a.b.c.d`)
503 // address. After to_ipv4 folding these MUST hit the V4 predicate and be rejected.
504 assert!(!is_usable_reflexive_addr(&addr("[::ffff:127.0.0.1]:1234"))); // mapped loopback
505 assert!(!is_usable_reflexive_addr(&addr(
506 "[::ffff:169.254.1.1]:1234"
507 ))); // mapped link-local
508 assert!(!is_usable_reflexive_addr(&addr("[::ffff:224.0.0.1]:1234"))); // mapped multicast
509 assert!(!is_usable_reflexive_addr(&addr("[::ffff:192.0.2.1]:1234"))); // mapped TEST-NET-1
510 assert!(!is_usable_reflexive_addr(&addr(
511 "[::ffff:255.255.255.255]:1234"
512 ))); // mapped broadcast
513 assert!(!is_usable_reflexive_addr(&addr("[::ffff:0.0.0.0]:1234"))); // mapped unspecified
514 assert!(!is_usable_reflexive_addr(&addr("[::7f00:1]:1234"))); // compat 127.0.0.1
515 }
516
517 #[test]
518 fn accepts_ipv4_mapped_private() {
519 // The accept-private design survives folding: a mapped private address is still ACCEPTED.
520 assert!(is_usable_reflexive_addr(&addr("[::ffff:10.0.0.1]:9000")));
521 }
522
523 #[test]
524 fn rejects_never_dialable_ipv4_ranges() {
525 // Bug 2: never-dialable ranges the stdlib predicates miss (their unstable helpers can't be
526 // called, so the masks are hand-rolled).
527 assert!(!is_usable_reflexive_addr(&addr("198.18.0.1:1234"))); // benchmarking 198.18.0.0/15
528 assert!(!is_usable_reflexive_addr(&addr("198.19.0.1:1234"))); // benchmarking upper half
529 assert!(!is_usable_reflexive_addr(&addr("240.0.0.1:1234"))); // reserved/class-E 240.0.0.0/4
530 assert!(!is_usable_reflexive_addr(&addr("0.1.2.3:1234"))); // this-network 0.0.0.0/8 non-zero host
531 assert!(!is_usable_reflexive_addr(&addr("192.88.99.1:1234"))); // 6to4 relay anycast
532 }
533}