dig_stun/codec.rs
1//! The RFC 5389 Binding message wire format, both directions (`SPEC.md` §2).
2//!
3//! A Binding transaction is a fixed 20-byte header followed by TLV attributes. This module encodes
4//! and parses that layout with no network I/O — every branch is unit-testable against the RFC byte
5//! layout alone. [`encode_binding_request`] / [`parse_binding_response`] are the CLIENT-side halves,
6//! extracted byte-for-byte from `dig-nat 0.21.1` `src/stun.rs` (`SPEC.md` §2 cites the exact lines).
7//! [`parse_binding_request`] / [`encode_binding_success`] are the SERVER-side halves this crate adds.
8
9use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
10
11use crate::scope::fold_ip;
12
13/// STUN magic cookie (RFC 5389 §6). Always the first 4 bytes after the message type + length.
14pub const MAGIC_COOKIE: u32 = 0x2112_A442;
15
16/// Binding request message type (RFC 5389 §6 — method Binding = 0x001, class Request = 0b00).
17pub const BINDING_REQUEST: u16 = 0x0001;
18/// Binding success response message type (method Binding, class Success = 0b10).
19pub const BINDING_SUCCESS: u16 = 0x0101;
20
21/// `XOR-MAPPED-ADDRESS` attribute type (RFC 5389 §15.2).
22pub const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
23/// Legacy `MAPPED-ADDRESS` attribute type (RFC 5389 §15.1) — some servers still emit it.
24pub const ATTR_MAPPED_ADDRESS: u16 = 0x0001;
25
26/// Address family markers inside a (XOR-)MAPPED-ADDRESS attribute.
27const FAMILY_IPV4: u8 = 0x01;
28const FAMILY_IPV6: u8 = 0x02;
29
30/// The 96-bit STUN transaction id. A plain array alias (not a newtype) so `dig-nat`'s re-exported
31/// signatures stay unchanged for its existing consumers (`SPEC.md` §2.1, §8.2).
32pub type TransactionId = [u8; 12];
33
34/// Errors decoding a STUN message or performing a Binding transaction (`SPEC.md` §2.8).
35///
36/// Exhaustive and NOT `#[non_exhaustive]`: `dig-nat` re-exports this type and some of its consumers
37/// match on it exhaustively, so adding a variant is a breaking change tracked by SemVer rather than
38/// absorbed silently.
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 (for [`crate::query_reflexive_address`] only) a parsed address that
52 /// failed the reflexive-usability guard (`SPEC.md` §5 — e.g. loopback, link-local, multicast, a
53 /// documentation range, or `port == 0`).
54 #[error("no usable mapped address in STUN response")]
55 NoMappedAddress,
56 /// The message type was not the one the caller expected (a Binding success response when
57 /// parsing a response; a Binding request when parsing a request), or an attribute's address
58 /// family byte was neither IPv4 nor IPv6.
59 #[error("unexpected STUN message type: {0:#06x}")]
60 UnexpectedType(u16),
61 /// Underlying socket I/O error (stringified so [`StunError`] stays `Clone`/`Eq`).
62 #[error("STUN io: {0}")]
63 Io(String),
64 /// The transaction did not complete within the deadline.
65 #[error("STUN request timed out")]
66 Timeout,
67}
68
69/// Encode a STUN **Binding request**: a 20-byte header (type, length = 0, cookie, the 96-bit
70/// transaction id) and no attributes. `transaction_id` is caller-supplied so the response can later
71/// be matched to this request (`SPEC.md` §2.3).
72///
73/// Golden vector: for id `00 01 02 03 04 05 06 07 08 09 0a 0b` this returns
74/// `00 01 00 00 21 12 a4 42 00 01 02 03 04 05 06 07 08 09 0a 0b` (20 bytes) — proven byte-exact in
75/// `tests/codec.rs`.
76pub fn encode_binding_request(transaction_id: &TransactionId) -> Vec<u8> {
77 let mut msg = Vec::with_capacity(20);
78 msg.extend_from_slice(&BINDING_REQUEST.to_be_bytes());
79 msg.extend_from_slice(&0u16.to_be_bytes()); // message length: no attributes
80 msg.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
81 msg.extend_from_slice(transaction_id);
82 msg
83}
84
85/// Parse a STUN **Binding success response**, returning the reflexive [`SocketAddr`] from its
86/// `XOR-MAPPED-ADDRESS` (preferred) or legacy `MAPPED-ADDRESS` attribute (`SPEC.md` §2.4).
87///
88/// A PURE parser: it does NOT apply the address-usability guard (`SPEC.md` §5) — that is
89/// [`crate::query_reflexive_address`]'s job, layered on top of this parse.
90///
91/// Validates, in order: length ≥ 20 ([`StunError::Truncated`]); the magic cookie (checked BEFORE
92/// the message type, so a non-STUN datagram is never misreported as an unexpected STUN type); the
93/// message type is [`BINDING_SUCCESS`]; when `expected_txid` is `Some`, the transaction id matches;
94/// the declared message length fits the datagram; then walks the TLV attributes, returning the
95/// FIRST `XOR-MAPPED-ADDRESS` as soon as one is seen, else the first `MAPPED-ADDRESS`, else
96/// [`StunError::NoMappedAddress`].
97pub fn parse_binding_response(
98 msg: &[u8],
99 expected_txid: Option<&TransactionId>,
100) -> Result<SocketAddr, StunError> {
101 if msg.len() < 20 {
102 return Err(StunError::Truncated);
103 }
104 let msg_type = u16::from_be_bytes([msg[0], msg[1]]);
105 let msg_len = u16::from_be_bytes([msg[2], msg[3]]) as usize;
106 let cookie = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
107 if cookie != MAGIC_COOKIE {
108 return Err(StunError::BadMagicCookie);
109 }
110 if msg_type != BINDING_SUCCESS {
111 return Err(StunError::UnexpectedType(msg_type));
112 }
113 let txid: TransactionId = msg[8..20].try_into().map_err(|_| StunError::Truncated)?;
114 if let Some(expected) = expected_txid {
115 if &txid != expected {
116 return Err(StunError::TransactionIdMismatch);
117 }
118 }
119 if msg.len() < 20 + msg_len {
120 return Err(StunError::Truncated);
121 }
122
123 // Walk the TLV attributes. Prefer XOR-MAPPED-ADDRESS; fall back to MAPPED-ADDRESS.
124 let mut fallback: Option<SocketAddr> = None;
125 let mut off = 20usize;
126 let end = 20 + msg_len;
127 while off + 4 <= end {
128 let attr_type = u16::from_be_bytes([msg[off], msg[off + 1]]);
129 let attr_len = u16::from_be_bytes([msg[off + 2], msg[off + 3]]) as usize;
130 let val_start = off + 4;
131 let val_end = val_start + attr_len;
132 if val_end > end {
133 return Err(StunError::Truncated);
134 }
135 let value = &msg[val_start..val_end];
136 match attr_type {
137 ATTR_XOR_MAPPED_ADDRESS => {
138 return decode_mapped_address(value, &txid, true);
139 }
140 ATTR_MAPPED_ADDRESS if fallback.is_none() => {
141 fallback = decode_mapped_address(value, &txid, false).ok();
142 }
143 _ => {}
144 }
145 // Attributes are padded to a 4-byte boundary (RFC 5389 §15).
146 off = val_end + ((4 - (attr_len % 4)) % 4);
147 }
148 fallback.ok_or(StunError::NoMappedAddress)
149}
150
151/// Parse a STUN **Binding request** datagram, returning its transaction id (`SPEC.md` §2.5). The
152/// server-side counterpart of [`parse_binding_response`], used to answer a peer or the operator/
153/// relay/public UDP tiers with [`encode_binding_success`].
154///
155/// Validates, in order: length ≥ 20 ([`StunError::Truncated`]); the magic cookie
156/// ([`StunError::BadMagicCookie`]); the message type is EXACTLY [`BINDING_REQUEST`]
157/// ([`StunError::UnexpectedType`] otherwise — this single equality check also rejects a message
158/// whose top two type bits are non-zero, since [`BINDING_REQUEST`]'s own encoding has them clear,
159/// so no STUN method or class other than Binding+Request is accepted here; a caller wanting RFC
160/// 5389's "silently ignore anything else" latitude does so by not replying to that error); the
161/// declared message length fits the datagram. Attributes present on the request (e.g. `SOFTWARE`)
162/// are accepted and ignored — nothing here needs them.
163pub fn parse_binding_request(datagram: &[u8]) -> Result<TransactionId, StunError> {
164 if datagram.len() < 20 {
165 return Err(StunError::Truncated);
166 }
167 let msg_type = u16::from_be_bytes([datagram[0], datagram[1]]);
168 let msg_len = u16::from_be_bytes([datagram[2], datagram[3]]) as usize;
169 let cookie = u32::from_be_bytes([datagram[4], datagram[5], datagram[6], datagram[7]]);
170 if cookie != MAGIC_COOKIE {
171 return Err(StunError::BadMagicCookie);
172 }
173 if msg_type != BINDING_REQUEST {
174 return Err(StunError::UnexpectedType(msg_type));
175 }
176 if datagram.len() < 20 + msg_len {
177 return Err(StunError::Truncated);
178 }
179 Ok(datagram[8..20]
180 .try_into()
181 .expect("slice of exactly 12 bytes"))
182}
183
184/// Encode a STUN **Binding success response** carrying `reflexive` in one `XOR-MAPPED-ADDRESS`
185/// attribute and nothing else — no `MAPPED-ADDRESS`, no `SOFTWARE`, no `FINGERPRINT` (`SPEC.md`
186/// §2.7). The server-side counterpart of [`parse_binding_response`].
187///
188/// `reflexive`'s IP is folded per `SPEC.md` §5.3 (`fold_ip`) BEFORE encoding, so an IPv4-mapped
189/// IPv6 address (`::ffff:a.b.c.d`) is encoded as family `0x01` carrying the embedded IPv4 address,
190/// never as a 16-byte family `0x02` value — answering an IPv4 caller with a 16-byte address is
191/// exactly the family-crossing defect measured on `relay.dig.net` (relay.dig.net#11). The port is
192/// carried unchanged; only the IP is folded.
193///
194/// `parse_binding_response(&encode_binding_success(id, a), Some(id)) == Ok(a)` for every
195/// `SocketAddr` `a` whose IP is native IPv4 or native IPv6 — proven in `tests/codec.rs` as the
196/// round-trip law `SPEC.md` §2.7 requires.
197pub fn encode_binding_success(transaction_id: &TransactionId, reflexive: SocketAddr) -> Vec<u8> {
198 let folded = SocketAddr::new(fold_ip(reflexive.ip()), reflexive.port());
199 let cookie_be = MAGIC_COOKIE.to_be_bytes();
200 let xor_port = folded.port() ^ ((MAGIC_COOKIE >> 16) as u16);
201
202 let mut value = Vec::new();
203 value.push(0); // reserved
204 match folded.ip() {
205 IpAddr::V4(v4) => {
206 value.push(FAMILY_IPV4);
207 value.extend_from_slice(&xor_port.to_be_bytes());
208 let mut octets = v4.octets();
209 for (i, o) in octets.iter_mut().enumerate() {
210 *o ^= cookie_be[i];
211 }
212 value.extend_from_slice(&octets);
213 }
214 IpAddr::V6(v6) => {
215 value.push(FAMILY_IPV6);
216 value.extend_from_slice(&xor_port.to_be_bytes());
217 let mut octets = v6.octets();
218 let mut key = [0u8; 16];
219 key[..4].copy_from_slice(&cookie_be);
220 key[4..].copy_from_slice(transaction_id);
221 for (o, k) in octets.iter_mut().zip(key.iter()) {
222 *o ^= *k;
223 }
224 value.extend_from_slice(&octets);
225 }
226 }
227 // Both possible value lengths (8 for IPv4, 20 for IPv6) are already 4-byte aligned, so the
228 // attribute never needs padding — unlike the general TLV walk in `parse_binding_response`.
229 debug_assert_eq!(
230 value.len() % 4,
231 0,
232 "XOR-MAPPED-ADDRESS value must be 4-byte aligned"
233 );
234
235 let mut attr = Vec::with_capacity(4 + value.len());
236 attr.extend_from_slice(&ATTR_XOR_MAPPED_ADDRESS.to_be_bytes());
237 attr.extend_from_slice(&(value.len() as u16).to_be_bytes());
238 attr.extend_from_slice(&value);
239
240 let mut msg = Vec::with_capacity(20 + attr.len());
241 msg.extend_from_slice(&BINDING_SUCCESS.to_be_bytes());
242 msg.extend_from_slice(&(attr.len() as u16).to_be_bytes());
243 msg.extend_from_slice(&cookie_be);
244 msg.extend_from_slice(transaction_id);
245 msg.extend_from_slice(&attr);
246 msg
247}
248
249/// Decode a (XOR-)MAPPED-ADDRESS attribute value into a [`SocketAddr`] (RFC 5389 §15.1/§15.2).
250///
251/// Layout: `[reserved:1][family:1][port:2][address:4 or 16]`. When `xor` is set, the port is XORed
252/// with the top 16 bits of the magic cookie and the address is XORed with the full cookie (IPv4) or
253/// cookie‖transaction-id (IPv6).
254fn decode_mapped_address(
255 value: &[u8],
256 txid: &TransactionId,
257 xor: bool,
258) -> Result<SocketAddr, StunError> {
259 if value.len() < 4 {
260 return Err(StunError::Truncated);
261 }
262 let family = value[1];
263 let raw_port = u16::from_be_bytes([value[2], value[3]]);
264 let cookie_be = MAGIC_COOKIE.to_be_bytes();
265 let port = if xor {
266 raw_port ^ ((MAGIC_COOKIE >> 16) as u16)
267 } else {
268 raw_port
269 };
270
271 match family {
272 FAMILY_IPV4 => {
273 if value.len() < 8 {
274 return Err(StunError::Truncated);
275 }
276 let mut octets = [value[4], value[5], value[6], value[7]];
277 if xor {
278 for (i, o) in octets.iter_mut().enumerate() {
279 *o ^= cookie_be[i];
280 }
281 }
282 Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(octets)), port))
283 }
284 FAMILY_IPV6 => {
285 if value.len() < 20 {
286 return Err(StunError::Truncated);
287 }
288 let mut octets = [0u8; 16];
289 octets.copy_from_slice(&value[4..20]);
290 if xor {
291 let mut key = [0u8; 16];
292 key[..4].copy_from_slice(&cookie_be);
293 key[4..].copy_from_slice(txid);
294 for (o, k) in octets.iter_mut().zip(key.iter()) {
295 *o ^= *k;
296 }
297 }
298 Ok(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), port))
299 }
300 other => Err(StunError::UnexpectedType(other as u16)),
301 }
302}