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
36/// The fields of an SRT handshake this listener inspects.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct SrtHandshake {
39    /// Protocol version (4 for HSv4, 5 for HSv5).
40    pub version: u32,
41    /// Encryption field — non-zero means the caller wants an encrypted link,
42    /// which this build rejects (see the module-level scope note).
43    pub encryption: u16,
44    /// Initial packet sequence number.
45    pub initial_sequence: u32,
46    /// The handshake request type.
47    pub handshake_type: HandshakeType,
48    /// The caller's SRT socket id.
49    pub socket_id: u32,
50    /// SYN cookie (0 on the caller's induction request).
51    pub cookie: u32,
52}
53
54impl SrtHandshake {
55    /// Offset of the handshake payload within a control datagram (past the
56    /// 16-byte SRT packet header).
57    const PAYLOAD: usize = 16;
58
59    /// Parse the handshake from a full control datagram. Returns `None` if the
60    /// datagram is too short to contain a handshake body.
61    pub fn parse(datagram: &[u8]) -> Option<SrtHandshake> {
62        let b = datagram.get(Self::PAYLOAD..)?;
63        if b.len() < 32 {
64            return None;
65        }
66        let w = |i: usize| u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
67        Some(SrtHandshake {
68            version: w(0),
69            encryption: u16::from_be_bytes([b[4], b[5]]),
70            initial_sequence: w(8),
71            handshake_type: HandshakeType::from_u32(w(20)),
72            socket_id: w(24),
73            cookie: w(28),
74        })
75    }
76
77    /// Whether the caller requested an encrypted link (unsupported here).
78    pub fn wants_encryption(&self) -> bool {
79        self.encryption != 0
80    }
81}
82
83/// A deterministic-but-opaque SYN cookie derived from the caller's socket id.
84/// A production listener would mix in the peer address and a per-boot secret;
85/// for an unencrypted loss-tolerant ingest the anti-spoofing value is modest, so
86/// a stable derivation keeps the responder dependency-free.
87fn syn_cookie(socket_id: u32) -> u32 {
88    socket_id
89        .rotate_left(13)
90        .wrapping_mul(0x9E37_79B1)
91        .wrapping_add(0x5247_5421)
92}
93
94/// Build a listener response to a caller's handshake datagram, or `None` if the
95/// datagram is not a parseable handshake or requests encryption.
96///
97/// For an `INDUCTION` request the response echoes the body with a freshly
98/// derived SYN cookie installed; for `CONCLUSION` it echoes the body to confirm
99/// agreement. The returned bytes are ready to send back to the caller.
100pub fn respond(datagram: &[u8]) -> Option<Vec<u8>> {
101    let hs = SrtHandshake::parse(datagram)?;
102    if hs.wants_encryption() {
103        return None; // encrypted SRT is out of scope; drop the handshake
104    }
105    let mut reply = datagram.to_vec();
106    let cookie = match hs.handshake_type {
107        HandshakeType::Induction => syn_cookie(hs.socket_id),
108        HandshakeType::Conclusion => hs.cookie,
109        _ => return None,
110    };
111    // Install the cookie at its offset (payload + 28).
112    let at = SrtHandshake::PAYLOAD + 28;
113    reply
114        .get_mut(at..at + 4)?
115        .copy_from_slice(&cookie.to_be_bytes());
116    Some(reply)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    /// Build a control datagram with a handshake body for the given type/enc.
124    fn handshake_datagram(req_type: u32, encryption: u16) -> Vec<u8> {
125        let mut d = vec![0u8; 16]; // control header
126        d[0] = 0x80; // control flag
127        let mut body = vec![0u8; 32];
128        body[0..4].copy_from_slice(&5u32.to_be_bytes()); // version 5
129        body[4..6].copy_from_slice(&encryption.to_be_bytes());
130        body[20..24].copy_from_slice(&req_type.to_be_bytes());
131        body[24..28].copy_from_slice(&0xABCD_1234u32.to_be_bytes()); // socket id
132        d.extend_from_slice(&body);
133        d
134    }
135
136    #[test]
137    fn parses_induction_handshake() {
138        let d = handshake_datagram(1, 0);
139        let hs = SrtHandshake::parse(&d).unwrap();
140        assert_eq!(hs.version, 5);
141        assert_eq!(hs.handshake_type, HandshakeType::Induction);
142        assert_eq!(hs.socket_id, 0xABCD_1234);
143        assert!(!hs.wants_encryption());
144    }
145
146    #[test]
147    fn induction_response_installs_nonzero_cookie() {
148        let d = handshake_datagram(1, 0);
149        let reply = respond(&d).unwrap();
150        let parsed = SrtHandshake::parse(&reply).unwrap();
151        assert_ne!(parsed.cookie, 0, "cookie installed in induction response");
152    }
153
154    #[test]
155    fn encrypted_handshake_is_rejected() {
156        let d = handshake_datagram(1, 0x0002);
157        assert!(respond(&d).is_none());
158    }
159
160    #[test]
161    fn non_handshake_request_type_has_no_response() {
162        let d = handshake_datagram(0, 0); // WAVEAHAND
163        assert!(respond(&d).is_none());
164    }
165}