arcly_stream/protocol/srt/
handshake.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum HandshakeType {
12 Induction,
14 Conclusion,
16 WaveAHand,
18 Agreement,
20 Other(u32),
22}
23
24impl HandshakeType {
25 fn from_u32(v: u32) -> HandshakeType {
26 match v {
27 1 => HandshakeType::Induction,
28 0xFFFF_FFFF => HandshakeType::Conclusion,
29 0 => HandshakeType::WaveAHand,
30 0xFFFF_FFFE => HandshakeType::Agreement,
31 other => HandshakeType::Other(other),
32 }
33 }
34
35 fn to_u32(self) -> u32 {
36 match self {
37 HandshakeType::Induction => 1,
38 HandshakeType::Conclusion => 0xFFFF_FFFF,
39 HandshakeType::WaveAHand => 0,
40 HandshakeType::Agreement => 0xFFFF_FFFE,
41 HandshakeType::Other(v) => v,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct SrtHandshake {
49 pub version: u32,
51 pub encryption: u16,
54 pub initial_sequence: u32,
56 pub handshake_type: HandshakeType,
58 pub socket_id: u32,
60 pub cookie: u32,
62}
63
64impl SrtHandshake {
65 const PAYLOAD: usize = 16;
68
69 pub fn parse(datagram: &[u8]) -> Option<SrtHandshake> {
72 let b = datagram.get(Self::PAYLOAD..)?;
73 if b.len() < 32 {
74 return None;
75 }
76 let w = |i: usize| u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
77 Some(SrtHandshake {
78 version: w(0),
79 encryption: u16::from_be_bytes([b[4], b[5]]),
80 initial_sequence: w(8),
81 handshake_type: HandshakeType::from_u32(w(20)),
82 socket_id: w(24),
83 cookie: w(28),
84 })
85 }
86
87 pub fn wants_encryption(&self) -> bool {
89 self.encryption != 0
90 }
91}
92
93const EXT_SID: u16 = 5;
95
96pub fn stream_id(datagram: &[u8]) -> Option<String> {
107 let mut p = SrtHandshake::PAYLOAD + 32; while p + 4 <= datagram.len() {
109 let ext_type = u16::from_be_bytes([datagram[p], datagram[p + 1]]);
110 let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
111 let end = p + 4 + words * 4;
112 if end > datagram.len() {
113 break;
114 }
115 if ext_type == EXT_SID {
116 let sid = decode_sid(&datagram[p + 4..end]);
117 return (!sid.is_empty()).then_some(sid);
118 }
119 p = end;
120 }
121 None
122}
123
124fn decode_sid(contents: &[u8]) -> String {
128 let mut out = Vec::with_capacity(contents.len());
129 for word in contents.chunks(4) {
130 out.extend(word.iter().rev());
131 }
132 while out.last() == Some(&0) {
133 out.pop();
134 }
135 String::from_utf8_lossy(&out).into_owned()
136}
137
138fn syn_cookie(socket_id: u32) -> u32 {
143 socket_id
144 .rotate_left(13)
145 .wrapping_mul(0x9E37_79B1)
146 .wrapping_add(0x5247_5421)
147}
148
149pub fn respond(datagram: &[u8]) -> Option<Vec<u8>> {
156 let hs = SrtHandshake::parse(datagram)?;
157 if hs.wants_encryption() {
158 return None; }
160 let mut reply = datagram.to_vec();
161 let cookie = match hs.handshake_type {
162 HandshakeType::Induction => syn_cookie(hs.socket_id),
163 HandshakeType::Conclusion => hs.cookie,
164 _ => return None,
165 };
166 let at = SrtHandshake::PAYLOAD + 28;
168 reply
169 .get_mut(at..at + 4)?
170 .copy_from_slice(&cookie.to_be_bytes());
171 Some(reply)
172}
173
174pub fn caller_handshake(
182 req_type: HandshakeType,
183 socket_id: u32,
184 initial_sequence: u32,
185 cookie: u32,
186) -> Vec<u8> {
187 let mut d = vec![0u8; 16]; d[0] = 0x80;
189 let mut body = vec![0u8; 32];
190 body[0..4].copy_from_slice(&5u32.to_be_bytes()); body[8..12].copy_from_slice(&initial_sequence.to_be_bytes());
193 body[12..16].copy_from_slice(&1500u32.to_be_bytes()); body[16..20].copy_from_slice(&8192u32.to_be_bytes()); body[20..24].copy_from_slice(&req_type.to_u32().to_be_bytes());
196 body[24..28].copy_from_slice(&socket_id.to_be_bytes());
197 body[28..32].copy_from_slice(&cookie.to_be_bytes());
198 d.extend_from_slice(&body);
199 d
200}
201
202pub fn caller_induction(socket_id: u32, initial_sequence: u32) -> Vec<u8> {
204 caller_handshake(HandshakeType::Induction, socket_id, initial_sequence, 0)
205}
206
207pub fn caller_conclusion(socket_id: u32, initial_sequence: u32, cookie: u32) -> Vec<u8> {
209 caller_handshake(
210 HandshakeType::Conclusion,
211 socket_id,
212 initial_sequence,
213 cookie,
214 )
215}
216
217#[cfg(feature = "srt-encrypt")]
228const EXT_KMREQ: u16 = 3;
229#[cfg(feature = "srt-encrypt")]
231const EXT_KMRSP: u16 = 4;
232#[cfg(feature = "srt-encrypt")]
234const HS_EXT_KM_FLAG: u16 = 0x0002;
235
236#[cfg(feature = "srt-encrypt")]
239pub fn encryption_field(key_len: usize) -> u16 {
240 (key_len / 8) as u16
241}
242
243#[cfg(feature = "srt-encrypt")]
246fn km_ext_offset(datagram: &[u8]) -> Option<usize> {
247 let mut p = SrtHandshake::PAYLOAD + 32; while p + 4 <= datagram.len() {
249 let ext_type = u16::from_be_bytes([datagram[p], datagram[p + 1]]);
250 let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
251 let end = p + 4 + words * 4;
252 if end > datagram.len() {
253 break;
254 }
255 if ext_type == EXT_KMREQ || ext_type == EXT_KMRSP {
256 return Some(p);
257 }
258 p = end;
259 }
260 None
261}
262
263#[cfg(feature = "srt-encrypt")]
265pub fn km_extension(datagram: &[u8]) -> Option<&[u8]> {
266 let p = km_ext_offset(datagram)?;
267 let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
268 datagram.get(p + 4..p + 4 + words * 4)
269}
270
271#[cfg(feature = "srt-encrypt")]
275pub fn caller_conclusion_encrypted(
276 socket_id: u32,
277 initial_sequence: u32,
278 cookie: u32,
279 key_len: usize,
280 km: &[u8],
281) -> Vec<u8> {
282 let mut d = caller_conclusion(socket_id, initial_sequence, cookie);
283 let body = SrtHandshake::PAYLOAD;
284 d[body + 4..body + 6].copy_from_slice(&encryption_field(key_len).to_be_bytes());
286 d[body + 6..body + 8].copy_from_slice(&HS_EXT_KM_FLAG.to_be_bytes());
287 d.extend_from_slice(&EXT_KMREQ.to_be_bytes());
289 d.extend_from_slice(&((km.len() / 4) as u16).to_be_bytes());
290 d.extend_from_slice(km);
291 d
292}
293
294#[cfg(feature = "srt-encrypt")]
300pub fn respond_with_km(
301 datagram: &[u8],
302 passphrase: &[u8],
303) -> Option<(Vec<u8>, Option<super::keymaterial::KeyMaterial>)> {
304 let hs = SrtHandshake::parse(datagram)?;
305 match hs.handshake_type {
306 HandshakeType::Induction => {
307 let mut reply = datagram.to_vec();
308 let at = SrtHandshake::PAYLOAD + 28;
309 reply
310 .get_mut(at..at + 4)?
311 .copy_from_slice(&syn_cookie(hs.socket_id).to_be_bytes());
312 Some((reply, None))
313 }
314 HandshakeType::Conclusion if hs.wants_encryption() => {
315 let km_bytes = km_extension(datagram)?;
316 let km = super::keymaterial::KeyMaterial::parse(passphrase, km_bytes)?;
317 let mut reply = datagram.to_vec();
319 let off = km_ext_offset(&reply)?;
320 reply[off..off + 2].copy_from_slice(&EXT_KMRSP.to_be_bytes());
321 Some((reply, Some(km)))
322 }
323 HandshakeType::Conclusion => Some((datagram.to_vec(), None)),
324 _ => None,
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn handshake_datagram(req_type: u32, encryption: u16) -> Vec<u8> {
334 let mut d = vec![0u8; 16]; d[0] = 0x80; let mut body = vec![0u8; 32];
337 body[0..4].copy_from_slice(&5u32.to_be_bytes()); body[4..6].copy_from_slice(&encryption.to_be_bytes());
339 body[20..24].copy_from_slice(&req_type.to_be_bytes());
340 body[24..28].copy_from_slice(&0xABCD_1234u32.to_be_bytes()); d.extend_from_slice(&body);
342 d
343 }
344
345 #[test]
346 fn parses_induction_handshake() {
347 let d = handshake_datagram(1, 0);
348 let hs = SrtHandshake::parse(&d).unwrap();
349 assert_eq!(hs.version, 5);
350 assert_eq!(hs.handshake_type, HandshakeType::Induction);
351 assert_eq!(hs.socket_id, 0xABCD_1234);
352 assert!(!hs.wants_encryption());
353 }
354
355 #[test]
356 fn induction_response_installs_nonzero_cookie() {
357 let d = handshake_datagram(1, 0);
358 let reply = respond(&d).unwrap();
359 let parsed = SrtHandshake::parse(&reply).unwrap();
360 assert_ne!(parsed.cookie, 0, "cookie installed in induction response");
361 }
362
363 #[test]
364 fn encrypted_handshake_is_rejected() {
365 let d = handshake_datagram(1, 0x0002);
366 assert!(respond(&d).is_none());
367 }
368
369 #[test]
370 fn non_handshake_request_type_has_no_response() {
371 let d = handshake_datagram(0, 0); assert!(respond(&d).is_none());
373 }
374
375 #[test]
376 fn caller_handshake_loops_through_listener() {
377 let induction = caller_induction(0x0BAD_F00D, 42);
380 let hs = SrtHandshake::parse(&induction).unwrap();
381 assert_eq!(hs.handshake_type, HandshakeType::Induction);
382 assert_eq!(hs.version, 5);
383 assert_eq!(hs.socket_id, 0x0BAD_F00D);
384
385 let resp = respond(&induction).expect("listener induction reply");
386 let cookie = SrtHandshake::parse(&resp).unwrap().cookie;
387 assert_ne!(cookie, 0, "listener installed a cookie");
388
389 let conclusion = caller_conclusion(0x0BAD_F00D, 42, cookie);
390 let chs = SrtHandshake::parse(&conclusion).unwrap();
391 assert_eq!(chs.handshake_type, HandshakeType::Conclusion);
392 assert_eq!(chs.cookie, cookie, "caller echoes the cookie");
393
394 let agree = respond(&conclusion).expect("listener conclusion reply");
395 assert_eq!(SrtHandshake::parse(&agree).unwrap().cookie, cookie);
396 }
397
398 fn datagram_with_sid(s: &str) -> Vec<u8> {
401 let mut bytes = s.as_bytes().to_vec();
402 while bytes.len() % 4 != 0 {
403 bytes.push(0);
404 }
405 let mut swapped = Vec::with_capacity(bytes.len());
406 for word in bytes.chunks(4) {
407 swapped.extend(word.iter().rev());
408 }
409 let mut d = handshake_datagram(0xFFFF_FFFF, 0); d.extend_from_slice(&EXT_SID.to_be_bytes());
411 d.extend_from_slice(&((swapped.len() / 4) as u16).to_be_bytes());
412 d.extend_from_slice(&swapped);
413 d
414 }
415
416 #[test]
417 fn decodes_stream_id_extension() {
418 let d = datagram_with_sid("live/cam01");
420 assert_eq!(stream_id(&d).as_deref(), Some("live/cam01"));
421
422 let d = datagram_with_sid("abcd");
423 assert_eq!(stream_id(&d).as_deref(), Some("abcd"));
424
425 let d = datagram_with_sid("#!::r=live/feed,m=publish");
427 assert_eq!(stream_id(&d).as_deref(), Some("#!::r=live/feed,m=publish"));
428 }
429
430 #[test]
431 fn absent_stream_id_is_none() {
432 let d = handshake_datagram(0xFFFF_FFFF, 0);
434 assert_eq!(stream_id(&d), None);
435 let mut d = handshake_datagram(0xFFFF_FFFF, 0);
437 d.extend_from_slice(&EXT_SID.to_be_bytes()); assert_eq!(stream_id(&d), None);
439 }
440}