Skip to main content

arcly_stream/protocol/srt/
handshake.rs

1//! SRT handshake parsing and a minimal listener-side responder.
2//!
3//! The handshake control payload follows the 16-byte packet header and carries
4//! the version, encryption/extension fields, sequence number, MTU/window sizes,
5//! the [request type][HandshakeType], the caller's socket id, and a SYN cookie
6//! (draft-sharabayko-srt §3.2.1).
7
8/// SRT handshake request type (the 32-bit `Handshake Type` field).
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum HandshakeType {
12    /// `INDUCTION` (1) — caller's first packet; listener replies with a cookie.
13    Induction,
14    /// `CONCLUSION` (0xFFFFFFFF) — caller echoes the cookie to finish setup.
15    Conclusion,
16    /// `WAVEAHAND` (0) — rendezvous probe.
17    WaveAHand,
18    /// `AGREEMENT` (0xFFFFFFFE) — rendezvous agreement.
19    Agreement,
20    /// Any other / error code.
21    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/// The fields of an SRT handshake this listener inspects.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct SrtHandshake {
49    /// Protocol version (4 for HSv4, 5 for HSv5).
50    pub version: u32,
51    /// Encryption field — non-zero means the caller wants an encrypted link,
52    /// which this build rejects (see the module-level scope note).
53    pub encryption: u16,
54    /// Initial packet sequence number.
55    pub initial_sequence: u32,
56    /// The handshake request type.
57    pub handshake_type: HandshakeType,
58    /// The caller's SRT socket id.
59    pub socket_id: u32,
60    /// SYN cookie (0 on the caller's induction request).
61    pub cookie: u32,
62}
63
64impl SrtHandshake {
65    /// Offset of the handshake payload within a control datagram (past the
66    /// 16-byte SRT packet header).
67    const PAYLOAD: usize = 16;
68
69    /// Parse the handshake from a full control datagram. Returns `None` if the
70    /// datagram is too short to contain a handshake body.
71    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    /// Whether the caller requested an encrypted link (unsupported here).
88    pub fn wants_encryption(&self) -> bool {
89        self.encryption != 0
90    }
91}
92
93/// A deterministic-but-opaque SYN cookie derived from the caller's socket id.
94/// A production listener would mix in the peer address and a per-boot secret;
95/// for an unencrypted loss-tolerant ingest the anti-spoofing value is modest, so
96/// a stable derivation keeps the responder dependency-free.
97fn 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
104/// Build a listener response to a caller's handshake datagram, or `None` if the
105/// datagram is not a parseable handshake or requests encryption.
106///
107/// For an `INDUCTION` request the response echoes the body with a freshly
108/// derived SYN cookie installed; for `CONCLUSION` it echoes the body to confirm
109/// agreement. The returned bytes are ready to send back to the caller.
110pub fn respond(datagram: &[u8]) -> Option<Vec<u8>> {
111    let hs = SrtHandshake::parse(datagram)?;
112    if hs.wants_encryption() {
113        return None; // encrypted SRT is out of scope; drop the handshake
114    }
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    // Install the cookie at its offset (payload + 28).
122    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
129/// Build a caller-side handshake control datagram (the **egress relay** uses
130/// this to dial an SRT listener). Mirrors the 32-byte HSv5 body this crate's
131/// [`respond`] listener understands.
132///
133/// Note: this is the minimal handshake subset (no `HSREQ`/`KMREQ` extension
134/// blocks), so it interoperates with this crate's own listener; full extension
135/// interop with third-party SRT stacks is a follow-up.
136pub 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]; // control header: control flag set, type 0x0000
143    d[0] = 0x80;
144    let mut body = vec![0u8; 32];
145    body[0..4].copy_from_slice(&5u32.to_be_bytes()); // version HSv5
146                                                     // bytes 4..8: encryption(0) + extension(0)
147    body[8..12].copy_from_slice(&initial_sequence.to_be_bytes());
148    body[12..16].copy_from_slice(&1500u32.to_be_bytes()); // MTU
149    body[16..20].copy_from_slice(&8192u32.to_be_bytes()); // flow-window
150    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
157/// The caller's first handshake (`INDUCTION`, cookie 0).
158pub fn caller_induction(socket_id: u32, initial_sequence: u32) -> Vec<u8> {
159    caller_handshake(HandshakeType::Induction, socket_id, initial_sequence, 0)
160}
161
162/// The caller's second handshake (`CONCLUSION`, echoing the listener's cookie).
163pub 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(test)]
173mod tests {
174    use super::*;
175
176    /// Build a control datagram with a handshake body for the given type/enc.
177    fn handshake_datagram(req_type: u32, encryption: u16) -> Vec<u8> {
178        let mut d = vec![0u8; 16]; // control header
179        d[0] = 0x80; // control flag
180        let mut body = vec![0u8; 32];
181        body[0..4].copy_from_slice(&5u32.to_be_bytes()); // version 5
182        body[4..6].copy_from_slice(&encryption.to_be_bytes());
183        body[20..24].copy_from_slice(&req_type.to_be_bytes());
184        body[24..28].copy_from_slice(&0xABCD_1234u32.to_be_bytes()); // socket id
185        d.extend_from_slice(&body);
186        d
187    }
188
189    #[test]
190    fn parses_induction_handshake() {
191        let d = handshake_datagram(1, 0);
192        let hs = SrtHandshake::parse(&d).unwrap();
193        assert_eq!(hs.version, 5);
194        assert_eq!(hs.handshake_type, HandshakeType::Induction);
195        assert_eq!(hs.socket_id, 0xABCD_1234);
196        assert!(!hs.wants_encryption());
197    }
198
199    #[test]
200    fn induction_response_installs_nonzero_cookie() {
201        let d = handshake_datagram(1, 0);
202        let reply = respond(&d).unwrap();
203        let parsed = SrtHandshake::parse(&reply).unwrap();
204        assert_ne!(parsed.cookie, 0, "cookie installed in induction response");
205    }
206
207    #[test]
208    fn encrypted_handshake_is_rejected() {
209        let d = handshake_datagram(1, 0x0002);
210        assert!(respond(&d).is_none());
211    }
212
213    #[test]
214    fn non_handshake_request_type_has_no_response() {
215        let d = handshake_datagram(0, 0); // WAVEAHAND
216        assert!(respond(&d).is_none());
217    }
218
219    #[test]
220    fn caller_handshake_loops_through_listener() {
221        // Full in-process loopback: caller INDUCTION → listener cookie →
222        // caller CONCLUSION → listener agreement, all over the wire format.
223        let induction = caller_induction(0x0BAD_F00D, 42);
224        let hs = SrtHandshake::parse(&induction).unwrap();
225        assert_eq!(hs.handshake_type, HandshakeType::Induction);
226        assert_eq!(hs.version, 5);
227        assert_eq!(hs.socket_id, 0x0BAD_F00D);
228
229        let resp = respond(&induction).expect("listener induction reply");
230        let cookie = SrtHandshake::parse(&resp).unwrap().cookie;
231        assert_ne!(cookie, 0, "listener installed a cookie");
232
233        let conclusion = caller_conclusion(0x0BAD_F00D, 42, cookie);
234        let chs = SrtHandshake::parse(&conclusion).unwrap();
235        assert_eq!(chs.handshake_type, HandshakeType::Conclusion);
236        assert_eq!(chs.cookie, cookie, "caller echoes the cookie");
237
238        let agree = respond(&conclusion).expect("listener conclusion reply");
239        assert_eq!(SrtHandshake::parse(&agree).unwrap().cookie, cookie);
240    }
241}