1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
16
17pub const MAGIC_COOKIE: u32 = 0x2112_a442;
19
20pub 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
30pub type TransactionId = [u8; 12];
32
33#[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#[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 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Reply {
83 Bound {
85 id: TransactionId,
87 mapped: Option<SocketAddr>,
89 },
90 Failed {
92 id: TransactionId,
94 },
95}
96
97impl Reply {
98 #[must_use]
100 pub fn id(&self) -> TransactionId {
101 match self {
102 Self::Bound { id, .. } | Self::Failed { id } => *id,
103 }
104 }
105}
106
107#[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 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
138fn 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 let padded = length.checked_add(3)? & !3;
155 body = body.get(4usize.checked_add(padded)?..).unwrap_or(&[]);
156 }
157 None
158}
159
160fn 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 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 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 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 #[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 #[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 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 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 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 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; 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}