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 attribute.
51    #[error("no mapped address in STUN response")]
52    NoMappedAddress,
53    /// The message type was not a Binding success response.
54    #[error("unexpected STUN message type: {0:#06x}")]
55    UnexpectedType(u16),
56    /// Underlying socket I/O error (stringified so [`StunError`] stays `Clone`/`Eq`).
57    #[error("STUN io: {0}")]
58    Io(String),
59    /// The transaction did not complete within the deadline.
60    #[error("STUN request timed out")]
61    Timeout,
62}
63
64/// A STUN Binding request: 20-byte header (type, length=0, cookie, 96-bit transaction id) and no
65/// attributes. `transaction_id` is caller-supplied so the response can be matched to the request.
66pub fn encode_binding_request(transaction_id: &[u8; 12]) -> Vec<u8> {
67    let mut msg = Vec::with_capacity(20);
68    msg.extend_from_slice(&BINDING_REQUEST.to_be_bytes()); // message type
69    msg.extend_from_slice(&0u16.to_be_bytes()); // message length (no attributes)
70    msg.extend_from_slice(&MAGIC_COOKIE.to_be_bytes()); // magic cookie
71    msg.extend_from_slice(transaction_id); // 96-bit transaction id
72    msg
73}
74
75/// Parse a STUN **Binding success response**, returning the reflexive [`SocketAddr`] from its
76/// `XOR-MAPPED-ADDRESS` (preferred) or legacy `MAPPED-ADDRESS` attribute.
77///
78/// Validates the magic cookie and (when `expected_txid` is `Some`) the transaction id, so a stale
79/// or spoofed datagram is rejected. Implements the XOR de-obfuscation of RFC 5389 §15.2.
80pub fn parse_binding_response(
81    msg: &[u8],
82    expected_txid: Option<&[u8; 12]>,
83) -> Result<SocketAddr, StunError> {
84    if msg.len() < 20 {
85        return Err(StunError::Truncated);
86    }
87    let msg_type = u16::from_be_bytes([msg[0], msg[1]]);
88    let msg_len = u16::from_be_bytes([msg[2], msg[3]]) as usize;
89    let cookie = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
90    if cookie != MAGIC_COOKIE {
91        return Err(StunError::BadMagicCookie);
92    }
93    if msg_type != BINDING_SUCCESS {
94        return Err(StunError::UnexpectedType(msg_type));
95    }
96    let txid: [u8; 12] = msg[8..20].try_into().map_err(|_| StunError::Truncated)?;
97    if let Some(expected) = expected_txid {
98        if &txid != expected {
99            return Err(StunError::TransactionIdMismatch);
100        }
101    }
102    if msg.len() < 20 + msg_len {
103        return Err(StunError::Truncated);
104    }
105
106    // Walk the TLV attributes. Prefer XOR-MAPPED-ADDRESS; fall back to MAPPED-ADDRESS.
107    let mut fallback: Option<SocketAddr> = None;
108    let mut off = 20usize;
109    let end = 20 + msg_len;
110    while off + 4 <= end {
111        let attr_type = u16::from_be_bytes([msg[off], msg[off + 1]]);
112        let attr_len = u16::from_be_bytes([msg[off + 2], msg[off + 3]]) as usize;
113        let val_start = off + 4;
114        let val_end = val_start + attr_len;
115        if val_end > end {
116            return Err(StunError::Truncated);
117        }
118        let value = &msg[val_start..val_end];
119        match attr_type {
120            ATTR_XOR_MAPPED_ADDRESS => {
121                return decode_mapped_address(value, &txid, true);
122            }
123            ATTR_MAPPED_ADDRESS if fallback.is_none() => {
124                fallback = decode_mapped_address(value, &txid, false).ok();
125            }
126            _ => {}
127        }
128        // Attributes are padded to a 4-byte boundary (RFC 5389 §15).
129        off = val_end + ((4 - (attr_len % 4)) % 4);
130    }
131    fallback.ok_or(StunError::NoMappedAddress)
132}
133
134/// Decode a (XOR-)MAPPED-ADDRESS attribute value into a [`SocketAddr`].
135///
136/// Layout (RFC 5389 §15.1/§15.2): `[reserved:1][family:1][port:2][address:4 or 16]`. When `xor` is
137/// set, the port is XORed with the top 16 bits of the magic cookie and the address is XORed with the
138/// full cookie (IPv4) or cookie‖transaction-id (IPv6).
139fn decode_mapped_address(
140    value: &[u8],
141    txid: &[u8; 12],
142    xor: bool,
143) -> Result<SocketAddr, StunError> {
144    if value.len() < 4 {
145        return Err(StunError::Truncated);
146    }
147    let family = value[1];
148    let raw_port = u16::from_be_bytes([value[2], value[3]]);
149    let cookie_be = MAGIC_COOKIE.to_be_bytes();
150    let port = if xor {
151        raw_port ^ ((MAGIC_COOKIE >> 16) as u16)
152    } else {
153        raw_port
154    };
155
156    match family {
157        FAMILY_IPV4 => {
158            if value.len() < 8 {
159                return Err(StunError::Truncated);
160            }
161            let mut octets = [value[4], value[5], value[6], value[7]];
162            if xor {
163                for (i, o) in octets.iter_mut().enumerate() {
164                    *o ^= cookie_be[i];
165                }
166            }
167            Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(octets)), port))
168        }
169        FAMILY_IPV6 => {
170            if value.len() < 20 {
171                return Err(StunError::Truncated);
172            }
173            let mut octets = [0u8; 16];
174            octets.copy_from_slice(&value[4..20]);
175            if xor {
176                // XOR key is the 32-bit cookie followed by the 96-bit transaction id.
177                let mut key = [0u8; 16];
178                key[..4].copy_from_slice(&cookie_be);
179                key[4..].copy_from_slice(txid);
180                for (o, k) in octets.iter_mut().zip(key.iter()) {
181                    *o ^= *k;
182                }
183            }
184            Ok(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), port))
185        }
186        other => Err(StunError::UnexpectedType(other as u16)),
187    }
188}
189
190/// Perform a single STUN Binding transaction against `server` over `socket`, returning the
191/// discovered reflexive (public) [`SocketAddr`] of `socket`. Bounded by `timeout`; a lost datagram
192/// surfaces as [`StunError::Timeout`] (the caller retries or falls through to the next method).
193///
194/// The `socket` should be the very UDP socket whose external mapping the caller wants to learn —
195/// the reflexive address is specific to the NAT binding created by *that* socket.
196///
197/// ## Anti-spoof: source address validation (#179 finding 2)
198///
199/// A UDP reply's source address is easy to check and hard for an off-path attacker to spoof
200/// (spoofing the source AND getting the reply routed back requires being on-path or the same
201/// network). This function therefore accepts a datagram only when it actually originates from
202/// `server`; anything else (a stray reply, a scan, an attacker racing a forged response) is
203/// discarded and the receive loop continues within the overall `timeout` deadline — a single
204/// mismatched-source datagram must not fail the whole transaction, since the real reply may still be
205/// in flight. This is independent, defense-in-depth hygiene alongside the transaction-id check
206/// ([`new_transaction_id`]); neither replaces the other.
207pub async fn query_reflexive_address(
208    socket: &UdpSocket,
209    server: SocketAddr,
210    timeout: Duration,
211) -> Result<SocketAddr, StunError> {
212    let txid = new_transaction_id();
213    let req = encode_binding_request(&txid);
214    socket
215        .send_to(&req, server)
216        .await
217        .map_err(|e| StunError::Io(e.to_string()))?;
218
219    let mut buf = [0u8; 512];
220    let deadline = tokio::time::Instant::now() + timeout;
221    loop {
222        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
223        if remaining.is_zero() {
224            return Err(StunError::Timeout);
225        }
226        let (n, from) = match tokio::time::timeout(remaining, socket.recv_from(&mut buf)).await {
227            Ok(Ok(x)) => x,
228            Ok(Err(e)) => return Err(StunError::Io(e.to_string())),
229            Err(_) => return Err(StunError::Timeout),
230        };
231        if from != server {
232            // Not from the queried server — ignore and keep waiting for the genuine reply.
233            continue;
234        }
235        return parse_binding_response(&buf[..n], Some(&txid));
236    }
237}
238
239/// Generate a 96-bit STUN transaction id from a CSPRNG (RFC 5389 §10.1: "It primarily serves to
240/// correlate requests with responses... **and MUST be uniformly and randomly chosen from the
241/// interval 0 .. 2**96 - 1, and SHOULD be cryptographically random").
242///
243/// The transaction id is the ONLY anti-spoof mechanism [`query_reflexive_address`] applies to a
244/// Binding response (the datagram source is not validated in isolation — see
245/// [`query_reflexive_address`]'s source check): a predictable id (e.g. one derived from wall-clock
246/// time) lets an off-path attacker who can approximate the send instant forge a `BINDING_SUCCESS`
247/// carrying a poisoned reflexive address before the real STUN server's reply arrives. Sourcing every
248/// bit from [`ring::rand::SystemRandom`] (already in the dependency tree via rustls) closes that.
249///
250/// `pub` so tests can assert directly on the id's statistical properties (see `tests/stun.rs`)
251/// without re-running the full network transaction.
252pub fn new_transaction_id() -> [u8; 12] {
253    use ring::rand::{SecureRandom, SystemRandom};
254
255    let mut id = [0u8; 12];
256    // `SystemRandom::fill` only fails on catastrophic RNG unavailability (e.g. no OS entropy
257    // source) — there is no sane fallback in that case, so we panic rather than silently degrade
258    // back to a predictable id (which would reopen exactly the vulnerability this fixes).
259    SystemRandom::new()
260        .fill(&mut id)
261        .expect("OS CSPRNG must be available to generate a STUN transaction id");
262    id
263}