use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use dig_nat::stun::{
encode_binding_request, parse_binding_response, StunError, ATTR_MAPPED_ADDRESS,
ATTR_XOR_MAPPED_ADDRESS, BINDING_REQUEST, BINDING_SUCCESS, MAGIC_COOKIE,
};
const TXID: [u8; 12] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
#[test]
fn binding_request_has_rfc_header() {
let req = encode_binding_request(&TXID);
assert_eq!(req.len(), 20);
assert_eq!(u16::from_be_bytes([req[0], req[1]]), BINDING_REQUEST);
assert_eq!(u16::from_be_bytes([req[2], req[3]]), 0, "no attributes");
assert_eq!(
u32::from_be_bytes([req[4], req[5], req[6], req[7]]),
MAGIC_COOKIE
);
assert_eq!(&req[8..20], &TXID);
}
#[test]
fn parses_xor_mapped_ipv4() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
let msg = build_response(ATTR_XOR_MAPPED_ADDRESS, addr, &TXID);
let got = parse_binding_response(&msg, Some(&TXID)).unwrap();
assert_eq!(got, addr);
}
#[test]
fn parses_xor_mapped_ipv6() {
let addr = SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1234)),
9450,
);
let msg = build_response(ATTR_XOR_MAPPED_ADDRESS, addr, &TXID);
let got = parse_binding_response(&msg, Some(&TXID)).unwrap();
assert_eq!(got, addr);
}
#[test]
fn parses_legacy_mapped_address_fallback() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 42)), 1234);
let msg = build_response(ATTR_MAPPED_ADDRESS, addr, &TXID);
let got = parse_binding_response(&msg, Some(&TXID)).unwrap();
assert_eq!(got, addr);
}
#[test]
fn rejects_bad_magic_cookie() {
let mut msg = build_response(
ATTR_XOR_MAPPED_ADDRESS,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1),
&TXID,
);
msg[4] ^= 0xff; assert_eq!(
parse_binding_response(&msg, Some(&TXID)),
Err(StunError::BadMagicCookie)
);
}
#[test]
fn rejects_transaction_id_mismatch() {
let msg = build_response(
ATTR_XOR_MAPPED_ADDRESS,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1),
&TXID,
);
let other = [9u8; 12];
assert_eq!(
parse_binding_response(&msg, Some(&other)),
Err(StunError::TransactionIdMismatch)
);
}
#[test]
fn rejects_truncated() {
assert_eq!(
parse_binding_response(&[0u8; 4], None),
Err(StunError::Truncated)
);
}
#[test]
fn rejects_no_mapped_address() {
let mut msg = Vec::new();
msg.extend_from_slice(&BINDING_SUCCESS.to_be_bytes());
msg.extend_from_slice(&0u16.to_be_bytes());
msg.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
msg.extend_from_slice(&TXID);
assert_eq!(
parse_binding_response(&msg, Some(&TXID)),
Err(StunError::NoMappedAddress)
);
}
#[test]
fn rejects_non_success_type() {
let req = encode_binding_request(&TXID);
assert!(matches!(
parse_binding_response(&req, Some(&TXID)),
Err(StunError::UnexpectedType(_))
));
}
fn build_response(attr_type: u16, addr: SocketAddr, txid: &[u8; 12]) -> Vec<u8> {
let xor = attr_type == ATTR_XOR_MAPPED_ADDRESS;
let cookie_be = MAGIC_COOKIE.to_be_bytes();
let mut value = Vec::new();
value.push(0); match addr.ip() {
IpAddr::V4(v4) => {
value.push(0x01); let port = if xor {
addr.port() ^ ((MAGIC_COOKIE >> 16) as u16)
} else {
addr.port()
};
value.extend_from_slice(&port.to_be_bytes());
let mut octets = v4.octets();
if xor {
for (i, o) in octets.iter_mut().enumerate() {
*o ^= cookie_be[i];
}
}
value.extend_from_slice(&octets);
}
IpAddr::V6(v6) => {
value.push(0x02); let port = if xor {
addr.port() ^ ((MAGIC_COOKIE >> 16) as u16)
} else {
addr.port()
};
value.extend_from_slice(&port.to_be_bytes());
let mut octets = v6.octets();
if xor {
let mut key = [0u8; 16];
key[..4].copy_from_slice(&cookie_be);
key[4..].copy_from_slice(txid);
for (o, k) in octets.iter_mut().zip(key.iter()) {
*o ^= *k;
}
}
value.extend_from_slice(&octets);
}
}
let mut attr = Vec::new();
attr.extend_from_slice(&attr_type.to_be_bytes());
attr.extend_from_slice(&(value.len() as u16).to_be_bytes());
attr.extend_from_slice(&value);
while attr.len() % 4 != 0 {
attr.push(0);
}
let mut msg = Vec::new();
msg.extend_from_slice(&BINDING_SUCCESS.to_be_bytes());
msg.extend_from_slice(&(attr.len() as u16).to_be_bytes());
msg.extend_from_slice(&cookie_be);
msg.extend_from_slice(txid);
msg.extend_from_slice(&attr);
msg
}
use dig_nat::stun::new_transaction_id;
#[test]
fn transaction_ids_are_not_sequential_wall_clock_samples() {
let ids: Vec<[u8; 12]> = (0..64).map(|_| new_transaction_id()).collect();
for i in 0..ids.len() {
for j in (i + 1)..ids.len() {
assert_ne!(ids[i], ids[j], "txids must not collide across samples");
}
}
assert!(
ids.iter().any(|id| id[8..12] != [0u8, 0, 0, 0]),
"high-order bytes must vary — a wall-clock-nanosecond source leaves them zero, which is the \
forgeable pattern finding 1 flags"
);
let differing_high_bytes = ids
.windows(2)
.filter(|w| w[0][6..12] != w[1][6..12])
.count();
assert!(
differing_high_bytes > ids.len() / 2,
"most consecutive samples should differ in their high-order bytes too (CSPRNG), not just \
the low bytes (wall-clock nanosecond counter)"
);
}
#[test]
fn transaction_id_is_not_derived_from_current_time() {
let now_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let id = new_transaction_id();
let low8 = u64::from_le_bytes(id[0..8].try_into().unwrap());
assert_ne!(
low8, now_nanos as u64,
"transaction id must not equal the wall-clock nanosecond timestamp"
);
}