#![cfg(feature = "integration")]
use std::net::{SocketAddr, UdpSocket};
use std::sync::Arc;
use std::time::Duration;
use xphone::ice::{self, IceAgent, IceSdpParams};
use xphone::stun;
use xphone::turn::{self, TurnClient};
fn turn_addr() -> SocketAddr {
let host = std::env::var("TURN_HOST").unwrap_or_else(|_| "127.0.0.1".into());
let port: u16 = std::env::var("TURN_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3478);
format!("{}:{}", host, port).parse().unwrap()
}
fn turn_user() -> String {
std::env::var("TURN_USER").unwrap_or_else(|_| "testuser".into())
}
fn turn_pass() -> String {
std::env::var("TURN_PASS").unwrap_or_else(|_| "testpass".into())
}
#[test]
fn stun_binding_via_coturn() {
let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let server = turn_addr();
let addr = stun::stun_mapped_address(&socket, server, Duration::from_secs(5)).unwrap();
assert!(
!addr.ip().is_unspecified(),
"mapped IP should not be 0.0.0.0"
);
assert_ne!(addr.port(), 0, "mapped port should not be 0");
println!("STUN mapped address: {}", addr);
}
#[test]
fn turn_allocate() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
let client = TurnClient::new(socket, turn_addr(), turn_user(), turn_pass());
let relay = client.allocate().unwrap();
println!("TURN relay address: {}", relay);
assert!(!relay.ip().is_unspecified());
assert_ne!(relay.port(), 0);
assert_eq!(client.relay_addr(), Some(relay));
client.stop();
}
#[test]
fn turn_allocate_wrong_credentials() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
let client = TurnClient::new(socket, turn_addr(), "wrong".into(), "wrong".into());
let result = client.allocate();
assert!(result.is_err(), "should fail with wrong credentials");
println!("Expected error: {}", result.unwrap_err());
}
#[test]
fn turn_full_lifecycle() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
let client = TurnClient::new(socket.clone(), turn_addr(), turn_user(), turn_pass());
let relay = client.allocate().unwrap();
println!("Relay: {}", relay);
let peer_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
peer_socket
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
let peer_addr: SocketAddr = format!("127.0.0.1:{}", peer_socket.local_addr().unwrap().port())
.parse()
.unwrap();
println!("Peer: {}", peer_addr);
client.create_permission(peer_addr).unwrap();
let channel = client.channel_bind(peer_addr).unwrap();
assert!(channel >= 0x4000 && channel <= 0x7FFE);
assert_eq!(client.channel_for_peer(&peer_addr), Some(channel));
println!("Channel: 0x{:04X}", channel);
let test_payload = b"hello from TURN relay";
let frame = turn::wrap_channel_data(channel, test_payload);
socket.send_to(&frame, turn_addr()).unwrap();
let mut buf = [0u8; 256];
match peer_socket.recv_from(&mut buf) {
Ok((n, from)) => {
println!("Peer received {} bytes from {}", n, from);
assert_eq!(from.ip(), relay.ip());
assert_eq!(&buf[..n], test_payload);
}
Err(e) => {
println!(
"Peer did not receive data (expected on macOS Docker): {}",
e
);
}
}
client.stop();
assert!(client.relay_addr().is_none());
}
#[test]
fn turn_receive_via_relay() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
socket
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
let client = TurnClient::new(socket.clone(), turn_addr(), turn_user(), turn_pass());
let relay = client.allocate().unwrap();
println!("Relay: {}", relay);
let peer_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let peer_addr: SocketAddr = format!("127.0.0.1:{}", peer_socket.local_addr().unwrap().port())
.parse()
.unwrap();
client.create_permission(peer_addr).unwrap();
let channel = client.channel_bind(peer_addr).unwrap();
let test_data = b"response from peer";
peer_socket.send_to(test_data, relay).unwrap();
std::thread::sleep(Duration::from_millis(100));
let mut buf = [0u8; 256];
match socket.recv_from(&mut buf) {
Ok((n, from)) => {
assert_eq!(from, turn_addr(), "data should come from TURN server");
if turn::is_channel_data(&buf[..n]) {
let (ch, payload) = turn::parse_channel_data(&buf[..n]).unwrap();
assert_eq!(ch, channel);
assert_eq!(payload, test_data);
println!(
"Received ChannelData: channel=0x{:04X}, {} bytes",
ch,
payload.len()
);
} else {
println!(
"Received non-ChannelData ({} bytes) — may be STUN indication",
n
);
}
}
Err(e) => {
println!(
"Client did not receive relay data (expected on macOS Docker): {}",
e
);
}
}
client.stop();
}
#[test]
fn turn_refresh() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
let client = TurnClient::new(socket, turn_addr(), turn_user(), turn_pass());
client.allocate().unwrap();
client.refresh().unwrap();
client.stop();
}
#[test]
fn ice_gather_with_stun() {
let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let local_port = socket.local_addr().unwrap().port();
let srflx = stun::stun_mapped_address(&socket, turn_addr(), Duration::from_secs(5)).unwrap();
println!("Server-reflexive: {}", srflx);
let local_addr: SocketAddr = format!("127.0.0.1:{}", local_port).parse().unwrap();
let candidates = ice::gather_candidates(local_addr, Some(srflx), None, 1);
assert!(candidates.len() >= 2, "should have host + srflx candidates");
println!("Candidates:");
for c in &candidates {
println!(" {}", c.to_sdp_value());
}
for c in &candidates {
let sdp = c.to_sdp_value();
assert!(sdp.contains("UDP"), "candidate should use UDP");
}
}
#[test]
fn ice_gather_with_turn_relay() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
let client = TurnClient::new(socket.clone(), turn_addr(), turn_user(), turn_pass());
let relay = client.allocate().unwrap();
let srflx = stun::stun_mapped_address(&socket, turn_addr(), Duration::from_secs(5)).unwrap();
let local_addr = socket.local_addr().unwrap();
let candidates = ice::gather_candidates(local_addr, Some(srflx), Some(relay), 1);
assert!(candidates.len() >= 3, "should have host + srflx + relay");
println!("Candidates:");
for c in &candidates {
println!(" {}", c.to_sdp_value());
}
let has_relay = candidates
.iter()
.any(|c| c.to_sdp_value().contains("typ relay"));
assert!(has_relay, "should have a relay candidate");
client.stop();
}
#[test]
fn ice_agent_binding_request() {
let creds = ice::generate_credentials();
println!("Local ICE creds: ufrag={}, pwd={}", creds.ufrag, creds.pwd);
let local_addr: SocketAddr = "127.0.0.1:5004".parse().unwrap();
let candidates = ice::gather_candidates(local_addr, None, None, 1);
let agent = IceAgent::new(creds.clone(), candidates);
let remote_ufrag = "remoteufrag";
let username = format!("{}:{}", creds.ufrag, remote_ufrag);
let txn_id = stun::generate_txn_id();
let mut request = stun::build_stun_message(
stun::BINDING_REQUEST,
&txn_id,
&[stun::StunAttr {
attr_type: stun::ATTR_USERNAME,
value: username.as_bytes().to_vec(),
}],
);
stun::append_message_integrity(&mut request, creds.pwd.as_bytes());
let from: SocketAddr = "10.0.0.1:5000".parse().unwrap();
let response = agent.handle_binding_request(&request, from);
assert!(response.is_some(), "should produce a Binding Response");
let resp = response.unwrap();
assert!(stun::is_stun_message(&resp));
let msg_type = stun::extract_msg_type(&resp).unwrap();
assert_eq!(msg_type, stun::BINDING_RESPONSE);
println!("ICE Binding Response: {} bytes", resp.len());
}
#[test]
fn ice_agent_rejects_wrong_ufrag() {
let creds = ice::generate_credentials();
let candidates = ice::gather_candidates("127.0.0.1:5004".parse().unwrap(), None, None, 1);
let agent = IceAgent::new(creds.clone(), candidates);
let txn_id = stun::generate_txn_id();
let mut request = stun::build_stun_message(
stun::BINDING_REQUEST,
&txn_id,
&[stun::StunAttr {
attr_type: stun::ATTR_USERNAME,
value: b"wrongufrag:remote".to_vec(),
}],
);
stun::append_message_integrity(&mut request, creds.pwd.as_bytes());
let from: SocketAddr = "10.0.0.1:5000".parse().unwrap();
assert!(agent.handle_binding_request(&request, from).is_none());
}
#[test]
fn sdp_offer_with_real_candidates() {
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
let local_port = socket.local_addr().unwrap().port();
let client = TurnClient::new(socket.clone(), turn_addr(), turn_user(), turn_pass());
let relay = client.allocate().unwrap();
let srflx = stun::stun_mapped_address(&socket, turn_addr(), Duration::from_secs(5)).unwrap();
let local_addr: SocketAddr = format!("127.0.0.1:{}", local_port).parse().unwrap();
let candidates = ice::gather_candidates(local_addr, Some(srflx), Some(relay), 1);
let creds = ice::generate_credentials();
let ice_params = IceSdpParams {
ufrag: creds.ufrag.clone(),
pwd: creds.pwd.clone(),
candidates,
ice_lite: true,
};
let sdp = xphone::sdp::build_offer_ice(
"127.0.0.1",
local_port as i32,
&[0, 8],
"sendrecv",
&ice_params,
);
println!("SDP offer:\n{}", sdp);
assert!(sdp.contains("a=ice-lite"), "should have ice-lite");
assert!(sdp.contains(&format!("a=ice-ufrag:{}", creds.ufrag)));
assert!(sdp.contains(&format!("a=ice-pwd:{}", creds.pwd)));
assert!(sdp.contains("typ host"), "should have host candidate");
assert!(sdp.contains("typ srflx"), "should have srflx candidate");
assert!(sdp.contains("typ relay"), "should have relay candidate");
client.stop();
}
#[test]
fn packet_demux() {
let stun_msg = stun::build_stun_message(stun::BINDING_REQUEST, &[0xAA; 12], &[]);
assert!(stun::is_stun_message(&stun_msg));
assert!(!turn::is_channel_data(&stun_msg));
let cd = turn::wrap_channel_data(0x4000, b"rtp payload");
assert!(turn::is_channel_data(&cd));
assert!(!stun::is_stun_message(&cd));
let mut rtp = vec![0x80, 0x00, 0x00, 0x01];
rtp.extend_from_slice(&[0u8; 8]); rtp.extend_from_slice(&[0xFFu8; 160]); assert!(!stun::is_stun_message(&rtp));
assert!(!turn::is_channel_data(&rtp));
let (ch, payload) = turn::parse_channel_data(&cd).unwrap();
assert_eq!(ch, 0x4000);
assert_eq!(payload, b"rtp payload");
}