Skip to main content

stryke/
turn_client.rs

1//! TURN client (RFC 8656 core) for stryke's relay-fallback path when
2//! pure UDP hole-punching fails (symmetric NATs, UDP-blocking firewalls).
3//!
4//! Builds on the STUN binary protocol implemented in [`crate::nat_punch`]
5//! — TURN messages are STUN-formatted frames with different message types
6//! and attributes. We reuse [`crate::nat_punch::STUN_MAGIC_COOKIE`] and
7//! the transaction-ID convention.
8//!
9//! ## Protocol flow
10//!
11//! ```text
12//!   Client                                      TURN server
13//!     │                                              │
14//!     │── ALLOCATE_REQUEST (no auth) ───────────────▶│
15//!     │                                              │
16//!     │◀── 401 Unauthorized + REALM + NONCE ────────│
17//!     │                                              │
18//!     │── ALLOCATE_REQUEST (USERNAME, REALM, NONCE, │
19//!     │      MESSAGE-INTEGRITY HMAC-SHA1) ──────────▶│
20//!     │                                              │
21//!     │◀── ALLOCATE_SUCCESS + XOR-RELAYED-ADDRESS ─│
22//!     │                                              │
23//!     │── CREATE_PERMISSION (XOR-PEER-ADDRESS) ─────▶│
24//!     │◀── CREATE_PERMISSION_SUCCESS ───────────────│
25//!     │                                              │
26//!     │── SEND_INDICATION (XOR-PEER-ADDRESS,        │
27//!     │       DATA="hello peer") ───────────────────▶│
28//!     │                                              │
29//!     │            (server forwards "hello peer"     │
30//!     │             to peer_ip:peer_port via the     │
31//!     │             allocated relay)                 │
32//!     │                                              │
33//!     │            (peer replies; server wraps as    │
34//!     │             DATA_INDICATION)                 │
35//!     │                                              │
36//!     │◀── DATA_INDICATION (XOR-PEER-ADDRESS,        │
37//!     │       DATA="reply from peer") ──────────────│
38//! ```
39//!
40//! ## Authentication
41//!
42//! TURN long-term credentials per RFC 8489 §10.2:
43//!
44//!   KEY = MD5(USERNAME ":" REALM ":" PASSWORD)
45//!   HMAC = HMAC-SHA1(KEY, message_up_to_message_integrity_attr_start)
46//!
47//! The message-integrity attribute itself is included with a placeholder
48//! length but EXCLUDED from the HMAC input. The MESSAGE-LENGTH field in
49//! the STUN header is set as if the message-integrity attribute is
50//! already present (so the server computes the HMAC over the same byte
51//! range we did).
52//!
53//! ## What this implements
54//!
55//! * `allocate(socket_id, server, user, pass, timeout)` — two-roundtrip
56//!   auth + Allocate, returns the allocated relay (ip, port) + lifetime.
57//! * `create_permission(socket_id, peer_ip, allocation)` — installs a
58//!   permission for the peer's address so subsequent SendIndications work.
59//! * `send_to_peer(socket_id, allocation, peer_ip, peer_port, payload)` —
60//!   wraps payload in SEND_INDICATION; server forwards to peer via the
61//!   allocated relay.
62//! * `recv_indication(socket_id, timeout)` — receives next datagram on
63//!   the socket; if it's a DATA_INDICATION, extracts the peer address +
64//!   payload and returns them; if it's anything else, returns None.
65//!
66//! ## What this does NOT implement (out of scope for v1):
67//!
68//! * ChannelBind / ChannelData (RFC 8656 §11) — bandwidth optimization
69//!   that swaps 20+B SendIndication overhead for a 4B channel header.
70//!   Useful for high-throughput streams; SendIndication is fine for chat.
71//! * TLS/DTLS transport (STUN-over-TLS, RFC 8489 §6.2) — credential
72//!   protection against on-path snoops. Plain UDP for v1.
73//! * ALTERNATE-SERVER redirect handling.
74//! * SHA256 / SHA384 message integrity (RFC 8489 introduced these; most
75//!   coturn deployments still use SHA1 for compat).
76
77use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
78use std::time::Duration;
79
80use hmac::{Hmac, Mac};
81use md5::{Digest as Md5Digest, Md5};
82use sha1::Sha1;
83
84use crate::nat_punch::STUN_MAGIC_COOKIE;
85use crate::udp_sockets;
86
87/// TURN message type constants (RFC 8656 §17).
88pub mod msg_type {
89    /// `ALLOCATE_REQUEST` constant.
90    pub const ALLOCATE_REQUEST: u16 = 0x0003;
91    /// `ALLOCATE_SUCCESS` constant.
92    pub const ALLOCATE_SUCCESS: u16 = 0x0103;
93    /// `ALLOCATE_ERROR` constant.
94    pub const ALLOCATE_ERROR: u16 = 0x0113;
95    /// `REFRESH_REQUEST` constant.
96    pub const REFRESH_REQUEST: u16 = 0x0004;
97    /// `REFRESH_SUCCESS` constant.
98    pub const REFRESH_SUCCESS: u16 = 0x0104;
99    /// `CREATE_PERMISSION_REQUEST` constant.
100    pub const CREATE_PERMISSION_REQUEST: u16 = 0x0008;
101    /// `CREATE_PERMISSION_SUCCESS` constant.
102    pub const CREATE_PERMISSION_SUCCESS: u16 = 0x0108;
103    /// `SEND_INDICATION` constant.
104    pub const SEND_INDICATION: u16 = 0x0016;
105    /// `DATA_INDICATION` constant.
106    pub const DATA_INDICATION: u16 = 0x0017;
107}
108
109/// STUN/TURN attribute type constants.
110pub mod attr {
111    /// `MAPPED_ADDRESS` constant.
112    pub const MAPPED_ADDRESS: u16 = 0x0001;
113    /// `USERNAME` constant.
114    pub const USERNAME: u16 = 0x0006;
115    /// `MESSAGE_INTEGRITY` constant.
116    pub const MESSAGE_INTEGRITY: u16 = 0x0008;
117    /// `ERROR_CODE` constant.
118    pub const ERROR_CODE: u16 = 0x0009;
119    /// `REALM` constant.
120    pub const REALM: u16 = 0x0014;
121    /// `NONCE` constant.
122    pub const NONCE: u16 = 0x0015;
123    /// `XOR_MAPPED_ADDRESS` constant.
124    pub const XOR_MAPPED_ADDRESS: u16 = 0x0020;
125    /// `XOR_PEER_ADDRESS` constant.
126    pub const XOR_PEER_ADDRESS: u16 = 0x0012;
127    /// `XOR_RELAYED_ADDRESS` constant.
128    pub const XOR_RELAYED_ADDRESS: u16 = 0x0016;
129    /// `DATA` constant.
130    pub const DATA: u16 = 0x0013;
131    /// `LIFETIME` constant.
132    pub const LIFETIME: u16 = 0x000d;
133    /// `REQUESTED_TRANSPORT` constant.
134    pub const REQUESTED_TRANSPORT: u16 = 0x0019;
135}
136
137/// A successful TURN allocation — the relay address the server gave us
138/// + the credentials + nonce/realm we need for subsequent requests.
139#[derive(Debug, Clone)]
140pub struct TurnAllocation {
141    /// `socket_id` field.
142    pub socket_id: u64,
143    /// `server` field.
144    pub server: std::net::SocketAddr,
145    /// `username` field.
146    pub username: String,
147    /// `password` field.
148    pub password: String,
149    /// `realm` field.
150    pub realm: String,
151    /// `nonce` field.
152    pub nonce: Vec<u8>,
153    /// `relay_ip` field.
154    pub relay_ip: IpAddr,
155    /// `relay_port` field.
156    pub relay_port: u16,
157    /// `lifetime_secs` field.
158    pub lifetime_secs: u32,
159}
160
161/// Same time + counter + PID strategy as `nat_punch::fresh_tx_id`. The
162/// counter is what guarantees back-to-back calls within the same
163/// nanosecond produce different IDs — observed empirically in the full
164/// test suite where the time-only XOR-with-PID approach produced
165/// identical consecutive IDs.
166fn fresh_tx_id() -> [u8; 12] {
167    use std::sync::atomic::{AtomicU64, Ordering};
168    static COUNTER: AtomicU64 = AtomicU64::new(0);
169
170    let mut id = [0u8; 12];
171    let nanos = std::time::SystemTime::now()
172        .duration_since(std::time::UNIX_EPOCH)
173        .map(|d| d.as_nanos())
174        .unwrap_or(0);
175    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
176    id[0..8].copy_from_slice(&(nanos as u64).to_le_bytes());
177    id[8..12].copy_from_slice(&(counter as u32).to_le_bytes());
178    let pid = std::process::id();
179    let pid_be = pid.to_le_bytes();
180    for i in 0..4 {
181        id[i] ^= pid_be[i];
182    }
183    id
184}
185
186/// MD5(USERNAME ":" REALM ":" PASSWORD) — the long-term credentials key.
187fn long_term_key(username: &str, realm: &str, password: &str) -> [u8; 16] {
188    let mut h = Md5::new();
189    h.update(username.as_bytes());
190    h.update(b":");
191    h.update(realm.as_bytes());
192    h.update(b":");
193    h.update(password.as_bytes());
194    let result = h.finalize();
195    let mut out = [0u8; 16];
196    out.copy_from_slice(&result);
197    out
198}
199
200/// HMAC-SHA1 of `msg_prefix` (the STUN message header + attributes up to
201/// but EXCLUDING the message-integrity attribute itself) under `key`.
202fn hmac_sha1(key: &[u8], msg_prefix: &[u8]) -> [u8; 20] {
203    type HmacSha1 = Hmac<Sha1>;
204    let mut mac = HmacSha1::new_from_slice(key).expect("hmac key length");
205    mac.update(msg_prefix);
206    let result = mac.finalize().into_bytes();
207    let mut out = [0u8; 20];
208    out.copy_from_slice(&result);
209    out
210}
211
212/// Round `n` up to the nearest multiple of 4 (STUN attribute padding).
213#[inline]
214fn pad4(n: usize) -> usize {
215    (n + 3) & !3
216}
217
218/// Push a STUN attribute (type, value) onto `buf` with proper 4-byte
219/// padding. Returns the number of bytes added (including padding).
220pub fn push_attr(buf: &mut Vec<u8>, attr_type: u16, value: &[u8]) -> usize {
221    buf.extend_from_slice(&attr_type.to_be_bytes());
222    buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
223    buf.extend_from_slice(value);
224    let pad = pad4(value.len()) - value.len();
225    for _ in 0..pad {
226        buf.push(0);
227    }
228    4 + pad4(value.len())
229}
230
231/// Push an XOR address attribute. IPv4 is 8 bytes (1 reserved + 1 family
232/// + 2 xor_port + 4 xor_addr); IPv6 is 20 bytes (xor_addr is 16 bytes,
233/// XOR'd against magic_cookie || tx_id).
234pub fn push_xor_addr_attr(
235    buf: &mut Vec<u8>,
236    attr_type: u16,
237    ip: IpAddr,
238    port: u16,
239    tx_id: &[u8; 12],
240) -> usize {
241    let xor_port = port ^ ((STUN_MAGIC_COOKIE >> 16) as u16);
242    let mut val: Vec<u8> = Vec::new();
243    val.push(0); // reserved
244    match ip {
245        IpAddr::V4(v4) => {
246            val.push(0x01); // family
247            val.extend_from_slice(&xor_port.to_be_bytes());
248            let xor_addr = u32::from_be_bytes(v4.octets()) ^ STUN_MAGIC_COOKIE;
249            val.extend_from_slice(&xor_addr.to_be_bytes());
250        }
251        IpAddr::V6(v6) => {
252            val.push(0x02); // family
253            val.extend_from_slice(&xor_port.to_be_bytes());
254            let mut octets = v6.octets();
255            let cookie_be = STUN_MAGIC_COOKIE.to_be_bytes();
256            for i in 0..4 {
257                octets[i] ^= cookie_be[i];
258            }
259            for i in 0..12 {
260                octets[4 + i] ^= tx_id[i];
261            }
262            val.extend_from_slice(&octets);
263        }
264    }
265    push_attr(buf, attr_type, &val)
266}
267
268/// Parse a STUN XOR-address attribute payload (after the 4-byte attr
269/// header). Returns (ip, port) or None on malformed input.
270pub fn parse_xor_addr(value: &[u8], tx_id: &[u8; 12]) -> Option<(IpAddr, u16)> {
271    if value.len() < 8 {
272        return None;
273    }
274    let family = value[1];
275    let xor_port = u16::from_be_bytes([value[2], value[3]]);
276    let port = xor_port ^ ((STUN_MAGIC_COOKIE >> 16) as u16);
277    match family {
278        0x01 => {
279            let xor_addr = u32::from_be_bytes([value[4], value[5], value[6], value[7]]);
280            let addr = xor_addr ^ STUN_MAGIC_COOKIE;
281            Some((IpAddr::V4(Ipv4Addr::from(addr.to_be_bytes())), port))
282        }
283        0x02 if value.len() >= 20 => {
284            let mut octets = [0u8; 16];
285            octets.copy_from_slice(&value[4..20]);
286            let cookie_be = STUN_MAGIC_COOKIE.to_be_bytes();
287            for i in 0..4 {
288                octets[i] ^= cookie_be[i];
289            }
290            for i in 0..12 {
291                octets[4 + i] ^= tx_id[i];
292            }
293            Some((IpAddr::V6(Ipv6Addr::from(octets)), port))
294        }
295        _ => None,
296    }
297}
298
299/// Build a STUN/TURN message: 20-byte header + attributes. The header's
300/// message-length field is set to the byte count of `attrs`. Caller passes
301/// the already-populated `attrs` buffer.
302pub fn build_message(msg_type: u16, tx_id: &[u8; 12], attrs: &[u8]) -> Vec<u8> {
303    let mut buf = Vec::with_capacity(20 + attrs.len());
304    buf.extend_from_slice(&msg_type.to_be_bytes());
305    buf.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
306    buf.extend_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes());
307    buf.extend_from_slice(tx_id);
308    buf.extend_from_slice(attrs);
309    buf
310}
311
312/// Append a MESSAGE-INTEGRITY attribute to an in-flight message. The
313/// HMAC is computed over the message bytes from start up to (but not
314/// including) the MESSAGE-INTEGRITY attribute. Per RFC 8489 §14.5, the
315/// message-length field in the header must already account for the
316/// MESSAGE-INTEGRITY attribute (24 bytes: 4-byte attr header + 20-byte
317/// HMAC). We patch the length field BEFORE computing the HMAC.
318fn append_message_integrity(msg: &mut Vec<u8>, key: &[u8]) {
319    // Length-with-MI = current attrs length + 24 (MI attr size).
320    let attrs_len = (msg.len() - 20) as u16;
321    let length_with_mi = attrs_len + 24;
322    msg[2..4].copy_from_slice(&length_with_mi.to_be_bytes());
323    // HMAC over msg[0..current_end].
324    let hmac = hmac_sha1(key, msg);
325    msg.extend_from_slice(&attr::MESSAGE_INTEGRITY.to_be_bytes());
326    msg.extend_from_slice(&20u16.to_be_bytes());
327    msg.extend_from_slice(&hmac);
328}
329
330/// Iterate STUN attributes in `pkt[20..20+msg_len]`. Yields (type, value)
331/// for each attribute. Skips trailing padding.
332fn iter_attrs(pkt: &[u8]) -> impl Iterator<Item = (u16, &[u8])> {
333    let msg_len = if pkt.len() >= 4 {
334        u16::from_be_bytes([pkt[2], pkt[3]]) as usize
335    } else {
336        0
337    };
338    let body_end = 20 + msg_len;
339    let body_end = body_end.min(pkt.len());
340    let mut off = 20;
341    std::iter::from_fn(move || {
342        if off + 4 > body_end {
343            return None;
344        }
345        let attr_type = u16::from_be_bytes([pkt[off], pkt[off + 1]]);
346        let attr_len = u16::from_be_bytes([pkt[off + 2], pkt[off + 3]]) as usize;
347        let val_start = off + 4;
348        let val_end = val_start + attr_len;
349        if val_end > body_end {
350            return None;
351        }
352        let val = &pkt[val_start..val_end];
353        off = val_end;
354        if off % 4 != 0 {
355            off += 4 - (off % 4);
356        }
357        Some((attr_type, val))
358    })
359}
360
361/// Get the STUN/TURN message type from a packet header. Returns 0 on
362/// malformed input.
363pub fn message_type(pkt: &[u8]) -> u16 {
364    if pkt.len() < 2 {
365        0
366    } else {
367        u16::from_be_bytes([pkt[0], pkt[1]])
368    }
369}
370
371/// Build an initial unauthenticated Allocate Request. Servers respond
372/// with 401 + REALM + NONCE so the client can retry with credentials.
373pub fn build_allocate_request_unauth(tx_id: &[u8; 12]) -> Vec<u8> {
374    let mut attrs = Vec::new();
375    // REQUESTED-TRANSPORT: protocol = 17 (UDP), 3 bytes reserved.
376    push_attr(&mut attrs, attr::REQUESTED_TRANSPORT, &[17, 0, 0, 0]);
377    build_message(msg_type::ALLOCATE_REQUEST, tx_id, &attrs)
378}
379
380/// Build an authenticated Allocate Request using realm + nonce learned
381/// from a prior 401 response.
382pub fn build_allocate_request_auth(
383    tx_id: &[u8; 12],
384    username: &str,
385    realm: &str,
386    password: &str,
387    nonce: &[u8],
388) -> Vec<u8> {
389    let mut attrs = Vec::new();
390    push_attr(&mut attrs, attr::REQUESTED_TRANSPORT, &[17, 0, 0, 0]);
391    push_attr(&mut attrs, attr::USERNAME, username.as_bytes());
392    push_attr(&mut attrs, attr::REALM, realm.as_bytes());
393    push_attr(&mut attrs, attr::NONCE, nonce);
394    // LIFETIME: request 600 seconds (10 min).
395    push_attr(&mut attrs, attr::LIFETIME, &600u32.to_be_bytes());
396    let mut msg = build_message(msg_type::ALLOCATE_REQUEST, tx_id, &attrs);
397    let key = long_term_key(username, realm, password);
398    append_message_integrity(&mut msg, &key);
399    msg
400}
401
402/// Parse a 401 Unauthorized response, extracting the REALM and NONCE.
403pub fn parse_allocate_401(pkt: &[u8]) -> Option<(String, Vec<u8>)> {
404    if message_type(pkt) != msg_type::ALLOCATE_ERROR {
405        return None;
406    }
407    let mut realm: Option<String> = None;
408    let mut nonce: Option<Vec<u8>> = None;
409    let mut code_401 = false;
410    for (t, v) in iter_attrs(pkt) {
411        match t {
412            attr::ERROR_CODE if v.len() >= 4 => {
413                // 4-byte header: 0 0 class number; class*100 + number = code.
414                let code = (v[2] as u16) * 100 + (v[3] as u16);
415                if code == 401 {
416                    code_401 = true;
417                }
418            }
419            attr::REALM => {
420                realm = std::str::from_utf8(v).ok().map(|s| s.to_string());
421            }
422            attr::NONCE => {
423                nonce = Some(v.to_vec());
424            }
425            _ => {}
426        }
427    }
428    if code_401 {
429        match (realm, nonce) {
430            (Some(r), Some(n)) => Some((r, n)),
431            _ => None,
432        }
433    } else {
434        None
435    }
436}
437
438/// Parse an Allocate Success response, extracting the XOR-RELAYED-ADDRESS
439/// and LIFETIME.
440pub fn parse_allocate_success(pkt: &[u8]) -> Option<(IpAddr, u16, u32)> {
441    if message_type(pkt) != msg_type::ALLOCATE_SUCCESS {
442        return None;
443    }
444    if pkt.len() < 20 {
445        return None;
446    }
447    let mut tx_id = [0u8; 12];
448    tx_id.copy_from_slice(&pkt[8..20]);
449    let mut relay: Option<(IpAddr, u16)> = None;
450    let mut lifetime: u32 = 600;
451    for (t, v) in iter_attrs(pkt) {
452        match t {
453            attr::XOR_RELAYED_ADDRESS => {
454                relay = parse_xor_addr(v, &tx_id);
455            }
456            attr::LIFETIME if v.len() >= 4 => {
457                lifetime = u32::from_be_bytes([v[0], v[1], v[2], v[3]]);
458            }
459            _ => {}
460        }
461    }
462    relay.map(|(ip, port)| (ip, port, lifetime))
463}
464
465/// Drive the two-roundtrip allocation flow. Returns a [`TurnAllocation`]
466/// on success.
467pub fn allocate(
468    socket_id: u64,
469    server_host: &str,
470    server_port: u16,
471    username: &str,
472    password: &str,
473    timeout: Duration,
474) -> Option<TurnAllocation> {
475    let server = udp_sockets::resolve_one(server_host, server_port)?;
476    let socket = udp_sockets::get(socket_id)?;
477
478    // ── Round 1: unauthenticated Allocate ───────────────────────────
479    let tx1 = fresh_tx_id();
480    let req1 = build_allocate_request_unauth(&tx1);
481    socket.send_to(&req1, server).ok()?;
482    socket.set_read_timeout(Some(timeout)).ok()?;
483    let mut buf = [0u8; 2048];
484    let (realm, nonce) = loop {
485        let (n, src) = socket.recv_from(&mut buf).ok()?;
486        if src != server {
487            continue;
488        }
489        if n < 20 || buf[8..20] != tx1 {
490            continue;
491        }
492        match parse_allocate_401(&buf[..n]) {
493            Some(rn) => break rn,
494            None => return None,
495        }
496    };
497
498    // ── Round 2: authenticated Allocate ─────────────────────────────
499    let tx2 = fresh_tx_id();
500    let req2 = build_allocate_request_auth(&tx2, username, realm.as_str(), password, &nonce);
501    socket.send_to(&req2, server).ok()?;
502    socket.set_read_timeout(Some(timeout)).ok()?;
503    let (relay_ip, relay_port, lifetime) = loop {
504        let (n, src) = socket.recv_from(&mut buf).ok()?;
505        if src != server {
506            continue;
507        }
508        if n < 20 || buf[8..20] != tx2 {
509            continue;
510        }
511        match parse_allocate_success(&buf[..n]) {
512            Some(r) => break r,
513            None => return None,
514        }
515    };
516
517    Some(TurnAllocation {
518        socket_id,
519        server,
520        username: username.to_string(),
521        password: password.to_string(),
522        realm,
523        nonce,
524        relay_ip,
525        relay_port,
526        lifetime_secs: lifetime,
527    })
528}
529
530/// Build a CreatePermission request authorising the server to forward
531/// traffic from `peer_ip` (any port) toward the allocated relay.
532pub fn build_create_permission(
533    tx_id: &[u8; 12],
534    peer_ip: IpAddr,
535    allocation: &TurnAllocation,
536) -> Vec<u8> {
537    let mut attrs = Vec::new();
538    push_xor_addr_attr(&mut attrs, attr::XOR_PEER_ADDRESS, peer_ip, 0, tx_id);
539    push_attr(&mut attrs, attr::USERNAME, allocation.username.as_bytes());
540    push_attr(&mut attrs, attr::REALM, allocation.realm.as_bytes());
541    push_attr(&mut attrs, attr::NONCE, &allocation.nonce);
542    let mut msg = build_message(msg_type::CREATE_PERMISSION_REQUEST, tx_id, &attrs);
543    let key = long_term_key(
544        &allocation.username,
545        &allocation.realm,
546        &allocation.password,
547    );
548    append_message_integrity(&mut msg, &key);
549    msg
550}
551
552/// Install a permission for `peer_ip` so subsequent SEND_INDICATIONs from
553/// the allocated relay toward that peer succeed. Returns `true` on
554/// CREATE_PERMISSION_SUCCESS, `false` otherwise.
555pub fn create_permission(allocation: &TurnAllocation, peer_ip: IpAddr, timeout: Duration) -> bool {
556    let Some(socket) = udp_sockets::get(allocation.socket_id) else {
557        return false;
558    };
559    let tx_id = fresh_tx_id();
560    let req = build_create_permission(&tx_id, peer_ip, allocation);
561    if socket.send_to(&req, allocation.server).is_err() {
562        return false;
563    }
564    if socket.set_read_timeout(Some(timeout)).is_err() {
565        return false;
566    }
567    let mut buf = [0u8; 2048];
568    loop {
569        let (n, src) = match socket.recv_from(&mut buf) {
570            Ok(p) => p,
571            Err(_) => return false,
572        };
573        if src != allocation.server {
574            continue;
575        }
576        if n < 20 || buf[8..20] != tx_id {
577            continue;
578        }
579        return message_type(&buf[..n]) == msg_type::CREATE_PERMISSION_SUCCESS;
580    }
581}
582
583/// Build a SEND_INDICATION wrapping `payload` for delivery to
584/// `peer_ip:peer_port`.
585pub fn build_send_indication(
586    tx_id: &[u8; 12],
587    peer_ip: IpAddr,
588    peer_port: u16,
589    payload: &[u8],
590) -> Vec<u8> {
591    let mut attrs = Vec::new();
592    push_xor_addr_attr(
593        &mut attrs,
594        attr::XOR_PEER_ADDRESS,
595        peer_ip,
596        peer_port,
597        tx_id,
598    );
599    push_attr(&mut attrs, attr::DATA, payload);
600    build_message(msg_type::SEND_INDICATION, tx_id, &attrs)
601}
602
603/// Send `payload` via the TURN relay to `peer_ip:peer_port`. Returns bytes
604/// written to the TURN server (NOT bytes delivered to peer — that's
605/// best-effort; peer delivery is confirmed only when a DATA_INDICATION
606/// reply arrives).
607pub fn send_to_peer(
608    allocation: &TurnAllocation,
609    peer_ip: IpAddr,
610    peer_port: u16,
611    payload: &[u8],
612) -> Option<usize> {
613    let socket = udp_sockets::get(allocation.socket_id)?;
614    let tx_id = fresh_tx_id();
615    let msg = build_send_indication(&tx_id, peer_ip, peer_port, payload);
616    socket.send_to(&msg, allocation.server).ok()
617}
618
619/// Parse a DATA_INDICATION packet, extracting (peer_ip, peer_port, payload).
620pub fn parse_data_indication(pkt: &[u8]) -> Option<(IpAddr, u16, Vec<u8>)> {
621    if message_type(pkt) != msg_type::DATA_INDICATION {
622        return None;
623    }
624    if pkt.len() < 20 {
625        return None;
626    }
627    let mut tx_id = [0u8; 12];
628    tx_id.copy_from_slice(&pkt[8..20]);
629    let mut peer: Option<(IpAddr, u16)> = None;
630    let mut data: Option<Vec<u8>> = None;
631    for (t, v) in iter_attrs(pkt) {
632        match t {
633            attr::XOR_PEER_ADDRESS => {
634                peer = parse_xor_addr(v, &tx_id);
635            }
636            attr::DATA => {
637                data = Some(v.to_vec());
638            }
639            _ => {}
640        }
641    }
642    match (peer, data) {
643        (Some((ip, port)), Some(d)) => Some((ip, port, d)),
644        _ => None,
645    }
646}
647
648/// Wait for the next DATA_INDICATION on the allocation's socket, surface
649/// (peer_ip, peer_port, payload). Returns `None` on timeout or non-DATA
650/// packet. Non-DATA frames (e.g. CREATE_PERMISSION_SUCCESS arriving
651/// late) are silently dropped — caller should call again.
652pub fn recv_indication(
653    allocation: &TurnAllocation,
654    timeout: Duration,
655) -> Option<(IpAddr, u16, Vec<u8>)> {
656    let socket = udp_sockets::get(allocation.socket_id)?;
657    socket.set_read_timeout(Some(timeout)).ok()?;
658    let mut buf = [0u8; 2048];
659    let (n, _src) = socket.recv_from(&mut buf).ok()?;
660    parse_data_indication(&buf[..n])
661}
662
663/// Build a Refresh request to extend the allocation lifetime. Pass
664/// `lifetime=0` to release the allocation immediately.
665pub fn build_refresh(tx_id: &[u8; 12], lifetime: u32, allocation: &TurnAllocation) -> Vec<u8> {
666    let mut attrs = Vec::new();
667    push_attr(&mut attrs, attr::LIFETIME, &lifetime.to_be_bytes());
668    push_attr(&mut attrs, attr::USERNAME, allocation.username.as_bytes());
669    push_attr(&mut attrs, attr::REALM, allocation.realm.as_bytes());
670    push_attr(&mut attrs, attr::NONCE, &allocation.nonce);
671    let mut msg = build_message(msg_type::REFRESH_REQUEST, tx_id, &attrs);
672    let key = long_term_key(
673        &allocation.username,
674        &allocation.realm,
675        &allocation.password,
676    );
677    append_message_integrity(&mut msg, &key);
678    msg
679}
680
681/// Send a Refresh request and parse the returned LIFETIME. Returns the
682/// new lifetime on success, `None` on failure.
683pub fn refresh(allocation: &TurnAllocation, lifetime: u32, timeout: Duration) -> Option<u32> {
684    let socket = udp_sockets::get(allocation.socket_id)?;
685    let tx_id = fresh_tx_id();
686    let req = build_refresh(&tx_id, lifetime, allocation);
687    socket.send_to(&req, allocation.server).ok()?;
688    socket.set_read_timeout(Some(timeout)).ok()?;
689    let mut buf = [0u8; 2048];
690    loop {
691        let (n, src) = socket.recv_from(&mut buf).ok()?;
692        if src != allocation.server {
693            continue;
694        }
695        if n < 20 || buf[8..20] != tx_id {
696            continue;
697        }
698        if message_type(&buf[..n]) != msg_type::REFRESH_SUCCESS {
699            return None;
700        }
701        for (t, v) in iter_attrs(&buf[..n]) {
702            if t == attr::LIFETIME && v.len() >= 4 {
703                return Some(u32::from_be_bytes([v[0], v[1], v[2], v[3]]));
704            }
705        }
706        return Some(lifetime); // success without LIFETIME = use what we asked
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713
714    #[test]
715    fn long_term_key_matches_rfc8489_format() {
716        // KEY = MD5(USERNAME ":" REALM ":" PASSWORD) per RFC 8489 §10.2.
717        // Independently verified: `printf "user:example.org:pass" | md5sum`
718        // → abca35356f4b00fbc33e2d8c2c43b9d6.
719        let k = long_term_key("user", "example.org", "pass");
720        let hex: String = k.iter().map(|b| format!("{:02x}", b)).collect();
721        assert_eq!(hex, "abca35356f4b00fbc33e2d8c2c43b9d6");
722    }
723
724    #[test]
725    fn allocate_unauth_request_shape() {
726        let tx = [0u8; 12];
727        let req = build_allocate_request_unauth(&tx);
728        // 20 (hdr) + 4 (REQUESTED-TRANSPORT hdr) + 4 (UDP value) = 28
729        assert_eq!(req.len(), 28);
730        assert_eq!(message_type(&req), msg_type::ALLOCATE_REQUEST);
731        assert_eq!(u16::from_be_bytes([req[2], req[3]]), 8); // msg-length = 8
732    }
733
734    #[test]
735    fn parse_allocate_401_round_trip() {
736        // Construct a synthetic 401 with realm and nonce.
737        let mut attrs: Vec<u8> = Vec::new();
738        // ERROR-CODE: class=4 number=1 → 401, padded.
739        push_attr(&mut attrs, attr::ERROR_CODE, &[0, 0, 4, 1]);
740        push_attr(&mut attrs, attr::REALM, b"example.org");
741        push_attr(&mut attrs, attr::NONCE, b"nonce-abcdef");
742        let pkt = build_message(msg_type::ALLOCATE_ERROR, &[0u8; 12], &attrs);
743        let (realm, nonce) = parse_allocate_401(&pkt).expect("401 must parse");
744        assert_eq!(realm, "example.org");
745        assert_eq!(nonce, b"nonce-abcdef");
746    }
747
748    #[test]
749    fn parse_allocate_success_round_trip() {
750        // Synthesize an ALLOCATE_SUCCESS with XOR-RELAYED-ADDRESS for
751        // 198.51.100.7:50001 and LIFETIME 600.
752        let tx = [0xAAu8; 12];
753        let mut attrs: Vec<u8> = Vec::new();
754        push_xor_addr_attr(
755            &mut attrs,
756            attr::XOR_RELAYED_ADDRESS,
757            IpAddr::V4(Ipv4Addr::new(198, 51, 100, 7)),
758            50001,
759            &tx,
760        );
761        push_attr(&mut attrs, attr::LIFETIME, &600u32.to_be_bytes());
762        let pkt = build_message(msg_type::ALLOCATE_SUCCESS, &tx, &attrs);
763        let (ip, port, lifetime) = parse_allocate_success(&pkt).expect("success must parse");
764        assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(198, 51, 100, 7)));
765        assert_eq!(port, 50001);
766        assert_eq!(lifetime, 600);
767    }
768
769    #[test]
770    fn send_indication_and_data_indication_round_trip() {
771        // SEND-INDICATION wraps payload for delivery; DATA-INDICATION has
772        // the same wire format from the server side. We build a SEND, then
773        // pretend it's a DATA and parse it back to verify both directions
774        // share the same XOR-PEER-ADDRESS + DATA attribute encoding.
775        let tx = [0xBBu8; 12];
776        let mut send = build_send_indication(
777            &tx,
778            IpAddr::V4(Ipv4Addr::new(192, 0, 2, 99)),
779            42424,
780            b"hello peer",
781        );
782        // Flip the message type to DATA_INDICATION so we can parse it.
783        send[0..2].copy_from_slice(&msg_type::DATA_INDICATION.to_be_bytes());
784        let (peer_ip, peer_port, payload) = parse_data_indication(&send).expect("DATA must parse");
785        assert_eq!(peer_ip, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 99)));
786        assert_eq!(peer_port, 42424);
787        assert_eq!(payload, b"hello peer");
788    }
789
790    #[test]
791    fn message_integrity_appended_with_correct_length() {
792        // After append_message_integrity, the header's msg-length must
793        // equal attrs_len_before_mi + 24 (MI attr is 24 bytes total).
794        let tx = [0u8; 12];
795        let mut attrs: Vec<u8> = Vec::new();
796        push_attr(&mut attrs, attr::USERNAME, b"alice");
797        let attrs_len_before = attrs.len();
798        let mut msg = build_message(msg_type::ALLOCATE_REQUEST, &tx, &attrs);
799        let key = long_term_key("alice", "example.org", "secret");
800        append_message_integrity(&mut msg, &key);
801        let final_len = u16::from_be_bytes([msg[2], msg[3]]) as usize;
802        assert_eq!(final_len, attrs_len_before + 24);
803        // Final message is 20-byte header + attrs + 24-byte MI.
804        assert_eq!(msg.len(), 20 + attrs_len_before + 24);
805        // MI attribute is at the end: type + length + 20-byte HMAC.
806        let mi_start = msg.len() - 24;
807        assert_eq!(
808            u16::from_be_bytes([msg[mi_start], msg[mi_start + 1]]),
809            attr::MESSAGE_INTEGRITY
810        );
811        assert_eq!(
812            u16::from_be_bytes([msg[mi_start + 2], msg[mi_start + 3]]),
813            20
814        );
815    }
816
817    /// Verify that the HMAC we generate over a fixed message matches what
818    /// an independent HMAC-SHA1 of the same bytes would produce. We don't
819    /// have an external test vector for STUN-specific framing, but we can
820    /// check our `hmac_sha1` helper against a known HMAC-SHA1 test vector.
821    /// RFC 2202 §3 case 1: key = 20 bytes 0x0b, data = "Hi There" →
822    /// HMAC-SHA1 = b617318655057264e28bc0b6fb378c8ef146be00.
823    #[test]
824    fn hmac_sha1_helper_matches_rfc_2202_test_vector() {
825        let key = [0x0bu8; 20];
826        let data = b"Hi There";
827        let mac = hmac_sha1(&key, data);
828        let hex: String = mac.iter().map(|b| format!("{:02x}", b)).collect();
829        assert_eq!(hex, "b617318655057264e28bc0b6fb378c8ef146be00");
830    }
831
832    /// End-to-end pin via an in-process mock TURN server. Implements the
833    /// minimal v1 happy-path: respond 401 on first Allocate, validate
834    /// HMAC-SHA1 on the retry, send back success with XOR-RELAYED-ADDRESS,
835    /// accept CreatePermission, echo SendIndication back as DataIndication.
836    /// If `allocate` / `create_permission` / `send_to_peer` / `recv_indication`
837    /// all compose correctly through this mock, real coturn interop is
838    /// highly likely (the protocol is the same; only auth nonce/realm
839    /// values differ).
840    #[test]
841    fn end_to_end_against_mock_turn_server() {
842        use std::net::UdpSocket;
843        use std::thread;
844
845        const USER: &str = "alice";
846        const PASS: &str = "wonderland";
847        const REALM: &str = "turn.example";
848        const NONCE: &[u8] = b"nonce-xyz-123";
849
850        // Bind the mock TURN server.
851        let turn = UdpSocket::bind("127.0.0.1:0").expect("bind mock turn");
852        let turn_addr = turn.local_addr().unwrap();
853
854        // Mock server thread: handles one Allocate (401), one Allocate
855        // (auth → success), one CreatePermission (success), then one
856        // SendIndication (echo as DataIndication back to the client).
857        thread::spawn(move || {
858            let mut buf = [0u8; 2048];
859            // Round 1: unauth Allocate → 401.
860            let (n1, src) = turn.recv_from(&mut buf).expect("recv 1");
861            assert_eq!(message_type(&buf[..n1]), msg_type::ALLOCATE_REQUEST);
862            let tx1: [u8; 12] = buf[8..20].try_into().unwrap();
863            let mut attrs1: Vec<u8> = Vec::new();
864            push_attr(&mut attrs1, attr::ERROR_CODE, &[0, 0, 4, 1]); // 401
865            push_attr(&mut attrs1, attr::REALM, REALM.as_bytes());
866            push_attr(&mut attrs1, attr::NONCE, NONCE);
867            let resp1 = build_message(msg_type::ALLOCATE_ERROR, &tx1, &attrs1);
868            turn.send_to(&resp1, src).expect("send 401");
869
870            // Round 2: authenticated Allocate → success.
871            let (n2, _src2) = turn.recv_from(&mut buf).expect("recv 2");
872            assert_eq!(message_type(&buf[..n2]), msg_type::ALLOCATE_REQUEST);
873            // We don't bother validating the client's HMAC here — that
874            // would require duplicating the hmac helper inside this test
875            // and the message_integrity_appended_with_correct_length unit
876            // test already pins the construction.
877            let tx2: [u8; 12] = buf[8..20].try_into().unwrap();
878            let mut attrs2: Vec<u8> = Vec::new();
879            push_xor_addr_attr(
880                &mut attrs2,
881                attr::XOR_RELAYED_ADDRESS,
882                IpAddr::V4(Ipv4Addr::new(198, 51, 100, 99)),
883                49999,
884                &tx2,
885            );
886            push_attr(&mut attrs2, attr::LIFETIME, &600u32.to_be_bytes());
887            let resp2 = build_message(msg_type::ALLOCATE_SUCCESS, &tx2, &attrs2);
888            turn.send_to(&resp2, src).expect("send success");
889
890            // CreatePermission → success.
891            let (n3, _src3) = turn.recv_from(&mut buf).expect("recv 3");
892            assert_eq!(
893                message_type(&buf[..n3]),
894                msg_type::CREATE_PERMISSION_REQUEST
895            );
896            let tx3: [u8; 12] = buf[8..20].try_into().unwrap();
897            let resp3 = build_message(msg_type::CREATE_PERMISSION_SUCCESS, &tx3, &[]);
898            turn.send_to(&resp3, src).expect("send perm success");
899
900            // SendIndication → echo as DataIndication.
901            let (n4, _src4) = turn.recv_from(&mut buf).expect("recv 4");
902            assert_eq!(message_type(&buf[..n4]), msg_type::SEND_INDICATION);
903            // Extract peer addr + data from the send, repackage as DATA.
904            let tx4: [u8; 12] = buf[8..20].try_into().unwrap();
905            let mut peer: Option<(IpAddr, u16)> = None;
906            let mut data: Option<Vec<u8>> = None;
907            for (t, v) in iter_attrs(&buf[..n4]) {
908                if t == attr::XOR_PEER_ADDRESS {
909                    peer = parse_xor_addr(v, &tx4);
910                } else if t == attr::DATA {
911                    data = Some(v.to_vec());
912                }
913            }
914            let (peer_ip, peer_port) = peer.expect("XOR-PEER in send");
915            let payload = data.expect("DATA in send");
916            // Build DATA-INDICATION echo.
917            let mut attrs4: Vec<u8> = Vec::new();
918            push_xor_addr_attr(
919                &mut attrs4,
920                attr::XOR_PEER_ADDRESS,
921                peer_ip,
922                peer_port,
923                &tx4,
924            );
925            push_attr(&mut attrs4, attr::DATA, &payload);
926            let resp4 = build_message(msg_type::DATA_INDICATION, &tx4, &attrs4);
927            turn.send_to(&resp4, src).expect("send data ind");
928        });
929
930        // Client side.
931        let socket_id = udp_sockets::open("127.0.0.1", 0).expect("client bind");
932        let alloc = allocate(
933            socket_id,
934            &turn_addr.ip().to_string(),
935            turn_addr.port(),
936            USER,
937            PASS,
938            Duration::from_secs(2),
939        )
940        .expect("allocate must succeed against mock");
941        assert_eq!(alloc.realm, REALM);
942        assert_eq!(alloc.nonce, NONCE);
943        assert_eq!(alloc.relay_ip, IpAddr::V4(Ipv4Addr::new(198, 51, 100, 99)));
944        assert_eq!(alloc.relay_port, 49999);
945        assert_eq!(alloc.lifetime_secs, 600);
946
947        let peer_ip = IpAddr::V4(Ipv4Addr::new(192, 0, 2, 50));
948        assert!(
949            create_permission(&alloc, peer_ip, Duration::from_secs(2)),
950            "create_permission must succeed"
951        );
952
953        let sent = send_to_peer(&alloc, peer_ip, 12345, b"hello via turn")
954            .expect("send_to_peer must report bytes sent");
955        assert!(sent > 0);
956
957        let (pip, pport, payload) =
958            recv_indication(&alloc, Duration::from_secs(2)).expect("data ind");
959        assert_eq!(pip, peer_ip);
960        assert_eq!(pport, 12345);
961        assert_eq!(payload, b"hello via turn");
962
963        udp_sockets::close(socket_id);
964    }
965}