use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
type HmacSha1 = Hmac<Sha1>;
const STUN_MAGIC: u32 = 0x2112_a442;
const STUN_FINGERPRINT_XOR: u32 = 0x5354_554e;
const STUN_XOR_PORT: u16 = 0x2112;
const STUN_XOR_ADDR: [u8; 4] = [0x21, 0x12, 0xa4, 0x42];
const ATTR_MESSAGE_INTEGRITY: u16 = 0x0008;
const ATTR_FINGERPRINT: u16 = 0x8028;
const ATTR_ERROR_CODE: u16 = 0x0009;
const ATTR_RELAY_TOKEN: u16 = 0x4000;
const STUN_ATTR_STREAM_DESCRIPTORS: u16 = 0x4024;
const STUN_ATTR_RECEIVER_SUBSCRIPTIONS: u16 = 0x4021;
const STUN_ATTR_PARTICIPANT_COUNT: u16 = 0x805a;
const STUN_ATTR_WASM_RELAY_ENDPOINT: u16 = 0x0016;
pub const MSG_BINDING_REQUEST: u16 = 0x0001;
pub const MSG_ALLOCATE_REQUEST: u16 = 0x0003;
pub const MSG_BINDING_SUCCESS: u16 = 0x0101;
pub const MSG_BINDING_ERROR: u16 = 0x0111;
pub const MSG_ALLOCATE_SUCCESS: u16 = 0x0103;
pub const MSG_ALLOCATE_ERROR: u16 = 0x0113;
pub const MSG_WHATSAPP_PING: u16 = 0x0801;
pub const MSG_WHATSAPP_PONG: u16 = 0x0802;
fn pad4(n: usize) -> usize {
(4 - (n % 4)) % 4
}
fn stun_attr(attr_type: u16, value: &[u8]) -> Vec<u8> {
let pad = pad4(value.len());
let mut buf = Vec::with_capacity(4 + value.len() + pad);
buf.extend_from_slice(&attr_type.to_be_bytes());
buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
buf.extend_from_slice(value);
buf.resize(buf.len() + pad, 0);
buf
}
fn crc32(buf: &[u8]) -> u32 {
let mut crc: u32 = 0xffff_ffff;
for &b in buf {
crc ^= b as u32;
for _ in 0..8 {
crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
}
}
!crc
}
fn stun_pseudo_header(msg_type: u16, msg_len: u16, transaction_id: &[u8; 12]) -> [u8; 20] {
let mut h = [0u8; 20];
h[0..2].copy_from_slice(&msg_type.to_be_bytes());
h[2..4].copy_from_slice(&msg_len.to_be_bytes());
h[4..8].copy_from_slice(&STUN_MAGIC.to_be_bytes());
h[8..20].copy_from_slice(transaction_id);
h
}
pub fn encode_stun_request(
msg_type: u16,
transaction_id: &[u8; 12],
attrs: &[u8],
integrity_key: Option<&[u8]>,
include_fingerprint: bool,
) -> Vec<u8> {
let mut body = attrs.to_vec();
if let Some(key) = integrity_key {
let msg_len = (body.len() + 24) as u16; let header = stun_pseudo_header(msg_type, msg_len, transaction_id);
let mut mac = HmacSha1::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(&header);
mac.update(&body);
let mi = mac.finalize().into_bytes(); body.extend_from_slice(&stun_attr(ATTR_MESSAGE_INTEGRITY, &mi));
}
if include_fingerprint {
let msg_len = (body.len() + 8) as u16; let header = stun_pseudo_header(msg_type, msg_len, transaction_id);
let mut crc_input = Vec::with_capacity(20 + body.len());
crc_input.extend_from_slice(&header);
crc_input.extend_from_slice(&body);
let fp = crc32(&crc_input) ^ STUN_FINGERPRINT_XOR;
body.extend_from_slice(&stun_attr(ATTR_FINGERPRINT, &fp.to_be_bytes()));
}
let mut out = Vec::with_capacity(20 + body.len());
out.extend_from_slice(&msg_type.to_be_bytes());
out.extend_from_slice(&(body.len() as u16).to_be_bytes());
out.extend_from_slice(&STUN_MAGIC.to_be_bytes());
out.extend_from_slice(transaction_id);
out.extend_from_slice(&body);
out
}
pub fn create_native_sender_subscription(ssrc: u32) -> [u8; 5] {
let mut buf = [0u8; 5];
buf[0] = 1;
buf[1..5].copy_from_slice(&ssrc.to_be_bytes());
buf
}
pub fn encode_xor_relay_endpoint(ipv4: &str, port: u16) -> Option<[u8; 6]> {
let octets: Vec<u8> = ipv4
.split('.')
.filter_map(|n| n.parse::<u8>().ok())
.collect();
if octets.len() != 4 {
return None;
}
let xor_port = port ^ STUN_XOR_PORT;
let mut buf = [0u8; 6];
buf[0..2].copy_from_slice(&xor_port.to_be_bytes());
for i in 0..4 {
buf[2 + i] = octets[i] ^ STUN_XOR_ADDR[i];
}
Some(buf)
}
fn create_wasm_relay_endpoint_attr(endpoint_xor: &[u8; 6]) -> [u8; 8] {
let mut buf = [0u8; 8];
buf[0..2].copy_from_slice(&1u16.to_be_bytes());
buf[2..8].copy_from_slice(endpoint_xor);
buf
}
const WASM_STREAM_SLOTS: [(u32, u32, u32); 9] = [
(0, 0, 0),
(0, 1, 1),
(0, 2, 4),
(1, 0, 2),
(1, 1, 3),
(1, 2, 5),
(2, 0, 7),
(2, 1, 8),
(2, 2, 6),
];
#[cfg(test)]
pub(crate) fn wasm_stream_slot_words() -> [u32; 9] {
WASM_STREAM_SLOTS.map(|(_, _, slot)| slot)
}
pub fn create_wasm_stream_descriptors(call_id: &str, self_participant_id: &str) -> Vec<u8> {
let ssrcs = WASM_STREAM_SLOTS.map(|(_, _, slot)| {
crate::voip::ssrc::derive_wasm_participant_ssrc(call_id, self_participant_id, slot)
});
create_wasm_stream_descriptors_from_ssrcs(&ssrcs, &[0, 0])
}
pub fn create_wasm_stream_descriptors_from_ssrcs(
stream_ssrcs: &[u32; 9],
hbh_fec_ssrcs: &[u32; 2],
) -> Vec<u8> {
let mut out = Vec::new();
for ((stream_index, sub_type, _), ssrc) in
WASM_STREAM_SLOTS.iter().zip(stream_ssrcs.iter().copied())
{
if ssrc == 0 {
continue;
}
let mut d = Vec::new();
if *stream_index != 0 {
pb_tag(&mut d, 1, 0);
pb_varint(&mut d, *stream_index as u64);
}
if *sub_type != 0 {
pb_tag(&mut d, 2, 0);
pb_varint(&mut d, *sub_type as u64);
}
pb_tag(&mut d, 3, 0);
pb_varint(&mut d, ssrc as u64);
pb_len_delim(&mut out, 1, &d);
}
for (index, ssrc) in hbh_fec_ssrcs.iter().copied().enumerate() {
if ssrc == 0 {
continue;
}
let mut descriptor = Vec::new();
pb_tag(&mut descriptor, 1, 0);
pb_varint(&mut descriptor, (index + 3) as u64);
pb_tag(&mut descriptor, 2, 0);
pb_varint(&mut descriptor, 3);
pb_tag(&mut descriptor, 3, 0);
pb_varint(&mut descriptor, ssrc as u64);
pb_len_delim(&mut out, 1, &descriptor);
}
out
}
pub fn create_wasm_group_sender_subscriptions(
stream_ssrcs: &[u32; 9],
app_data_ssrc: u32,
participant_pids: &[u32],
) -> Vec<u8> {
let pids = normalized_pids(participant_pids);
let mut out = Vec::new();
out.extend(create_wasm_sender_subscription(
&stream_ssrcs[3..6],
&pids,
true,
));
out.extend(create_wasm_sender_subscription(
&stream_ssrcs[6..9],
&[],
false,
));
out.extend(create_wasm_sender_subscription(
&stream_ssrcs[0..3],
&pids,
false,
));
out.extend(create_wasm_sender_subscription(
&[app_data_ssrc],
&pids,
false,
));
out
}
pub fn create_wasm_group_receiver_subscriptions(participant_pids: &[u32]) -> Vec<u8> {
let mut out = Vec::new();
for pid in normalized_pids(participant_pids) {
let mut participant = Vec::new();
pb_tag(&mut participant, 1, 0);
pb_varint(&mut participant, pid as u64);
pb_len_delim(&mut out, 2, &participant);
}
out
}
fn create_wasm_sender_subscription(
ssrcs: &[u32],
participant_pids: &[u32],
video: bool,
) -> Vec<u8> {
let mut packed_ssrcs = Vec::new();
for ssrc in ssrcs.iter().copied().filter(|ssrc| *ssrc != 0) {
pb_varint(&mut packed_ssrcs, ssrc as u64);
}
let mut subscription = Vec::new();
pb_len_delim(&mut subscription, 1, &packed_ssrcs);
for pid in participant_pids {
let mut participant = Vec::new();
pb_tag(&mut participant, 1, 0);
pb_varint(&mut participant, *pid as u64);
if video {
pb_tag(&mut participant, 2, 0);
pb_varint(&mut participant, 1);
}
pb_len_delim(&mut subscription, 2, &participant);
}
let mut wrapper = Vec::new();
pb_len_delim(&mut wrapper, 1, &subscription);
let mut out = Vec::new();
pb_len_delim(&mut out, 1, &wrapper);
out
}
fn normalized_pids(participant_pids: &[u32]) -> Vec<u32> {
let mut pids = participant_pids.to_vec();
pids.sort_unstable();
pids.dedup();
pids
}
pub struct WasmGroupStunAllocateRequest<'a> {
pub transaction_id: &'a [u8; 12],
pub relay_token: &'a [u8],
pub endpoint_xor: &'a [u8; 6],
pub integrity_key: &'a [u8],
pub stream_ssrcs: &'a [u32; 9],
pub app_data_ssrc: u32,
pub hbh_fec_ssrcs: &'a [u32; 2],
pub participant_pids: &'a [u32],
}
pub fn build_wasm_group_stun_allocate_request(
request: &WasmGroupStunAllocateRequest<'_>,
) -> Vec<u8> {
let pids = normalized_pids(request.participant_pids);
let descriptors = create_wasm_stream_descriptors_from_ssrcs(
request.stream_ssrcs,
if pids.len() > 1 {
request.hbh_fec_ssrcs
} else {
&[0, 0]
},
);
let mut attrs = stun_attr(ATTR_RELAY_TOKEN, request.relay_token);
if !pids.is_empty() {
attrs.extend_from_slice(&stun_attr(
ATTR_SENDER_SUBSCRIPTIONS_V2,
&create_wasm_group_sender_subscriptions(
request.stream_ssrcs,
request.app_data_ssrc,
&pids,
),
));
attrs.extend_from_slice(&stun_attr(
STUN_ATTR_RECEIVER_SUBSCRIPTIONS,
&create_wasm_group_receiver_subscriptions(&pids),
));
}
attrs.extend_from_slice(&stun_attr(STUN_ATTR_STREAM_DESCRIPTORS, &descriptors));
if !pids.is_empty() {
let mut participant_count = Vec::new();
pb_varint(&mut participant_count, pids.len() as u64);
attrs.extend_from_slice(&stun_attr(STUN_ATTR_PARTICIPANT_COUNT, &participant_count));
}
attrs.extend_from_slice(&stun_attr(
STUN_ATTR_WASM_RELAY_ENDPOINT,
&create_wasm_relay_endpoint_attr(request.endpoint_xor),
));
encode_stun_request(
MSG_ALLOCATE_REQUEST,
request.transaction_id,
&attrs,
Some(request.integrity_key),
false,
)
}
pub fn build_wasm_stun_allocate_request(
transaction_id: &[u8; 12],
relay_token: &[u8],
endpoint_xor: &[u8; 6],
integrity_key: &[u8],
call_id: &str,
self_participant_id: &str,
) -> Vec<u8> {
let mut attrs = stun_attr(ATTR_RELAY_TOKEN, relay_token);
attrs.extend_from_slice(&stun_attr(
STUN_ATTR_STREAM_DESCRIPTORS,
&create_wasm_stream_descriptors(call_id, self_participant_id),
));
attrs.extend_from_slice(&stun_attr(
STUN_ATTR_WASM_RELAY_ENDPOINT,
&create_wasm_relay_endpoint_attr(endpoint_xor),
));
encode_stun_request(
MSG_ALLOCATE_REQUEST,
transaction_id,
&attrs,
Some(integrity_key),
false,
)
}
pub fn build_whatsapp_ping(transaction_id: &[u8; 12]) -> [u8; 20] {
let mut out = [0u8; 20];
out[0..2].copy_from_slice(&MSG_WHATSAPP_PING.to_be_bytes());
out[4..8].copy_from_slice(&STUN_MAGIC.to_be_bytes());
out[8..20].copy_from_slice(transaction_id);
out
}
pub fn is_stun_packet(data: &[u8]) -> bool {
data.len() >= 2 && (data[0] & 0xc0) == 0x00
}
pub fn stun_message_type(data: &[u8]) -> Option<u16> {
(data.len() >= 2).then(|| (((data[0] & 0x3f) as u16) << 8) | data[1] as u16)
}
pub fn stun_transaction_id(data: &[u8]) -> Option<&[u8]> {
(data.len() >= 20).then(|| &data[8..20])
}
fn is_complete_stun(data: &[u8]) -> bool {
if !(is_stun_packet(data) && data.len() >= 20 && data[4..8] == STUN_MAGIC.to_be_bytes()) {
return false;
}
let body_len = ((data[2] as usize) << 8) | data[3] as usize;
body_len.is_multiple_of(4) && data.len() >= 20 + body_len
}
pub fn is_allocate_or_binding_success(data: &[u8]) -> bool {
is_complete_stun(data)
&& matches!(
stun_message_type(data),
Some(MSG_ALLOCATE_SUCCESS | MSG_BINDING_SUCCESS)
)
}
pub fn is_allocate_error(data: &[u8]) -> bool {
is_complete_stun(data) && stun_message_type(data) == Some(MSG_ALLOCATE_ERROR)
}
pub fn is_whatsapp_pong(data: &[u8], transaction_id: Option<&[u8]>) -> bool {
if !is_stun_packet(data) || stun_message_type(data) != Some(MSG_WHATSAPP_PONG) {
return false;
}
match transaction_id {
None | Some(&[]) => true,
Some(want) => stun_transaction_id(data) == Some(want),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StunAttribute<'a> {
pub attr_type: u16,
pub value: &'a [u8],
}
#[derive(zerocopy::FromBytes, zerocopy::KnownLayout, zerocopy::Immutable, zerocopy::Unaligned)]
#[repr(C)]
struct StunAttrHeader {
attr_type: zerocopy::big_endian::U16,
length: zerocopy::big_endian::U16,
}
pub fn parse_stun_attributes(data: &[u8]) -> Vec<StunAttribute<'_>> {
if !is_stun_packet(data) || data.len() < 20 {
return Vec::new();
}
let mut attrs = Vec::new();
let mut off = 20;
while let Some(rest) = data.get(off..)
&& let Ok((hdr, _)) = zerocopy::Ref::<_, StunAttrHeader>::from_prefix(rest)
{
let len = hdr.length.get() as usize;
off += 4;
if off + len > data.len() {
break;
}
attrs.push(StunAttribute {
attr_type: hdr.attr_type.get(),
value: &data[off..off + len],
});
off += len + pad4(len);
}
attrs
}
pub fn parse_stun_error_code(data: &[u8]) -> Option<u16> {
if !is_complete_stun(data) {
return None;
}
let t = stun_message_type(data)?;
if t != MSG_ALLOCATE_ERROR && t != MSG_BINDING_ERROR {
return None;
}
let body_len = ((data[2] as usize) << 8) | data[3] as usize;
let end = (20 + body_len).min(data.len());
let mut off = 20;
while off + 4 <= end {
let attr_type = ((data[off] as u16) << 8) | data[off + 1] as u16;
let len = ((data[off + 2] as usize) << 8) | data[off + 3] as usize;
if attr_type == ATTR_ERROR_CODE && len >= 4 && off + 8 <= end {
let class = data[off + 6] as u16;
let number = data[off + 7] as u16;
return Some(class * 100 + number);
}
off += 4 + len + pad4(len);
}
None
}
const ATTR_SENDER_SUBSCRIPTIONS_V2: u16 = 0x4025;
use crate::voip::encode_varint as pb_varint;
fn pb_tag(out: &mut Vec<u8>, field: u32, wire: u32) {
pb_varint(out, ((field << 3) | wire) as u64);
}
fn pb_zigzag(n: i64) -> u64 {
((n << 1) ^ (n >> 63)) as u64
}
fn pb_len_delim(out: &mut Vec<u8>, field: u32, bytes: &[u8]) {
pb_tag(out, field, 2);
pb_varint(out, bytes.len() as u64);
out.extend_from_slice(bytes);
}
pub fn create_voip_sender_subscriptions(ssrc: u32) -> Vec<u8> {
let mut sender = Vec::new();
pb_tag(&mut sender, 3, 0); pb_varint(&mut sender, ssrc as u64);
pb_tag(&mut sender, 5, 0); pb_varint(&mut sender, 0);
pb_tag(&mut sender, 6, 0); pb_varint(&mut sender, 0);
let mut out = Vec::new();
pb_len_delim(&mut out, 1, &sender); out
}
pub fn create_apk_sender_subscriptions(ssrc: u32, pid: Option<u32>) -> Vec<u8> {
let mut ssrc_layers = Vec::new();
pb_tag(&mut ssrc_layers, 1, 0); pb_varint(&mut ssrc_layers, pb_zigzag(ssrc as i64));
if let Some(pid) = pid {
let mut p = Vec::new();
pb_tag(&mut p, 1, 0); pb_varint(&mut p, pb_zigzag(pid as i64));
pb_len_delim(&mut p, 2, b"audio"); pb_len_delim(&mut ssrc_layers, 2, &p); }
let mut ext = Vec::new();
pb_len_delim(&mut ext, 1, &ssrc_layers); let mut out = Vec::new();
pb_len_delim(&mut out, 1, &ext); out
}
pub fn create_apk_stream_descriptors(ssrc: u32) -> Vec<u8> {
let mut sd = Vec::new();
pb_len_delim(&mut sd, 1, b"audio"); pb_len_delim(&mut sd, 2, b"OPUS"); pb_tag(&mut sd, 3, 0); pb_varint(&mut sd, pb_zigzag(ssrc as i64));
pb_tag(&mut sd, 4, 0); pb_varint(&mut sd, 0);
let mut out = Vec::new();
pb_len_delim(&mut out, 1, &sd); out
}
pub fn build_android_stun_allocate_request(
transaction_id: &[u8; 12],
relay_token: &[u8],
ssrc: u32,
pid: Option<u32>,
integrity_key: &[u8],
include_fingerprint: bool,
) -> Vec<u8> {
let mut attrs = stun_attr(ATTR_RELAY_TOKEN, relay_token);
attrs.extend_from_slice(&stun_attr(
ATTR_SENDER_SUBSCRIPTIONS_V2,
&create_apk_sender_subscriptions(ssrc, pid),
));
attrs.extend_from_slice(&stun_attr(
STUN_ATTR_STREAM_DESCRIPTORS,
&create_apk_stream_descriptors(ssrc),
));
encode_stun_request(
MSG_ALLOCATE_REQUEST,
transaction_id,
&attrs,
Some(integrity_key),
include_fingerprint,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::voip::testkat::{hexd, kats};
fn tx12(k: &serde_json::Value) -> [u8; 12] {
let mut tx = [0u8; 12];
tx.copy_from_slice(&hexd(k, &["stun", "tx"]));
tx
}
#[test]
fn crc32_is_ieee() {
let k = kats();
assert_eq!(
crc32(b"abc") as u64,
k["stun"]["crc32_abc"].as_u64().unwrap()
);
assert_eq!(crc32(b"abc"), 0x3524_41c2);
}
#[test]
fn attr_and_endpoint_match_kat() {
let k = kats();
let token = hexd(&k, &["stun", "relayToken"]);
assert_eq!(
hex::encode(stun_attr(ATTR_RELAY_TOKEN, &token)),
k["stun"]["attr_token"].as_str().unwrap()
);
let ep = encode_xor_relay_endpoint("157.240.226.133", 3478).unwrap();
assert_eq!(hex::encode(ep), k["stun"]["xorEndpoint"].as_str().unwrap());
let ssrc = k["inputs"]["ssrc"].as_u64().unwrap() as u32;
assert_eq!(
hex::encode(create_native_sender_subscription(ssrc)),
k["stun"]["nativeSenderSub"].as_str().unwrap()
);
}
#[test]
fn encode_request_mi_and_fingerprint_match_kat() {
let k = kats();
let tx = tx12(&k);
let token = hexd(&k, &["stun", "relayToken"]);
let mi_key = hexd(&k, &["stun", "miKey"]);
let attrs = stun_attr(ATTR_RELAY_TOKEN, &token);
let minimal = encode_stun_request(MSG_ALLOCATE_REQUEST, &tx, &attrs, Some(&mi_key), false);
assert_eq!(
hex::encode(&minimal),
k["stun"]["minimalMi"].as_str().unwrap()
);
let with_fp = encode_stun_request(MSG_ALLOCATE_REQUEST, &tx, &attrs, Some(&mi_key), true);
assert_eq!(hex::encode(&with_fp), k["stun"]["withFp"].as_str().unwrap());
}
#[test]
fn truncated_stun_drives_no_allocate_decision() {
let tx = [0u8; 12];
let ok = encode_stun_request(MSG_ALLOCATE_SUCCESS, &tx, &[], None, false);
assert!(is_allocate_or_binding_success(&ok));
let mut truncated_ok = ok.clone();
truncated_ok[2] = 0x00;
truncated_ok[3] = 0x40;
assert!(!is_allocate_or_binding_success(&truncated_ok));
let mut truncated_err = encode_stun_request(MSG_ALLOCATE_ERROR, &tx, &[], None, false);
truncated_err[2] = 0x00;
truncated_err[3] = 0x40;
assert!(!is_allocate_error(&truncated_err));
}
#[test]
fn error_code_value_beyond_body_is_not_parsed() {
let mut pkt = vec![0x01, 0x13, 0x00, 0x04];
pkt.extend_from_slice(&STUN_MAGIC.to_be_bytes());
pkt.extend_from_slice(&[0u8; 12]);
pkt.extend_from_slice(&[0x00, 0x09, 0x00, 0x04]);
pkt.extend_from_slice(&[0x00, 0x00, 0x04, 0x01]);
assert!(is_allocate_error(&pkt));
assert_eq!(parse_stun_error_code(&pkt), None);
}
#[test]
fn unaligned_stun_body_length_is_rejected() {
let tx = [0u8; 12];
let mut pkt = encode_stun_request(MSG_ALLOCATE_SUCCESS, &tx, &[], None, false);
pkt.push(0xAA);
pkt[2] = 0x00;
pkt[3] = 0x01;
assert!(!is_allocate_or_binding_success(&pkt));
}
fn dv(b: &[u8], i: &mut usize) -> u64 {
let (mut val, mut shift) = (0u64, 0u32);
loop {
let byte = b[*i];
*i += 1;
val |= ((byte & 0x7f) as u64) << shift;
if byte & 0x80 == 0 {
return val;
}
shift += 7;
}
}
fn decode_stream_descriptors(buf: &[u8]) -> std::collections::HashMap<(u32, u32), u32> {
let mut out = std::collections::HashMap::new();
let mut i = 0;
while i < buf.len() {
assert_eq!(dv(buf, &mut i), (1 << 3) | 2, "top-level repeated field 1");
let end = i + dv(buf, &mut i) as usize;
let (mut sidx, mut sub, mut ssrc) = (0u32, 0u32, None);
while i < end {
let field = dv(buf, &mut i) >> 3;
match field {
1 => sidx = dv(buf, &mut i) as u32,
2 => sub = dv(buf, &mut i) as u32,
3 => ssrc = Some(dv(buf, &mut i) as u32),
other => panic!("unexpected descriptor field {other}"),
}
}
out.insert((sidx, sub), ssrc.expect("descriptor carries an ssrc"));
}
out
}
#[test]
fn wasm_allocate_carries_dynamic_stream_descriptors() {
let k = kats();
let tx = tx12(&k);
let token = hexd(&k, &["stun", "relayToken"]);
let mi_key = hexd(&k, &["stun", "miKey"]);
let ep = encode_xor_relay_endpoint("157.240.226.133", 3478).unwrap();
let call_id = "CALL-ID-0001";
let participant = crate::voip::ssrc::format_e2e_srtp_participant_id("12345:0@lid");
let alloc =
build_wasm_stun_allocate_request(&tx, &token, &ep, &mi_key, call_id, &participant);
let attrs = parse_stun_attributes(&alloc);
assert_eq!(attrs[0].attr_type, ATTR_RELAY_TOKEN);
let sd = attrs
.iter()
.find(|a| a.attr_type == STUN_ATTR_STREAM_DESCRIPTORS)
.expect("stream descriptors attr present");
assert_eq!(
sd.value,
create_wasm_stream_descriptors(call_id, &participant)
);
assert!(
attrs
.iter()
.any(|a| a.attr_type == STUN_ATTR_WASM_RELAY_ENDPOINT)
);
let mi = attrs
.iter()
.find(|a| a.attr_type == ATTR_MESSAGE_INTEGRITY)
.expect("message integrity present");
assert_eq!(mi.value.len(), 20);
assert_eq!(
hex::encode(build_whatsapp_ping(&tx)),
k["stun"]["ping"].as_str().unwrap()
);
}
#[test]
fn group_allocate_matches_captured_subscription_and_hbh_fec_shape() {
let stream_ssrcs = [
0x3ea2_6c0c,
0x0bf9_9b28,
0xf42e_4556,
0x14e8_f126,
0xbb16_134f,
0x98b1_4f00,
0xe0e0_4163,
0x74ed_8516,
0xdea8_a613,
];
let tx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
let endpoint = [0x2c, 0x84, 0xbc, 0xe2, 0xb5, 0xc7];
let packet = build_wasm_group_stun_allocate_request(&WasmGroupStunAllocateRequest {
transaction_id: &tx,
relay_token: &[1, 2, 3],
endpoint_xor: &endpoint,
integrity_key: b"0123456789abcdef",
stream_ssrcs: &stream_ssrcs,
app_data_ssrc: 0xb31d_ed3e,
hbh_fec_ssrcs: &[0xc1a1_7938, 0x1bb2_0c84],
participant_pids: &[2, 1, 2],
});
let attrs = parse_stun_attributes(&packet);
assert_eq!(
attrs.iter().map(|attr| attr.attr_type).collect::<Vec<_>>(),
[
ATTR_RELAY_TOKEN,
ATTR_SENDER_SUBSCRIPTIONS_V2,
STUN_ATTR_RECEIVER_SUBSCRIPTIONS,
STUN_ATTR_STREAM_DESCRIPTORS,
STUN_ATTR_PARTICIPANT_COUNT,
STUN_ATTR_WASM_RELAY_ENDPOINT,
ATTR_MESSAGE_INTEGRITY,
]
);
assert_eq!(
hex::encode(attrs[1].value),
"0a1f0a1d0a0fa6e2a3a701cfa6d8d80b809ec5c5091204080110011204080210010a130a110a0fe38281870e968ab6a70793cca2f50d0a1a0a180a0e8cd889f503a8b6e65fd68ab9a10f12020801120208020a110a0f0a05bedaf7980b1202080112020802"
);
assert_eq!(hex::encode(attrs[2].value), "1202080112020802");
assert_eq!(
hex::encode(attrs[3].value),
"0a06188cd889f5030a07100118a8b6e65f0a08100218d68ab9a10f0a08080118a6e2a3a7010a0a0801100118cfa6d8d80b0a0a0801100218809ec5c5090a08080218e38281870e0a0a0802100118968ab6a7070a0a080210021893cca2f50d0a0a0803100318b8f2858d0c0a0a08041003188499c8dd01"
);
assert_eq!(attrs[4].value, [2]);
let one_pid = build_wasm_group_stun_allocate_request(&WasmGroupStunAllocateRequest {
transaction_id: &tx,
relay_token: &[1, 2, 3],
endpoint_xor: &endpoint,
integrity_key: b"0123456789abcdef",
stream_ssrcs: &stream_ssrcs,
app_data_ssrc: 0xb31d_ed3e,
hbh_fec_ssrcs: &[0xc1a1_7938, 0x1bb2_0c84],
participant_pids: &[1],
});
let one_attrs = parse_stun_attributes(&one_pid);
assert_eq!(
one_attrs[3].value,
create_wasm_stream_descriptors_from_ssrcs(&stream_ssrcs, &[0, 0])
);
let no_pids = build_wasm_group_stun_allocate_request(&WasmGroupStunAllocateRequest {
transaction_id: &tx,
relay_token: &[1, 2, 3],
endpoint_xor: &endpoint,
integrity_key: b"0123456789abcdef",
stream_ssrcs: &stream_ssrcs,
app_data_ssrc: 0xb31d_ed3e,
hbh_fec_ssrcs: &[0xc1a1_7938, 0x1bb2_0c84],
participant_pids: &[],
});
assert_eq!(
parse_stun_attributes(&no_pids)
.iter()
.map(|attr| attr.attr_type)
.collect::<Vec<_>>(),
[
ATTR_RELAY_TOKEN,
STUN_ATTR_STREAM_DESCRIPTORS,
STUN_ATTR_WASM_RELAY_ENDPOINT,
ATTR_MESSAGE_INTEGRITY,
]
);
}
#[test]
fn stream_descriptors_announce_the_live_media_ssrcs() {
let call_id = "CALL-ID-0001";
let participant = crate::voip::ssrc::format_e2e_srtp_participant_id("12345:0@lid");
let entries =
decode_stream_descriptors(&create_wasm_stream_descriptors(call_id, &participant));
assert_eq!(
entries.get(&(1, 0)).copied(),
Some(crate::voip::ssrc::derive_video_participant_ssrc(
call_id,
&participant
))
);
assert_eq!(
entries.get(&(0, 0)).copied(),
Some(crate::voip::ssrc::derive_wasm_participant_ssrc(
call_id,
&participant,
0
))
);
assert_eq!(entries.len(), 9);
}
#[test]
fn parse_round_trips_attributes() {
let k = kats();
let minimal = hexd(&k, &["stun", "minimalMi"]);
assert!(is_stun_packet(&minimal));
assert_eq!(stun_message_type(&minimal), Some(MSG_ALLOCATE_REQUEST));
let attrs = parse_stun_attributes(&minimal);
assert_eq!(attrs.len(), 2);
assert_eq!(attrs[0].attr_type, ATTR_RELAY_TOKEN);
assert_eq!(attrs[0].value, hexd(&k, &["stun", "relayToken"]));
assert_eq!(attrs[1].attr_type, ATTR_MESSAGE_INTEGRITY);
assert_eq!(attrs[1].value.len(), 20);
}
#[test]
fn parse_attributes_truncated_padding_does_not_panic() {
let mut p = vec![0u8; 20];
p[0] = 0x00; p.extend_from_slice(&[0x40, 0x00, 0x00, 0x01, 0xAB]);
assert_eq!(p.len(), 25);
let attrs = parse_stun_attributes(&p);
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].value, [0xAB]);
}
#[test]
fn protobuf_payloads_match_kat() {
let k = kats();
let ssrc = k["inputs"]["ssrc"].as_u64().unwrap() as u32;
assert_eq!(
hex::encode(create_voip_sender_subscriptions(ssrc)),
k["stun_proto"]["voip_sender_subscriptions"]
.as_str()
.unwrap()
);
assert_eq!(
hex::encode(create_apk_sender_subscriptions(ssrc, None)),
k["stun_proto"]["apk_sender_subscriptions_nopid"]
.as_str()
.unwrap()
);
assert_eq!(
hex::encode(create_apk_sender_subscriptions(ssrc, Some(7))),
k["stun_proto"]["apk_sender_subscriptions_pid"]
.as_str()
.unwrap()
);
assert_eq!(
hex::encode(create_apk_stream_descriptors(ssrc)),
k["stun_proto"]["apk_stream_descriptors"].as_str().unwrap()
);
}
#[test]
fn android_allocate_carries_three_attrs() {
let k = kats();
let tx = tx12(&k);
let token = hexd(&k, &["stun", "relayToken"]);
let mi_key = hexd(&k, &["stun", "miKey"]);
let ssrc = k["inputs"]["ssrc"].as_u64().unwrap() as u32;
let pkt = build_android_stun_allocate_request(&tx, &token, ssrc, None, &mi_key, false);
let attrs = parse_stun_attributes(&pkt);
assert_eq!(attrs[0].attr_type, ATTR_RELAY_TOKEN);
assert_eq!(attrs[1].attr_type, ATTR_SENDER_SUBSCRIPTIONS_V2);
assert_eq!(attrs[2].attr_type, STUN_ATTR_STREAM_DESCRIPTORS);
assert_eq!(attrs[3].attr_type, ATTR_MESSAGE_INTEGRITY);
assert_eq!(attrs[2].value, create_apk_stream_descriptors(ssrc));
}
#[test]
fn pong_matching() {
let k = kats();
let tx = tx12(&k);
let mut pong = build_whatsapp_ping(&tx).to_vec();
pong[0..2].copy_from_slice(&MSG_WHATSAPP_PONG.to_be_bytes());
assert!(is_whatsapp_pong(&pong, Some(&tx)));
assert!(is_whatsapp_pong(&pong, None));
let wrong_tx = [0u8; 12];
assert!(!is_whatsapp_pong(&pong, Some(&wrong_tx)));
}
}