use tokio::net::UdpSocket;
use tokio::time::Duration;
use crate::error::Error;
pub const IP_DISCOVERY_PACKET_SIZE: usize = 74;
pub fn build_ip_discovery_packet(ssrc: u32) -> [u8; IP_DISCOVERY_PACKET_SIZE] {
let mut packet = [0u8; IP_DISCOVERY_PACKET_SIZE];
packet[0..2].copy_from_slice(&1u16.to_be_bytes());
packet[2..4].copy_from_slice(&70u16.to_be_bytes());
packet[4..8].copy_from_slice(&ssrc.to_be_bytes());
packet
}
pub fn parse_ip_discovery_response(packet: &[u8]) -> Result<(String, u16), Error> {
if packet.len() < IP_DISCOVERY_PACKET_SIZE {
return Err(Error::Voice(
"voice IP discovery response must be at least 74 bytes".to_string(),
));
}
let response_type = u16::from_be_bytes([packet[0], packet[1]]);
let length = u16::from_be_bytes([packet[2], packet[3]]);
if response_type != 2 || length != 70 {
return Err(Error::Voice(
"invalid voice IP discovery response header".to_string(),
));
}
let raw_address = &packet[8..72];
let end = raw_address
.iter()
.position(|&byte| byte == 0)
.unwrap_or(raw_address.len());
let address = std::str::from_utf8(&raw_address[..end])
.map_err(|_| Error::Voice("non-ASCII address in IP discovery response".to_string()))?
.to_string();
let port = u16::from_be_bytes([packet[72], packet[73]]);
Ok((address, port))
}
#[derive(Debug)]
pub struct VoiceUdpSocket {
socket: UdpSocket,
}
impl VoiceUdpSocket {
pub async fn connect(ip: &str, port: u16) -> Result<Self, Error> {
let socket = UdpSocket::bind("0.0.0.0:0")
.await
.map_err(|error| Error::Voice(format!("failed to bind voice UDP socket: {error}")))?;
socket
.connect((ip, port))
.await
.map_err(|error| Error::Voice(format!("failed to connect voice UDP socket: {error}")))?;
Ok(Self { socket })
}
pub async fn send(&self, data: &[u8]) -> Result<(), Error> {
self.socket
.send(data)
.await
.map_err(|error| Error::Voice(format!("voice UDP send failed: {error}")))?;
Ok(())
}
pub async fn recv(&self) -> Result<Vec<u8>, Error> {
let mut buffer = vec![0u8; 4096];
let received = self
.socket
.recv(&mut buffer)
.await
.map_err(|error| Error::Voice(format!("voice UDP recv failed: {error}")))?;
buffer.truncate(received);
Ok(buffer)
}
pub async fn discover_ip(&self, ssrc: u32, timeout: Duration) -> Result<(String, u16), Error> {
self.send(&build_ip_discovery_packet(ssrc)).await?;
let response = tokio::time::timeout(timeout, self.recv())
.await
.map_err(|_| Error::Voice("timed out waiting for IP discovery response".to_string()))??;
parse_ip_discovery_response(&response)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discovery_packet_format_is_74_bytes_type1_length70() {
let packet = build_ip_discovery_packet(0xDEADBEEF);
assert_eq!(packet.len(), 74);
assert_eq!(&packet[0..2], &[0, 1]); assert_eq!(&packet[2..4], &[0, 70]); assert_eq!(&packet[4..8], &[0xDE, 0xAD, 0xBE, 0xEF]); assert!(packet[8..].iter().all(|&byte| byte == 0));
}
#[test]
fn discovery_response_roundtrip() {
let mut response = [0u8; 74];
response[0..2].copy_from_slice(&2u16.to_be_bytes());
response[2..4].copy_from_slice(&70u16.to_be_bytes());
response[4..8].copy_from_slice(&7u32.to_be_bytes());
response[8..8 + 9].copy_from_slice(b"203.0.113");
response[72..74].copy_from_slice(&50004u16.to_be_bytes());
let (address, port) = parse_ip_discovery_response(&response).unwrap();
assert_eq!(address, "203.0.113");
assert_eq!(port, 50004);
}
#[test]
fn discovery_response_rejects_bad_header_or_size() {
assert!(parse_ip_discovery_response(&[0u8; 10]).is_err());
let mut response = [0u8; 74];
response[0..2].copy_from_slice(&1u16.to_be_bytes()); response[2..4].copy_from_slice(&70u16.to_be_bytes());
assert!(parse_ip_discovery_response(&response).is_err());
}
#[tokio::test]
async fn discover_ip_over_loopback_socket() {
let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let server_addr = server.local_addr().unwrap();
tokio::spawn(async move {
let mut buffer = [0u8; 128];
let (received, peer) = server.recv_from(&mut buffer).await.unwrap();
let request = &buffer[..received];
assert_eq!(request.len(), 74);
let mut response = [0u8; 74];
response[0..2].copy_from_slice(&2u16.to_be_bytes());
response[2..4].copy_from_slice(&70u16.to_be_bytes());
response[4..8].copy_from_slice(&request[4..8]);
response[8..8 + 9].copy_from_slice(b"127.0.0.1");
response[72..74].copy_from_slice(&peer.port().to_be_bytes());
server.send_to(&response, peer).await.unwrap();
});
let socket = VoiceUdpSocket::connect("127.0.0.1", server_addr.port())
.await
.unwrap();
let (address, port) = socket
.discover_ip(321, Duration::from_secs(5))
.await
.unwrap();
assert_eq!(address, "127.0.0.1");
assert!(port > 0);
}
}