Skip to main content

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}