use std::io::{self, Result};
#[cfg(feature = "stun")]
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "stun")]
use async_trait::async_trait;
#[cfg(feature = "stun")]
use tokio::net::UdpSocket;
#[cfg(feature = "stun")]
use crate::pinger::Pinger;
use crate::uri::get_uri;
#[cfg(feature = "stun")]
use crate::util::with_timeout;
#[cfg(feature = "stun")]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(feature = "stun")]
const DEFAULT_PORT: u16 = 3478;
#[cfg(feature = "stun")]
const BUF_SIZE: usize = 0xFF;
pub(crate) const STUN_HEADER_LEN: usize = 20;
pub(crate) const STUN_TXID_LEN: usize = 12;
pub(crate) const MAGIC_COOKIE: u32 = 0x2112_A442;
#[cfg(feature = "stun")]
const MSG_BINDING_REQUEST: u16 = 0x0001;
#[cfg(feature = "stun")]
const MSG_BINDING_SUCCESS: u16 = 0x0101;
#[cfg(feature = "stun")]
pub struct StunPinger {
pub server: String,
pub timeout: Duration,
}
#[cfg(feature = "stun")]
impl StunPinger {
pub fn new(server: impl Into<String>) -> Self {
Self {
server: server.into(),
timeout: DEFAULT_TIMEOUT,
}
}
pub fn with_timeout(mut self, t: Duration) -> Self {
self.timeout = t;
self
}
}
#[cfg(feature = "stun")]
#[async_trait]
impl Pinger for StunPinger {
async fn ping(&self) -> Result<()> {
let endpoint = server_endpoint(&self.server, DEFAULT_PORT)?;
let txid = random_transaction_id();
let request = build_header(MSG_BINDING_REQUEST, 0, &txid);
with_timeout(self.timeout, async move {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(&endpoint).await?;
socket.send(&request).await?;
let mut buf = [0u8; BUF_SIZE];
let n = socket.recv(&mut buf).await?;
validate_binding_response(&buf[..n], &txid)
})
.await
}
}
pub(crate) fn server_endpoint(server: &str, default_port: u16) -> Result<String> {
let uri = get_uri(server);
if uri.domain.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"STUN/TURN server target is missing a host",
));
}
let port = if uri.port > 0 {
uri.port as u16
} else {
default_port
};
Ok(format!("{}:{}", uri.domain, port))
}
#[cfg(feature = "turn")]
pub(crate) fn build_message(
message_type: u16,
attributes: &[u8],
transaction_id: &[u8; STUN_TXID_LEN],
) -> Vec<u8> {
let mut packet = Vec::with_capacity(STUN_HEADER_LEN + attributes.len());
let length = u16::try_from(attributes.len()).expect("STUN attributes fit in u16");
packet.extend_from_slice(&build_header(message_type, length, transaction_id));
packet.extend_from_slice(attributes);
packet
}
#[cfg(any(feature = "stun", feature = "turn"))]
fn build_header(
message_type: u16,
message_length: u16,
transaction_id: &[u8; STUN_TXID_LEN],
) -> [u8; STUN_HEADER_LEN] {
let mut header = [0u8; STUN_HEADER_LEN];
header[0..2].copy_from_slice(&message_type.to_be_bytes());
header[2..4].copy_from_slice(&message_length.to_be_bytes());
header[4..8].copy_from_slice(&MAGIC_COOKIE.to_be_bytes());
header[8..20].copy_from_slice(transaction_id);
header
}
pub(crate) fn random_transaction_id() -> [u8; STUN_TXID_LEN] {
let mut id = [0u8; STUN_TXID_LEN];
if let Ok(d) = SystemTime::now().duration_since(UNIX_EPOCH) {
let secs = d.as_secs();
let nanos = u64::from(d.subsec_nanos());
id[0..8].copy_from_slice(&secs.to_be_bytes());
id[8..12].copy_from_slice(&(nanos as u32).to_be_bytes());
}
id
}
#[cfg(feature = "stun")]
fn validate_binding_response(buf: &[u8], expected_txid: &[u8; STUN_TXID_LEN]) -> Result<()> {
validate_response_header(buf, MSG_BINDING_SUCCESS, expected_txid)
}
pub(crate) fn validate_response_header(
buf: &[u8],
expected_type: u16,
expected_txid: &[u8; STUN_TXID_LEN],
) -> Result<()> {
if buf.len() < STUN_HEADER_LEN {
return Err(io::Error::other(format!(
"STUN response shorter than 20-byte header (got {} bytes)",
buf.len()
)));
}
let message_type = u16::from_be_bytes([buf[0], buf[1]]);
if message_type != expected_type {
return Err(io::Error::other(format!(
"STUN response message type {message_type:#06x} (expected {expected_type:#06x})"
)));
}
let cookie = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
if cookie != MAGIC_COOKIE {
return Err(io::Error::other(format!(
"STUN response magic cookie {cookie:#010x} does not match {MAGIC_COOKIE:#010x}"
)));
}
if &buf[8..20] != expected_txid.as_slice() {
return Err(io::Error::other(
"STUN response transaction ID does not match request",
));
}
Ok(())
}
#[cfg(all(test, feature = "stun", feature = "turn"))]
mod tests {
use super::*;
#[test]
fn build_message_no_attributes() {
let txid = [0xAB; STUN_TXID_LEN];
let pkt = build_message(MSG_BINDING_REQUEST, &[], &txid);
assert_eq!(pkt.len(), STUN_HEADER_LEN);
assert_eq!(&pkt[0..2], &MSG_BINDING_REQUEST.to_be_bytes());
assert_eq!(&pkt[2..4], &0u16.to_be_bytes());
assert_eq!(&pkt[4..8], &MAGIC_COOKIE.to_be_bytes());
assert_eq!(&pkt[8..20], &txid);
}
#[test]
fn build_message_with_attributes() {
let txid = [0; STUN_TXID_LEN];
let attrs = [1, 2, 3, 4];
let pkt = build_message(0x0003, &attrs, &txid);
assert_eq!(pkt.len(), STUN_HEADER_LEN + 4);
assert_eq!(u16::from_be_bytes([pkt[2], pkt[3]]), 4);
assert_eq!(&pkt[STUN_HEADER_LEN..], &attrs);
}
#[test]
fn validate_binding_response_accepts_well_formed() {
let txid = [0x55; STUN_TXID_LEN];
let mut buf = [0u8; STUN_HEADER_LEN];
buf[0..2].copy_from_slice(&MSG_BINDING_SUCCESS.to_be_bytes());
buf[4..8].copy_from_slice(&MAGIC_COOKIE.to_be_bytes());
buf[8..20].copy_from_slice(&txid);
validate_binding_response(&buf, &txid).unwrap();
}
#[test]
fn validate_binding_response_rejects_short_buffer() {
let txid = [0u8; STUN_TXID_LEN];
let buf = [0u8; STUN_HEADER_LEN - 1];
assert!(validate_binding_response(&buf, &txid).is_err());
}
#[test]
fn validate_binding_response_rejects_wrong_type() {
let txid = [0x55; STUN_TXID_LEN];
let mut buf = [0u8; STUN_HEADER_LEN];
buf[0..2].copy_from_slice(&MSG_BINDING_REQUEST.to_be_bytes());
buf[4..8].copy_from_slice(&MAGIC_COOKIE.to_be_bytes());
buf[8..20].copy_from_slice(&txid);
assert!(validate_binding_response(&buf, &txid).is_err());
}
#[test]
fn validate_binding_response_rejects_bad_cookie() {
let txid = [0x55; STUN_TXID_LEN];
let mut buf = [0u8; STUN_HEADER_LEN];
buf[0..2].copy_from_slice(&MSG_BINDING_SUCCESS.to_be_bytes());
buf[4..8].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
buf[8..20].copy_from_slice(&txid);
assert!(validate_binding_response(&buf, &txid).is_err());
}
#[test]
fn validate_binding_response_rejects_txid_mismatch() {
let sent = [0x55; STUN_TXID_LEN];
let other = [0x66; STUN_TXID_LEN];
let mut buf = [0u8; STUN_HEADER_LEN];
buf[0..2].copy_from_slice(&MSG_BINDING_SUCCESS.to_be_bytes());
buf[4..8].copy_from_slice(&MAGIC_COOKIE.to_be_bytes());
buf[8..20].copy_from_slice(&other);
assert!(validate_binding_response(&buf, &sent).is_err());
}
#[test]
fn server_endpoint_applies_default_port() {
assert_eq!(
server_endpoint("stun.example.com", DEFAULT_PORT).unwrap(),
"stun.example.com:3478"
);
}
#[test]
fn server_endpoint_keeps_explicit_port() {
assert_eq!(
server_endpoint("stun.example.com:19302", DEFAULT_PORT).unwrap(),
"stun.example.com:19302"
);
}
}