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
93fn syn_cookie(socket_id: u32) -> u32 {
98 socket_id
99 .rotate_left(13)
100 .wrapping_mul(0x9E37_79B1)
101 .wrapping_add(0x5247_5421)
102}
103
104pub fn respond(datagram: &[u8]) -> Option<Vec<u8>> {
111 let hs = SrtHandshake::parse(datagram)?;
112 if hs.wants_encryption() {
113 return None; }
115 let mut reply = datagram.to_vec();
116 let cookie = match hs.handshake_type {
117 HandshakeType::Induction => syn_cookie(hs.socket_id),
118 HandshakeType::Conclusion => hs.cookie,
119 _ => return None,
120 };
121 let at = SrtHandshake::PAYLOAD + 28;
123 reply
124 .get_mut(at..at + 4)?
125 .copy_from_slice(&cookie.to_be_bytes());
126 Some(reply)
127}
128
129pub fn caller_handshake(
137 req_type: HandshakeType,
138 socket_id: u32,
139 initial_sequence: u32,
140 cookie: u32,
141) -> Vec<u8> {
142 let mut d = vec![0u8; 16]; d[0] = 0x80;
144 let mut body = vec![0u8; 32];
145 body[0..4].copy_from_slice(&5u32.to_be_bytes()); body[8..12].copy_from_slice(&initial_sequence.to_be_bytes());
148 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());
151 body[24..28].copy_from_slice(&socket_id.to_be_bytes());
152 body[28..32].copy_from_slice(&cookie.to_be_bytes());
153 d.extend_from_slice(&body);
154 d
155}
156
157pub fn caller_induction(socket_id: u32, initial_sequence: u32) -> Vec<u8> {
159 caller_handshake(HandshakeType::Induction, socket_id, initial_sequence, 0)
160}
161
162pub fn caller_conclusion(socket_id: u32, initial_sequence: u32, cookie: u32) -> Vec<u8> {
164 caller_handshake(
165 HandshakeType::Conclusion,
166 socket_id,
167 initial_sequence,
168 cookie,
169 )
170}
171
172#[cfg(feature = "srt-encrypt")]
183const EXT_KMREQ: u16 = 3;
184#[cfg(feature = "srt-encrypt")]
186const EXT_KMRSP: u16 = 4;
187#[cfg(feature = "srt-encrypt")]
189const HS_EXT_KM_FLAG: u16 = 0x0002;
190
191#[cfg(feature = "srt-encrypt")]
194pub fn encryption_field(key_len: usize) -> u16 {
195 (key_len / 8) as u16
196}
197
198#[cfg(feature = "srt-encrypt")]
201fn km_ext_offset(datagram: &[u8]) -> Option<usize> {
202 let mut p = SrtHandshake::PAYLOAD + 32; while p + 4 <= datagram.len() {
204 let ext_type = u16::from_be_bytes([datagram[p], datagram[p + 1]]);
205 let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
206 let end = p + 4 + words * 4;
207 if end > datagram.len() {
208 break;
209 }
210 if ext_type == EXT_KMREQ || ext_type == EXT_KMRSP {
211 return Some(p);
212 }
213 p = end;
214 }
215 None
216}
217
218#[cfg(feature = "srt-encrypt")]
220pub fn km_extension(datagram: &[u8]) -> Option<&[u8]> {
221 let p = km_ext_offset(datagram)?;
222 let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
223 datagram.get(p + 4..p + 4 + words * 4)
224}
225
226#[cfg(feature = "srt-encrypt")]
230pub fn caller_conclusion_encrypted(
231 socket_id: u32,
232 initial_sequence: u32,
233 cookie: u32,
234 key_len: usize,
235 km: &[u8],
236) -> Vec<u8> {
237 let mut d = caller_conclusion(socket_id, initial_sequence, cookie);
238 let body = SrtHandshake::PAYLOAD;
239 d[body + 4..body + 6].copy_from_slice(&encryption_field(key_len).to_be_bytes());
241 d[body + 6..body + 8].copy_from_slice(&HS_EXT_KM_FLAG.to_be_bytes());
242 d.extend_from_slice(&EXT_KMREQ.to_be_bytes());
244 d.extend_from_slice(&((km.len() / 4) as u16).to_be_bytes());
245 d.extend_from_slice(km);
246 d
247}
248
249#[cfg(feature = "srt-encrypt")]
255pub fn respond_with_km(
256 datagram: &[u8],
257 passphrase: &[u8],
258) -> Option<(Vec<u8>, Option<super::keymaterial::KeyMaterial>)> {
259 let hs = SrtHandshake::parse(datagram)?;
260 match hs.handshake_type {
261 HandshakeType::Induction => {
262 let mut reply = datagram.to_vec();
263 let at = SrtHandshake::PAYLOAD + 28;
264 reply
265 .get_mut(at..at + 4)?
266 .copy_from_slice(&syn_cookie(hs.socket_id).to_be_bytes());
267 Some((reply, None))
268 }
269 HandshakeType::Conclusion if hs.wants_encryption() => {
270 let km_bytes = km_extension(datagram)?;
271 let km = super::keymaterial::KeyMaterial::parse(passphrase, km_bytes)?;
272 let mut reply = datagram.to_vec();
274 let off = km_ext_offset(&reply)?;
275 reply[off..off + 2].copy_from_slice(&EXT_KMRSP.to_be_bytes());
276 Some((reply, Some(km)))
277 }
278 HandshakeType::Conclusion => Some((datagram.to_vec(), None)),
279 _ => None,
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn handshake_datagram(req_type: u32, encryption: u16) -> Vec<u8> {
289 let mut d = vec![0u8; 16]; d[0] = 0x80; let mut body = vec![0u8; 32];
292 body[0..4].copy_from_slice(&5u32.to_be_bytes()); body[4..6].copy_from_slice(&encryption.to_be_bytes());
294 body[20..24].copy_from_slice(&req_type.to_be_bytes());
295 body[24..28].copy_from_slice(&0xABCD_1234u32.to_be_bytes()); d.extend_from_slice(&body);
297 d
298 }
299
300 #[test]
301 fn parses_induction_handshake() {
302 let d = handshake_datagram(1, 0);
303 let hs = SrtHandshake::parse(&d).unwrap();
304 assert_eq!(hs.version, 5);
305 assert_eq!(hs.handshake_type, HandshakeType::Induction);
306 assert_eq!(hs.socket_id, 0xABCD_1234);
307 assert!(!hs.wants_encryption());
308 }
309
310 #[test]
311 fn induction_response_installs_nonzero_cookie() {
312 let d = handshake_datagram(1, 0);
313 let reply = respond(&d).unwrap();
314 let parsed = SrtHandshake::parse(&reply).unwrap();
315 assert_ne!(parsed.cookie, 0, "cookie installed in induction response");
316 }
317
318 #[test]
319 fn encrypted_handshake_is_rejected() {
320 let d = handshake_datagram(1, 0x0002);
321 assert!(respond(&d).is_none());
322 }
323
324 #[test]
325 fn non_handshake_request_type_has_no_response() {
326 let d = handshake_datagram(0, 0); assert!(respond(&d).is_none());
328 }
329
330 #[test]
331 fn caller_handshake_loops_through_listener() {
332 let induction = caller_induction(0x0BAD_F00D, 42);
335 let hs = SrtHandshake::parse(&induction).unwrap();
336 assert_eq!(hs.handshake_type, HandshakeType::Induction);
337 assert_eq!(hs.version, 5);
338 assert_eq!(hs.socket_id, 0x0BAD_F00D);
339
340 let resp = respond(&induction).expect("listener induction reply");
341 let cookie = SrtHandshake::parse(&resp).unwrap().cookie;
342 assert_ne!(cookie, 0, "listener installed a cookie");
343
344 let conclusion = caller_conclusion(0x0BAD_F00D, 42, cookie);
345 let chs = SrtHandshake::parse(&conclusion).unwrap();
346 assert_eq!(chs.handshake_type, HandshakeType::Conclusion);
347 assert_eq!(chs.cookie, cookie, "caller echoes the cookie");
348
349 let agree = respond(&conclusion).expect("listener conclusion reply");
350 assert_eq!(SrtHandshake::parse(&agree).unwrap().cookie, cookie);
351 }
352}