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}