//! Tests for ICE candidate handling and configuration. use std::net::Ipv4Addr; use std::time::{Duration, Instant}; use str0m::RtcConfig; use str0m::RtcError; use str0m::ice::IceCreds; use tracing::info_span; mod common; use common::{Peer, TestRtc, init_crypto_default, init_log, progress}; /// Test connection with only host candidates. #[test] fn ice_candidate_types_host_only() -> Result<(), RtcError> { init_log(); init_crypto_default(); let mut l = TestRtc::new(Peer::Left); let mut r = TestRtc::new(Peer::Right); // Add only host candidates (no srflx or relay) l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; // Should connect using host candidates loop { if l.is_connected() && r.is_connected() { break; } if l.duration() > Duration::from_secs(5) { panic!("Failed to connect with host-only candidates"); } progress(&mut l, &mut r)?; } Ok(()) } /// Test trickle ICE - connect with relay candidates, then trickle in host candidates. /// Verifies that ICE switches to the better (host) candidates after they're added. #[test] fn ice_trickle_incremental_candidates() -> Result<(), RtcError> { use std::net::SocketAddr; use str0m::{Candidate, Output}; init_log(); init_crypto_default(); let mut l = TestRtc::new(Peer::Left); let mut r = TestRtc::new(Peer::Right); // Define addresses for relay and host candidates // Relay addresses (TURN allocated addresses, lower priority) let l_relay_addr: SocketAddr = (Ipv4Addr::new(10, 0, 0, 1), 10000).into(); let r_relay_addr: SocketAddr = (Ipv4Addr::new(10, 0, 0, 2), 20000).into(); // Local addresses (base addresses for relay candidates) let l_local_addr: SocketAddr = (Ipv4Addr::new(192, 168, 1, 1), 1000).into(); let r_local_addr: SocketAddr = (Ipv4Addr::new(192, 168, 1, 2), 2000).into(); // Host addresses (higher priority, will be trickled later) let l_host_addr: SocketAddr = (Ipv4Addr::new(1, 1, 1, 1), 1000).into(); let r_host_addr: SocketAddr = (Ipv4Addr::new(2, 2, 2, 2), 2000).into(); // Create relay candidates (lower priority than host) let l_relay = Candidate::relayed(l_relay_addr, l_local_addr, "udp").unwrap(); let r_relay = Candidate::relayed(r_relay_addr, r_local_addr, "udp").unwrap(); // Add relay candidates initially l.rtc.add_local_candidate(l_relay.clone()).unwrap(); r.rtc.add_local_candidate(r_relay.clone()).unwrap(); // Exchange relay candidates l.rtc.add_remote_candidate(r_relay.clone()); r.rtc.add_remote_candidate(l_relay.clone()); // Create offer/answer let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; // Connect using relay candidates loop { if l.is_connected() && r.is_connected() { break; } if l.duration() > Duration::from_secs(5) { panic!("Failed to connect with relay candidates"); } progress(&mut l, &mut r)?; } // Get the data channel id let channel_id = l .events .iter() .find_map(|(_, e)| { if let str0m::Event::ChannelOpen(id, _) = e { Some(*id) } else { None } }) .expect("Should have opened a data channel"); // Send data to trigger a Transmit and capture the source address l.rtc .channel(channel_id) .unwrap() .write(true, b"test") .unwrap(); let mut initial_send_addr: Option = None; l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap(); loop { match l.rtc.poll_output() { Ok(Output::Transmit(t)) => { initial_send_addr = Some(t.source); break; } Ok(Output::Timeout(_)) => break, Ok(_) => continue, Err(e) => return Err(e), } } let initial_send_addr = initial_send_addr.expect("Should have a Transmit after sending data"); assert_eq!( initial_send_addr, l_relay_addr, "Initial send address should be relay candidate" ); // Now trickle in host candidates (higher priority) let l_host = Candidate::host(l_host_addr, "udp").unwrap(); let r_host = Candidate::host(r_host_addr, "udp").unwrap(); let l_host_added = l.rtc.add_local_candidate(l_host.clone()).unwrap().clone(); let r_host_added = r.rtc.add_local_candidate(r_host.clone()).unwrap().clone(); // Exchange host candidates between peers (simulating trickle ICE signaling) l.rtc.add_remote_candidate(r_host_added); r.rtc.add_remote_candidate(l_host_added); // Progress to allow ICE to discover and switch to better candidates for _ in 0..100 { progress(&mut l, &mut r)?; } // Send data again and capture the new source address - should have switched to host l.rtc .channel(channel_id) .unwrap() .write(true, b"test2") .unwrap(); let mut final_send_addr: Option = None; l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap(); loop { match l.rtc.poll_output() { Ok(Output::Transmit(t)) => { final_send_addr = Some(t.source); break; } Ok(Output::Timeout(_)) => break, Ok(_) => continue, Err(e) => return Err(e), } } let final_send_addr = final_send_addr.expect("Should have a Transmit after sending data post-trickle"); // Verify that ICE switched from relay to host candidates assert_eq!( final_send_addr, l_host_addr, "After trickle, send address should switch to host candidate" ); // Verify the switch actually happened assert_ne!( initial_send_addr, final_send_addr, "Send address should have changed from relay ({}) to host ({})", initial_send_addr, final_send_addr ); Ok(()) } /// Test custom ICE credentials via set_local_ice_credentials(). #[test] fn ice_custom_credentials() -> Result<(), RtcError> { init_log(); init_crypto_default(); let custom_creds = IceCreds { ufrag: "customufrag123".into(), pass: "custompassword456789012".into(), }; let rtc = RtcConfig::new() .set_local_ice_credentials(custom_creds.clone()) .build(Instant::now()); let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc); let mut r = TestRtc::new(Peer::Right); l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); // Verify custom credentials are used let actual_creds = l._local_ice_creds(); assert_eq!( actual_creds.ufrag, custom_creds.ufrag, "Custom ufrag should be used" ); assert_eq!( actual_creds.pass, custom_creds.pass, "Custom password should be used" ); let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); // Verify custom ufrag appears in offer SDP let offer_str = offer.to_string(); assert!( offer_str.contains(&custom_creds.ufrag), "Offer SDP should contain custom ufrag" ); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; loop { if l.is_connected() && r.is_connected() { break; } if l.duration() > Duration::from_secs(5) { panic!("Failed to connect with custom credentials"); } progress(&mut l, &mut r)?; } Ok(()) } /// Test set_initial_stun_rto() configuration by measuring retransmission timing. /// Uses a short RTO and verifies retransmissions happen at the expected interval. #[test] fn ice_stun_timeout_initial_rto() -> Result<(), RtcError> { use str0m::Output; init_log(); init_crypto_default(); // Set a short initial RTO of 50ms (default is 250ms) let custom_rto = Duration::from_millis(50); let mut config = RtcConfig::new(); config.set_initial_stun_rto(custom_rto); let start = Instant::now(); let rtc = config.build(start); let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc); let mut r = TestRtc::new(Peer::Right); // Sync TestRtc time with Rtc creation time l.start = start; l.last = start; l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); // Set up SDP exchange let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; // Track transmit times from L only (don't deliver to R, so L will retransmit) let mut transmit_times: Vec = Vec::new(); // Progress L only, capture STUN transmit times for _ in 0..30 { l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap(); loop { match l.rtc.poll_output()? { Output::Transmit(_) => { transmit_times.push(l.last); } Output::Timeout(t) => { l.last = t; break; } Output::Event(_) => {} } } // Stop once we have enough samples if transmit_times.len() >= 3 { break; } } assert!( transmit_times.len() >= 2, "Should have at least 2 STUN transmissions, got {}", transmit_times.len() ); // Check the interval between first and second transmit matches initial RTO let first_interval = transmit_times[1].duration_since(transmit_times[0]); // Allow some tolerance (35-65ms for 50ms RTO) let min_expected = custom_rto - Duration::from_millis(15); let max_expected = custom_rto + Duration::from_millis(15); assert!( first_interval >= min_expected && first_interval <= max_expected, "First retransmit interval should be ~{}ms (initial RTO), got {}ms", custom_rto.as_millis(), first_interval.as_millis() ); Ok(()) } /// Test set_max_stun_rto() configuration by verifying retransmit intervals are capped. #[test] fn ice_stun_timeout_max_rto() -> Result<(), RtcError> { use str0m::Output; init_log(); init_crypto_default(); // Set initial RTO to 100ms and max RTO to 150ms // Without max cap, RTO would double: 100 -> 200 -> 400... // With max 150ms, it should cap at: 100 -> 150 -> 150... let initial_rto = Duration::from_millis(100); let max_rto = Duration::from_millis(150); let mut config = RtcConfig::new(); config.set_initial_stun_rto(initial_rto); config.set_max_stun_rto(max_rto); let start = Instant::now(); let rtc = config.build(start); let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc); let mut r = TestRtc::new(Peer::Right); // Sync TestRtc time with Rtc creation time l.start = start; l.last = start; l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; // Track transmit times from L only (don't deliver to R) let mut transmit_times: Vec = Vec::new(); for _ in 0..50 { l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap(); loop { match l.rtc.poll_output()? { Output::Transmit(_) => { transmit_times.push(l.last); } Output::Timeout(t) => { l.last = t; break; } Output::Event(_) => {} } } if transmit_times.len() >= 4 { break; } } assert!( transmit_times.len() >= 4, "Should have at least 4 transmissions, got {}", transmit_times.len() ); // Check intervals - first should be ~100ms, subsequent should be capped at ~150ms let interval_1_2 = transmit_times[1].duration_since(transmit_times[0]); let interval_2_3 = transmit_times[2].duration_since(transmit_times[1]); let interval_3_4 = transmit_times[3].duration_since(transmit_times[2]); // First interval should be around initial RTO (100ms) assert!( interval_1_2 >= Duration::from_millis(85) && interval_1_2 <= Duration::from_millis(115), "First interval should be ~100ms (initial RTO), got {}ms", interval_1_2.as_millis() ); // Later intervals should be capped at max RTO (150ms), not doubled (200ms) // Allow tolerance for timing let max_allowed = max_rto + Duration::from_millis(20); assert!( interval_2_3 <= max_allowed, "Second interval should be capped at ~{}ms (max RTO), got {}ms", max_rto.as_millis(), interval_2_3.as_millis() ); assert!( interval_3_4 <= max_allowed, "Third interval should be capped at ~{}ms (max RTO), got {}ms", max_rto.as_millis(), interval_3_4.as_millis() ); Ok(()) } /// Test set_max_stun_retransmits() configuration by counting actual retransmissions. #[test] fn ice_stun_max_retransmits() -> Result<(), RtcError> { use str0m::Output; init_log(); init_crypto_default(); // Set max retransmits to 3 (default is 9) and short RTOs for faster test let max_retransmits = 3; let mut config = RtcConfig::new(); config.set_max_stun_retransmits(max_retransmits); config.set_initial_stun_rto(Duration::from_millis(20)); config.set_max_stun_rto(Duration::from_millis(40)); let start = Instant::now(); let rtc = config.build(start); let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc); let mut r = TestRtc::new(Peer::Right); // Sync TestRtc time with Rtc creation time l.start = start; l.last = start; l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; // Count transmissions from L (without delivering to R) let mut transmit_count = 0; for _ in 0..100 { l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap(); loop { match l.rtc.poll_output()? { Output::Transmit(_) => { transmit_count += 1; } Output::Timeout(t) => { l.last = t; break; } Output::Event(_) => {} } } // Give enough time for all retransmits to happen if l.duration() > Duration::from_millis(500) { break; } } // With max_retransmits=3, we should see limited retransmissions // The exact count depends on implementation details, but should be bounded // Key verification: count should be <= max_retransmits + 1 (initial + retransmits) let expected_max = (max_retransmits + 1) as usize + 2; // +2 for timing tolerance assert!( transmit_count <= expected_max, "Transmit count should be bounded by max_retransmits setting. \ Expected at most {} (initial + {} retransmits + tolerance), got {}", expected_max, max_retransmits, transmit_count ); // Should have at least some transmissions assert!( transmit_count >= 2, "Should have at least 2 transmissions (initial + at least 1 retransmit), got {}", transmit_count ); Ok(()) } /// Test ICE lite mode connectivity. #[test] fn ice_lite_mode() -> Result<(), RtcError> { init_log(); init_crypto_default(); let mut l = TestRtc::new(Peer::Left); let rtc = RtcConfig::new().set_ice_lite(true).build(Instant::now()); let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc); l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); let (offer, pending) = l.span.in_scope(|| { let mut change = l.rtc.sdp_api(); let _ = change.add_channel("test".into()); change.apply().unwrap() }); let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; l.span .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; loop { if l.is_connected() && r.is_connected() { break; } if l.duration() > Duration::from_secs(5) { panic!("Failed to connect with ICE lite"); } progress(&mut l, &mut r)?; } Ok(()) }