Skip to main content

sipx_transport/
stun.rs

1//! A STUN Binding client, only as much of RFC 5389 as a keep-alive needs.
2//!
3//! RFC 5626 §4.4.2 makes STUN the keep-alive for UDP flows: "All SIP UAs MUST support the STUN
4//! keep-alive technique for UDP flows." It is a better keep-alive than a SIP request because the
5//! response carries the address the far end *sees*, so a UA learns that its NAT mapping changed
6//! rather than only that the flow still works — §4.4.2 has a changed `XOR-MAPPED-ADDRESS` mean the
7//! flow has failed.
8//!
9//! Scope, deliberately: a Binding Request with no attributes, and a Binding Response read for its
10//! mapped address. No `MESSAGE-INTEGRITY`, no `FINGERPRINT`, no long-term credentials — §4.4.2's
11//! keep-alive is unauthenticated, and RFC 5389 §10 does not require authentication for Binding
12//! over an established flow. Anything that needs the full protocol (ICE, RFC 8445) needs a
13//! different module, not more attributes bolted onto this one.
14
15use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
16
17/// RFC 5389 §6: the fixed cookie that distinguishes STUN from RFC 3489 and from other traffic.
18pub const MAGIC_COOKIE: u32 = 0x2112_a442;
19
20/// RFC 5389 §6: every STUN message begins with a 20-byte header.
21pub const HEADER_LEN: usize = 20;
22
23const BINDING_REQUEST: u16 = 0x0001;
24const BINDING_RESPONSE: u16 = 0x0101;
25const BINDING_ERROR: u16 = 0x0111;
26const XOR_MAPPED_ADDRESS: u16 = 0x0020;
27const FAMILY_IPV4: u8 = 0x01;
28const FAMILY_IPV6: u8 = 0x02;
29
30/// The 96 bits that tie a response to its request (RFC 5389 §6).
31pub type TransactionId = [u8; 12];
32
33/// A fresh transaction ID.
34///
35/// §6 requires it to be "uniformly and randomly chosen ... cryptographically random": it is the
36/// only thing preventing an off-path attacker from forging a response, and a forged response with
37/// a different mapped address would make a UA declare a working flow dead (§4.4.2).
38#[must_use]
39pub fn new_transaction_id() -> TransactionId {
40    use rand::Rng as _;
41    let mut id = [0u8; 12];
42    rand::rng().fill(&mut id);
43    id
44}
45
46/// Encode a Binding Request with no attributes (RFC 5389 §6, §7.1).
47#[must_use]
48pub fn binding_request(id: &TransactionId) -> Vec<u8> {
49    let mut out = Vec::with_capacity(HEADER_LEN);
50    out.extend_from_slice(&BINDING_REQUEST.to_be_bytes());
51    // Length counts the attributes only, and there are none.
52    out.extend_from_slice(&0u16.to_be_bytes());
53    out.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
54    out.extend_from_slice(id);
55    out
56}
57
58/// Whether a datagram is STUN rather than SIP (RFC 5389 §7.3).
59///
60/// Two checks, both from §7.3: "the most significant 2 bits of every STUN message MUST be zeroes",
61/// and the magic cookie. Demultiplexing on the same socket is what §4.4.2's keep-alive requires —
62/// the ping has to travel over the very flow it is testing — and this is the test the RFC gives
63/// for doing it. A SIP message cannot collide: its first byte is a method letter or `S`, all of
64/// which have a high bit set within the first two bits' meaning here (`0x53` is `0101…`, whose top
65/// two bits are `01`), and the cookie makes a collision beyond that vanishingly unlikely.
66#[must_use]
67pub fn is_stun(datagram: &[u8]) -> bool {
68    let Some(first) = datagram.first() else {
69        return false;
70    };
71    if first & 0xc0 != 0 || datagram.len() < HEADER_LEN {
72        return false;
73    }
74    datagram
75        .get(4..8)
76        .and_then(|cookie| <[u8; 4]>::try_from(cookie).ok())
77        .is_some_and(|cookie| u32::from_be_bytes(cookie) == MAGIC_COOKIE)
78}
79
80/// What a datagram that is STUN turned out to say.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Reply {
83    /// A Binding Response, with the address the server saw — if it named one.
84    Bound {
85        /// The transaction this answers.
86        id: TransactionId,
87        /// The reflexive address, from `XOR-MAPPED-ADDRESS`.
88        mapped: Option<SocketAddr>,
89    },
90    /// A Binding Error Response. §4.4.2: the flow "is considered failed".
91    Failed {
92        /// The transaction this answers.
93        id: TransactionId,
94    },
95}
96
97impl Reply {
98    /// The transaction this reply belongs to.
99    #[must_use]
100    pub fn id(&self) -> TransactionId {
101        match self {
102            Self::Bound { id, .. } | Self::Failed { id } => *id,
103        }
104    }
105}
106
107/// Read a STUN reply, or `None` if the datagram is not one.
108///
109/// Requests are not decoded: sipx is a STUN *client* here. A Binding Request arriving on a SIP
110/// socket is something else's business, and answering it would make sipx a STUN server by
111/// accident.
112#[must_use]
113pub fn parse_reply(datagram: &[u8]) -> Option<Reply> {
114    if !is_stun(datagram) {
115        return None;
116    }
117    let kind = u16::from_be_bytes(<[u8; 2]>::try_from(datagram.get(0..2)?).ok()?);
118    let length = usize::from(u16::from_be_bytes(
119        <[u8; 2]>::try_from(datagram.get(2..4)?).ok()?,
120    ));
121    let id: TransactionId = <[u8; 12]>::try_from(datagram.get(8..20)?).ok()?;
122
123    match kind {
124        BINDING_ERROR => Some(Reply::Failed { id }),
125        BINDING_RESPONSE => {
126            // The stated length is authoritative; a datagram carrying extra bytes is not licence
127            // to read them.
128            let body = datagram.get(HEADER_LEN..HEADER_LEN.checked_add(length)?)?;
129            Some(Reply::Bound {
130                id,
131                mapped: mapped_address(body, &id),
132            })
133        }
134        _ => None,
135    }
136}
137
138/// Walk the attributes for `XOR-MAPPED-ADDRESS` (RFC 5389 §15.2).
139fn mapped_address(mut body: &[u8], id: &TransactionId) -> Option<SocketAddr> {
140    while body.len() >= 4 {
141        let kind = u16::from_be_bytes(<[u8; 2]>::try_from(body.get(0..2)?).ok()?);
142        let length = usize::from(u16::from_be_bytes(
143            <[u8; 2]>::try_from(body.get(2..4)?).ok()?,
144        ));
145        let value = body.get(4..4usize.checked_add(length)?)?;
146        if kind == XOR_MAPPED_ADDRESS {
147            return decode_xor_mapped(value, id);
148        }
149        // §15: "the value in the length field MUST contain the length of the Value part ...
150        // Since STUN aligns attributes on 32-bit boundaries, attributes whose content is not a
151        // multiple of 4 bytes are padded". Skipping without the padding walks into the middle of
152        // the next attribute — the SOFTWARE attribute in RFC 5769's own vector is 11 bytes, so a
153        // decoder that forgets this fails on the RFC's example.
154        let padded = length.checked_add(3)? & !3;
155        body = body.get(4usize.checked_add(padded)?..).unwrap_or(&[]);
156    }
157    None
158}
159
160/// Undo the obfuscation §15.2 applies to the address.
161///
162/// The port is `XOR`ed with the top 16 bits of the cookie and the address with the cookie itself,
163/// extended by the transaction ID for IPv6. §15.2 explains the point: some NATs rewrite anything
164/// that looks like an address in a payload, and obfuscating it stops them corrupting the very
165/// value the mechanism exists to report.
166fn decode_xor_mapped(value: &[u8], id: &TransactionId) -> Option<SocketAddr> {
167    let family = *value.get(1)?;
168    let port = u16::from_be_bytes(<[u8; 2]>::try_from(value.get(2..4)?).ok()?)
169        ^ u16::try_from(MAGIC_COOKIE >> 16).ok()?;
170    match family {
171        FAMILY_IPV4 => {
172            let raw = u32::from_be_bytes(<[u8; 4]>::try_from(value.get(4..8)?).ok()?);
173            Some(SocketAddr::new(
174                IpAddr::V4(Ipv4Addr::from(raw ^ MAGIC_COOKIE)),
175                port,
176            ))
177        }
178        FAMILY_IPV6 => {
179            let raw = <[u8; 16]>::try_from(value.get(4..20)?).ok()?;
180            let mut key = [0u8; 16];
181            key.get_mut(..4)?
182                .copy_from_slice(&MAGIC_COOKIE.to_be_bytes());
183            key.get_mut(4..)?.copy_from_slice(id);
184            let mut out = [0u8; 16];
185            for (index, byte) in out.iter_mut().enumerate() {
186                *byte = raw.get(index)? ^ key.get(index)?;
187            }
188            Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(out)), port))
189        }
190        _ => None,
191    }
192}
193
194#[cfg(test)]
195#[allow(
196    clippy::unwrap_used,
197    clippy::expect_used,
198    clippy::panic,
199    clippy::indexing_slicing
200)]
201mod tests {
202    use super::*;
203
204    /// Decode the hex listings RFC 5769 prints, so the test input is the RFC's bytes rather than
205    /// something transcribed by hand into a different shape.
206    fn hex(text: &str) -> Vec<u8> {
207        text.split_whitespace()
208            .map(|byte| u8::from_str_radix(byte, 16).expect("a hex byte"))
209            .collect()
210    }
211
212    /// RFC 5769 §2.1, the sample request. Its transaction ID is what §2.2's response answers.
213    const SAMPLE_REQUEST: &str = "
214        00 01 00 58  21 12 a4 42  b7 e7 a7 01  bc 34 d6 86
215        fa 87 df ae  80 22 00 10  53 54 55 4e  20 74 65 73
216        74 20 63 6c  69 65 6e 74  00 24 00 04  6e 00 01 ff
217        80 29 00 08  93 2f f9 b1  51 26 3b 36  00 06 00 09
218        65 76 74 6a  3a 68 36 76  59 20 20 20  00 08 00 14
219        9a ea a7 0c  bf d8 cb 56  78 1e f2 b5  b2 d3 f2 49
220        c1 b5 71 a2  80 28 00 04  e5 7a 3b cf";
221
222    /// RFC 5769 §2.2, the sample IPv4 response. The RFC states the decoded address itself:
223    /// 192.0.2.1 port 32853.
224    const SAMPLE_RESPONSE: &str = "
225        01 01 00 3c  21 12 a4 42  b7 e7 a7 01  bc 34 d6 86
226        fa 87 df ae  80 22 00 0b  74 65 73 74  20 76 65 63
227        74 6f 72 20  00 20 00 08  00 01 a1 47  e1 12 a6 43
228        00 08 00 14  2b 91 f5 99  fd 9e 90 c3  8c 74 89 f9
229        2a f9 ba 53  f0 6b e7 d7  80 28 00 04  c0 7d 4c 96";
230
231    const SAMPLE_ID: TransactionId = [
232        0xb7, 0xe7, 0xa7, 0x01, 0xbc, 0x34, 0xd6, 0x86, 0xfa, 0x87, 0xdf, 0xae,
233    ];
234
235    /// The address RFC 5769 §2.2 says its own vector decodes to. Not computed here — the point of
236    /// a published vector is that the expected value comes from the publisher.
237    #[test]
238    fn the_rfc_5769_ipv4_response_decodes_to_the_address_the_rfc_states() {
239        let reply = parse_reply(&hex(SAMPLE_RESPONSE)).expect("a STUN reply");
240        assert_eq!(
241            reply,
242            Reply::Bound {
243                id: SAMPLE_ID,
244                mapped: Some("192.0.2.1:32853".parse().expect("valid")),
245            }
246        );
247    }
248
249    /// The `SOFTWARE` attribute in that vector is 11 bytes, so its 32-bit padding has to be
250    /// skipped to reach `XOR-MAPPED-ADDRESS` at all. A decoder that ignores §15's padding rule
251    /// walks into the middle of the next attribute and finds nothing — which is why the vector is
252    /// worth using rather than a hand-built two-attribute message.
253    #[test]
254    fn an_attribute_whose_length_is_not_a_multiple_of_four_is_padded_past() {
255        let bytes = hex(SAMPLE_RESPONSE);
256        let software_length = u16::from_be_bytes([bytes[22], bytes[23]]);
257        assert_eq!(software_length, 11, "the vector's SOFTWARE attribute");
258        assert!(
259            matches!(
260                parse_reply(&bytes),
261                Some(Reply::Bound {
262                    mapped: Some(_),
263                    ..
264                })
265            ),
266            "the padded attribute was not skipped correctly"
267        );
268    }
269
270    #[test]
271    fn the_rfc_5769_request_is_recognised_as_stun_but_not_as_a_reply() {
272        let bytes = hex(SAMPLE_REQUEST);
273        assert!(is_stun(&bytes));
274        assert!(
275            parse_reply(&bytes).is_none(),
276            "sipx is a STUN client; answering a Binding Request would make it a server by accident"
277        );
278    }
279
280    #[test]
281    fn our_binding_request_has_the_header_the_rfc_specifies() {
282        let request = binding_request(&SAMPLE_ID);
283        assert_eq!(request.len(), HEADER_LEN, "no attributes");
284        assert_eq!(&request[0..2], &[0x00, 0x01], "Binding Request");
285        assert_eq!(&request[2..4], &[0x00, 0x00], "length counts attributes");
286        assert_eq!(&request[4..8], &MAGIC_COOKIE.to_be_bytes());
287        assert_eq!(&request[8..20], &SAMPLE_ID);
288        assert!(is_stun(&request), "our own request must pass §7.3's test");
289    }
290
291    #[test]
292    fn a_sip_message_is_not_mistaken_for_stun() {
293        // The demultiplexing has to be safe in the direction that matters: a SIP request read as
294        // STUN would be dropped, and the far end would see a request vanish.
295        for message in [
296            &b"INVITE sip:bob@example.com SIP/2.0\r\n\r\n"[..],
297            &b"SIP/2.0 200 OK\r\n\r\n"[..],
298            &b"REGISTER sip:example.com SIP/2.0\r\n\r\n"[..],
299            &b"\r\n\r\n"[..],
300            &b""[..],
301        ] {
302            assert!(
303                !is_stun(message),
304                "{:?} was taken for STUN",
305                String::from_utf8_lossy(message)
306            );
307        }
308    }
309
310    #[test]
311    fn a_truncated_or_cookieless_datagram_is_not_stun() {
312        let mut short = binding_request(&SAMPLE_ID);
313        short.truncate(HEADER_LEN - 1);
314        assert!(!is_stun(&short), "a header must be complete to be one");
315
316        let mut wrong_cookie = binding_request(&SAMPLE_ID);
317        wrong_cookie[4] = 0x00;
318        assert!(!is_stun(&wrong_cookie), "§7.3's cookie check");
319    }
320
321    #[test]
322    fn a_binding_error_response_reads_as_a_failed_flow() {
323        // §4.4.2: "If a STUN Binding Error Response is received ... the UA considers the flow
324        // failed."
325        let mut bytes = binding_request(&SAMPLE_ID);
326        bytes[0] = 0x01;
327        bytes[1] = 0x11;
328        assert_eq!(
329            parse_reply(&bytes),
330            Some(Reply::Failed { id: SAMPLE_ID }),
331            "an error response is a failed flow, not an absent answer"
332        );
333    }
334
335    #[test]
336    fn a_response_with_no_mapped_address_still_answers_the_transaction() {
337        // A server is not obliged to be useful. What matters is that the flow is proven alive;
338        // the mapped address is extra, and treating its absence as a parse failure would declare
339        // a working flow dead.
340        let mut bytes = binding_request(&SAMPLE_ID);
341        bytes[0] = 0x01;
342        bytes[1] = 0x01;
343        assert_eq!(
344            parse_reply(&bytes),
345            Some(Reply::Bound {
346                id: SAMPLE_ID,
347                mapped: None
348            })
349        );
350    }
351
352    #[test]
353    fn an_ipv6_mapped_address_is_unxored_with_the_transaction_id() {
354        // §15.2 extends the XOR key with the transaction ID for IPv6. Built by XORing a known
355        // address *with the rule the RFC states*, then asserting it decodes back — the closest
356        // thing to a vector available, since RFC 5769 §2.3's IPv6 response is for a different
357        // transaction ID than §2.1's.
358        let addr: Ipv6Addr = "2001:db8::1".parse().expect("valid");
359        let port: u16 = 32853;
360        let mut key = [0u8; 16];
361        key[..4].copy_from_slice(&MAGIC_COOKIE.to_be_bytes());
362        key[4..].copy_from_slice(&SAMPLE_ID);
363        let xored: Vec<u8> = addr
364            .octets()
365            .iter()
366            .zip(key.iter())
367            .map(|(a, k)| a ^ k)
368            .collect();
369
370        let mut bytes = binding_request(&SAMPLE_ID);
371        bytes[0] = 0x01;
372        bytes[1] = 0x01;
373        bytes[2] = 0x00;
374        bytes[3] = 24; // one attribute: 4 header + 20 value
375        bytes.extend_from_slice(&XOR_MAPPED_ADDRESS.to_be_bytes());
376        bytes.extend_from_slice(&20u16.to_be_bytes());
377        bytes.push(0);
378        bytes.push(FAMILY_IPV6);
379        bytes.extend_from_slice(
380            &(port ^ u16::try_from(MAGIC_COOKIE >> 16).expect("fits")).to_be_bytes(),
381        );
382        bytes.extend_from_slice(&xored);
383
384        assert_eq!(
385            parse_reply(&bytes),
386            Some(Reply::Bound {
387                id: SAMPLE_ID,
388                mapped: Some(SocketAddr::new(IpAddr::V6(addr), port)),
389            })
390        );
391    }
392
393    #[test]
394    fn two_transaction_ids_differ() {
395        assert_ne!(new_transaction_id(), new_transaction_id());
396    }
397}