#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HandshakeType {
Induction,
Conclusion,
WaveAHand,
Agreement,
Other(u32),
}
impl HandshakeType {
fn from_u32(v: u32) -> HandshakeType {
match v {
1 => HandshakeType::Induction,
0xFFFF_FFFF => HandshakeType::Conclusion,
0 => HandshakeType::WaveAHand,
0xFFFF_FFFE => HandshakeType::Agreement,
other => HandshakeType::Other(other),
}
}
fn to_u32(self) -> u32 {
match self {
HandshakeType::Induction => 1,
HandshakeType::Conclusion => 0xFFFF_FFFF,
HandshakeType::WaveAHand => 0,
HandshakeType::Agreement => 0xFFFF_FFFE,
HandshakeType::Other(v) => v,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SrtHandshake {
pub version: u32,
pub encryption: u16,
pub initial_sequence: u32,
pub handshake_type: HandshakeType,
pub socket_id: u32,
pub cookie: u32,
}
impl SrtHandshake {
const PAYLOAD: usize = 16;
pub fn parse(datagram: &[u8]) -> Option<SrtHandshake> {
let b = datagram.get(Self::PAYLOAD..)?;
if b.len() < 32 {
return None;
}
let w = |i: usize| u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
Some(SrtHandshake {
version: w(0),
encryption: u16::from_be_bytes([b[4], b[5]]),
initial_sequence: w(8),
handshake_type: HandshakeType::from_u32(w(20)),
socket_id: w(24),
cookie: w(28),
})
}
pub fn wants_encryption(&self) -> bool {
self.encryption != 0
}
}
const EXT_SID: u16 = 5;
pub fn stream_id(datagram: &[u8]) -> Option<String> {
let mut p = SrtHandshake::PAYLOAD + 32; while p + 4 <= datagram.len() {
let ext_type = u16::from_be_bytes([datagram[p], datagram[p + 1]]);
let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
let end = p + 4 + words * 4;
if end > datagram.len() {
break;
}
if ext_type == EXT_SID {
let sid = decode_sid(&datagram[p + 4..end]);
return (!sid.is_empty()).then_some(sid);
}
p = end;
}
None
}
fn decode_sid(contents: &[u8]) -> String {
let mut out = Vec::with_capacity(contents.len());
for word in contents.chunks(4) {
out.extend(word.iter().rev());
}
while out.last() == Some(&0) {
out.pop();
}
String::from_utf8_lossy(&out).into_owned()
}
fn syn_cookie(socket_id: u32) -> u32 {
socket_id
.rotate_left(13)
.wrapping_mul(0x9E37_79B1)
.wrapping_add(0x5247_5421)
}
pub fn respond(datagram: &[u8]) -> Option<Vec<u8>> {
let hs = SrtHandshake::parse(datagram)?;
if hs.wants_encryption() {
return None; }
let mut reply = datagram.to_vec();
let cookie = match hs.handshake_type {
HandshakeType::Induction => syn_cookie(hs.socket_id),
HandshakeType::Conclusion => hs.cookie,
_ => return None,
};
let at = SrtHandshake::PAYLOAD + 28;
reply
.get_mut(at..at + 4)?
.copy_from_slice(&cookie.to_be_bytes());
Some(reply)
}
pub fn caller_handshake(
req_type: HandshakeType,
socket_id: u32,
initial_sequence: u32,
cookie: u32,
) -> Vec<u8> {
let mut d = vec![0u8; 16]; d[0] = 0x80;
let mut body = vec![0u8; 32];
body[0..4].copy_from_slice(&5u32.to_be_bytes()); body[8..12].copy_from_slice(&initial_sequence.to_be_bytes());
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());
body[24..28].copy_from_slice(&socket_id.to_be_bytes());
body[28..32].copy_from_slice(&cookie.to_be_bytes());
d.extend_from_slice(&body);
d
}
pub fn caller_induction(socket_id: u32, initial_sequence: u32) -> Vec<u8> {
caller_handshake(HandshakeType::Induction, socket_id, initial_sequence, 0)
}
pub fn caller_conclusion(socket_id: u32, initial_sequence: u32, cookie: u32) -> Vec<u8> {
caller_handshake(
HandshakeType::Conclusion,
socket_id,
initial_sequence,
cookie,
)
}
#[cfg(feature = "srt-encrypt")]
const EXT_KMREQ: u16 = 3;
#[cfg(feature = "srt-encrypt")]
const EXT_KMRSP: u16 = 4;
#[cfg(feature = "srt-encrypt")]
const HS_EXT_KM_FLAG: u16 = 0x0002;
#[cfg(feature = "srt-encrypt")]
pub fn encryption_field(key_len: usize) -> u16 {
(key_len / 8) as u16
}
#[cfg(feature = "srt-encrypt")]
fn km_ext_offset(datagram: &[u8]) -> Option<usize> {
let mut p = SrtHandshake::PAYLOAD + 32; while p + 4 <= datagram.len() {
let ext_type = u16::from_be_bytes([datagram[p], datagram[p + 1]]);
let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
let end = p + 4 + words * 4;
if end > datagram.len() {
break;
}
if ext_type == EXT_KMREQ || ext_type == EXT_KMRSP {
return Some(p);
}
p = end;
}
None
}
#[cfg(feature = "srt-encrypt")]
pub fn km_extension(datagram: &[u8]) -> Option<&[u8]> {
let p = km_ext_offset(datagram)?;
let words = u16::from_be_bytes([datagram[p + 2], datagram[p + 3]]) as usize;
datagram.get(p + 4..p + 4 + words * 4)
}
#[cfg(feature = "srt-encrypt")]
pub fn caller_conclusion_encrypted(
socket_id: u32,
initial_sequence: u32,
cookie: u32,
key_len: usize,
km: &[u8],
) -> Vec<u8> {
let mut d = caller_conclusion(socket_id, initial_sequence, cookie);
let body = SrtHandshake::PAYLOAD;
d[body + 4..body + 6].copy_from_slice(&encryption_field(key_len).to_be_bytes());
d[body + 6..body + 8].copy_from_slice(&HS_EXT_KM_FLAG.to_be_bytes());
d.extend_from_slice(&EXT_KMREQ.to_be_bytes());
d.extend_from_slice(&((km.len() / 4) as u16).to_be_bytes());
d.extend_from_slice(km);
d
}
#[cfg(feature = "srt-encrypt")]
pub fn respond_with_km(
datagram: &[u8],
passphrase: &[u8],
) -> Option<(Vec<u8>, Option<super::keymaterial::KeyMaterial>)> {
let hs = SrtHandshake::parse(datagram)?;
match hs.handshake_type {
HandshakeType::Induction => {
let mut reply = datagram.to_vec();
let at = SrtHandshake::PAYLOAD + 28;
reply
.get_mut(at..at + 4)?
.copy_from_slice(&syn_cookie(hs.socket_id).to_be_bytes());
Some((reply, None))
}
HandshakeType::Conclusion if hs.wants_encryption() => {
let km_bytes = km_extension(datagram)?;
let km = super::keymaterial::KeyMaterial::parse(passphrase, km_bytes)?;
let mut reply = datagram.to_vec();
let off = km_ext_offset(&reply)?;
reply[off..off + 2].copy_from_slice(&EXT_KMRSP.to_be_bytes());
Some((reply, Some(km)))
}
HandshakeType::Conclusion => Some((datagram.to_vec(), None)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn handshake_datagram(req_type: u32, encryption: u16) -> Vec<u8> {
let mut d = vec![0u8; 16]; d[0] = 0x80; let mut body = vec![0u8; 32];
body[0..4].copy_from_slice(&5u32.to_be_bytes()); body[4..6].copy_from_slice(&encryption.to_be_bytes());
body[20..24].copy_from_slice(&req_type.to_be_bytes());
body[24..28].copy_from_slice(&0xABCD_1234u32.to_be_bytes()); d.extend_from_slice(&body);
d
}
#[test]
fn parses_induction_handshake() {
let d = handshake_datagram(1, 0);
let hs = SrtHandshake::parse(&d).unwrap();
assert_eq!(hs.version, 5);
assert_eq!(hs.handshake_type, HandshakeType::Induction);
assert_eq!(hs.socket_id, 0xABCD_1234);
assert!(!hs.wants_encryption());
}
#[test]
fn induction_response_installs_nonzero_cookie() {
let d = handshake_datagram(1, 0);
let reply = respond(&d).unwrap();
let parsed = SrtHandshake::parse(&reply).unwrap();
assert_ne!(parsed.cookie, 0, "cookie installed in induction response");
}
#[test]
fn encrypted_handshake_is_rejected() {
let d = handshake_datagram(1, 0x0002);
assert!(respond(&d).is_none());
}
#[test]
fn non_handshake_request_type_has_no_response() {
let d = handshake_datagram(0, 0); assert!(respond(&d).is_none());
}
#[test]
fn caller_handshake_loops_through_listener() {
let induction = caller_induction(0x0BAD_F00D, 42);
let hs = SrtHandshake::parse(&induction).unwrap();
assert_eq!(hs.handshake_type, HandshakeType::Induction);
assert_eq!(hs.version, 5);
assert_eq!(hs.socket_id, 0x0BAD_F00D);
let resp = respond(&induction).expect("listener induction reply");
let cookie = SrtHandshake::parse(&resp).unwrap().cookie;
assert_ne!(cookie, 0, "listener installed a cookie");
let conclusion = caller_conclusion(0x0BAD_F00D, 42, cookie);
let chs = SrtHandshake::parse(&conclusion).unwrap();
assert_eq!(chs.handshake_type, HandshakeType::Conclusion);
assert_eq!(chs.cookie, cookie, "caller echoes the cookie");
let agree = respond(&conclusion).expect("listener conclusion reply");
assert_eq!(SrtHandshake::parse(&agree).unwrap().cookie, cookie);
}
fn datagram_with_sid(s: &str) -> Vec<u8> {
let mut bytes = s.as_bytes().to_vec();
while bytes.len() % 4 != 0 {
bytes.push(0);
}
let mut swapped = Vec::with_capacity(bytes.len());
for word in bytes.chunks(4) {
swapped.extend(word.iter().rev());
}
let mut d = handshake_datagram(0xFFFF_FFFF, 0); d.extend_from_slice(&EXT_SID.to_be_bytes());
d.extend_from_slice(&((swapped.len() / 4) as u16).to_be_bytes());
d.extend_from_slice(&swapped);
d
}
#[test]
fn decodes_stream_id_extension() {
let d = datagram_with_sid("live/cam01");
assert_eq!(stream_id(&d).as_deref(), Some("live/cam01"));
let d = datagram_with_sid("abcd");
assert_eq!(stream_id(&d).as_deref(), Some("abcd"));
let d = datagram_with_sid("#!::r=live/feed,m=publish");
assert_eq!(stream_id(&d).as_deref(), Some("#!::r=live/feed,m=publish"));
}
#[test]
fn absent_stream_id_is_none() {
let d = handshake_datagram(0xFFFF_FFFF, 0);
assert_eq!(stream_id(&d), None);
let mut d = handshake_datagram(0xFFFF_FFFF, 0);
d.extend_from_slice(&EXT_SID.to_be_bytes()); assert_eq!(stream_id(&d), None);
}
}