Skip to content
File

Blob: firmware/vendor/str0m/tests/ice-candidates.rs

rust577 lines
1//! Tests for ICE candidate handling and configuration.
2 
3use std::net::Ipv4Addr;
4use std::time::{Duration, Instant};
5 
6use str0m::RtcConfig;
7use str0m::RtcError;
8use str0m::ice::IceCreds;
9use tracing::info_span;
10 
11mod common;
12use common::{Peer, TestRtc, init_crypto_default, init_log, progress};
13 
14/// Test connection with only host candidates.
15#[test]
16fn ice_candidate_types_host_only() -> Result<(), RtcError> {
17 init_log();
18 init_crypto_default();
19 
20 let mut l = TestRtc::new(Peer::Left);
21 let mut r = TestRtc::new(Peer::Right);
22 
23 // Add only host candidates (no srflx or relay)
24 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
25 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
26 
27 let (offer, pending) = l.span.in_scope(|| {
28 let mut change = l.rtc.sdp_api();
29 let _ = change.add_channel("test".into());
30 change.apply().unwrap()
31 });
32 
33 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
34 l.span
35 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
36 
37 // Should connect using host candidates
38 loop {
39 if l.is_connected() && r.is_connected() {
40 break;
41 }
42 if l.duration() > Duration::from_secs(5) {
43 panic!("Failed to connect with host-only candidates");
44 }
45 progress(&mut l, &mut r)?;
46 }
47 
48 Ok(())
49}
50 
51/// Test trickle ICE - connect with relay candidates, then trickle in host candidates.
52/// Verifies that ICE switches to the better (host) candidates after they're added.
53#[test]
54fn ice_trickle_incremental_candidates() -> Result<(), RtcError> {
55 use std::net::SocketAddr;
56 use str0m::{Candidate, Output};
57 
58 init_log();
59 init_crypto_default();
60 
61 let mut l = TestRtc::new(Peer::Left);
62 let mut r = TestRtc::new(Peer::Right);
63 
64 // Define addresses for relay and host candidates
65 // Relay addresses (TURN allocated addresses, lower priority)
66 let l_relay_addr: SocketAddr = (Ipv4Addr::new(10, 0, 0, 1), 10000).into();
67 let r_relay_addr: SocketAddr = (Ipv4Addr::new(10, 0, 0, 2), 20000).into();
68 
69 // Local addresses (base addresses for relay candidates)
70 let l_local_addr: SocketAddr = (Ipv4Addr::new(192, 168, 1, 1), 1000).into();
71 let r_local_addr: SocketAddr = (Ipv4Addr::new(192, 168, 1, 2), 2000).into();
72 
73 // Host addresses (higher priority, will be trickled later)
74 let l_host_addr: SocketAddr = (Ipv4Addr::new(1, 1, 1, 1), 1000).into();
75 let r_host_addr: SocketAddr = (Ipv4Addr::new(2, 2, 2, 2), 2000).into();
76 
77 // Create relay candidates (lower priority than host)
78 let l_relay = Candidate::relayed(l_relay_addr, l_local_addr, "udp").unwrap();
79 let r_relay = Candidate::relayed(r_relay_addr, r_local_addr, "udp").unwrap();
80 
81 // Add relay candidates initially
82 l.rtc.add_local_candidate(l_relay.clone()).unwrap();
83 r.rtc.add_local_candidate(r_relay.clone()).unwrap();
84 
85 // Exchange relay candidates
86 l.rtc.add_remote_candidate(r_relay.clone());
87 r.rtc.add_remote_candidate(l_relay.clone());
88 
89 // Create offer/answer
90 let (offer, pending) = l.span.in_scope(|| {
91 let mut change = l.rtc.sdp_api();
92 let _ = change.add_channel("test".into());
93 change.apply().unwrap()
94 });
95 
96 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
97 l.span
98 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
99 
100 // Connect using relay candidates
101 loop {
102 if l.is_connected() && r.is_connected() {
103 break;
104 }
105 if l.duration() > Duration::from_secs(5) {
106 panic!("Failed to connect with relay candidates");
107 }
108 progress(&mut l, &mut r)?;
109 }
110 
111 // Get the data channel id
112 let channel_id = l
113 .events
114 .iter()
115 .find_map(|(_, e)| {
116 if let str0m::Event::ChannelOpen(id, _) = e {
117 Some(*id)
118 } else {
119 None
120 }
121 })
122 .expect("Should have opened a data channel");
123 
124 // Send data to trigger a Transmit and capture the source address
125 l.rtc
126 .channel(channel_id)
127 .unwrap()
128 .write(true, b"test")
129 .unwrap();
130 
131 let mut initial_send_addr: Option<SocketAddr> = None;
132 l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap();
133 loop {
134 match l.rtc.poll_output() {
135 Ok(Output::Transmit(t)) => {
136 initial_send_addr = Some(t.source);
137 break;
138 }
139 Ok(Output::Timeout(_)) => break,
140 Ok(_) => continue,
141 Err(e) => return Err(e),
142 }
143 }
144 
145 let initial_send_addr = initial_send_addr.expect("Should have a Transmit after sending data");
146 assert_eq!(
147 initial_send_addr, l_relay_addr,
148 "Initial send address should be relay candidate"
149 );
150 
151 // Now trickle in host candidates (higher priority)
152 let l_host = Candidate::host(l_host_addr, "udp").unwrap();
153 let r_host = Candidate::host(r_host_addr, "udp").unwrap();
154 
155 let l_host_added = l.rtc.add_local_candidate(l_host.clone()).unwrap().clone();
156 let r_host_added = r.rtc.add_local_candidate(r_host.clone()).unwrap().clone();
157 
158 // Exchange host candidates between peers (simulating trickle ICE signaling)
159 l.rtc.add_remote_candidate(r_host_added);
160 r.rtc.add_remote_candidate(l_host_added);
161 
162 // Progress to allow ICE to discover and switch to better candidates
163 for _ in 0..100 {
164 progress(&mut l, &mut r)?;
165 }
166 
167 // Send data again and capture the new source address - should have switched to host
168 l.rtc
169 .channel(channel_id)
170 .unwrap()
171 .write(true, b"test2")
172 .unwrap();
173 
174 let mut final_send_addr: Option<SocketAddr> = None;
175 l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap();
176 loop {
177 match l.rtc.poll_output() {
178 Ok(Output::Transmit(t)) => {
179 final_send_addr = Some(t.source);
180 break;
181 }
182 Ok(Output::Timeout(_)) => break,
183 Ok(_) => continue,
184 Err(e) => return Err(e),
185 }
186 }
187 
188 let final_send_addr =
189 final_send_addr.expect("Should have a Transmit after sending data post-trickle");
190 
191 // Verify that ICE switched from relay to host candidates
192 assert_eq!(
193 final_send_addr, l_host_addr,
194 "After trickle, send address should switch to host candidate"
195 );
196 
197 // Verify the switch actually happened
198 assert_ne!(
199 initial_send_addr, final_send_addr,
200 "Send address should have changed from relay ({}) to host ({})",
201 initial_send_addr, final_send_addr
202 );
203 
204 Ok(())
205}
206 
207/// Test custom ICE credentials via set_local_ice_credentials().
208#[test]
209fn ice_custom_credentials() -> Result<(), RtcError> {
210 init_log();
211 init_crypto_default();
212 
213 let custom_creds = IceCreds {
214 ufrag: "customufrag123".into(),
215 pass: "custompassword456789012".into(),
216 };
217 
218 let rtc = RtcConfig::new()
219 .set_local_ice_credentials(custom_creds.clone())
220 .build(Instant::now());
221 
222 let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);
223 let mut r = TestRtc::new(Peer::Right);
224 
225 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
226 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
227 
228 // Verify custom credentials are used
229 let actual_creds = l._local_ice_creds();
230 assert_eq!(
231 actual_creds.ufrag, custom_creds.ufrag,
232 "Custom ufrag should be used"
233 );
234 assert_eq!(
235 actual_creds.pass, custom_creds.pass,
236 "Custom password should be used"
237 );
238 
239 let (offer, pending) = l.span.in_scope(|| {
240 let mut change = l.rtc.sdp_api();
241 let _ = change.add_channel("test".into());
242 change.apply().unwrap()
243 });
244 
245 // Verify custom ufrag appears in offer SDP
246 let offer_str = offer.to_string();
247 assert!(
248 offer_str.contains(&custom_creds.ufrag),
249 "Offer SDP should contain custom ufrag"
250 );
251 
252 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
253 l.span
254 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
255 
256 loop {
257 if l.is_connected() && r.is_connected() {
258 break;
259 }
260 if l.duration() > Duration::from_secs(5) {
261 panic!("Failed to connect with custom credentials");
262 }
263 progress(&mut l, &mut r)?;
264 }
265 
266 Ok(())
267}
268 
269/// Test set_initial_stun_rto() configuration by measuring retransmission timing.
270/// Uses a short RTO and verifies retransmissions happen at the expected interval.
271#[test]
272fn ice_stun_timeout_initial_rto() -> Result<(), RtcError> {
273 use str0m::Output;
274 
275 init_log();
276 init_crypto_default();
277 
278 // Set a short initial RTO of 50ms (default is 250ms)
279 let custom_rto = Duration::from_millis(50);
280 let mut config = RtcConfig::new();
281 config.set_initial_stun_rto(custom_rto);
282 let start = Instant::now();
283 let rtc = config.build(start);
284 
285 let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);
286 let mut r = TestRtc::new(Peer::Right);
287 
288 // Sync TestRtc time with Rtc creation time
289 l.start = start;
290 l.last = start;
291 
292 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
293 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
294 
295 // Set up SDP exchange
296 let (offer, pending) = l.span.in_scope(|| {
297 let mut change = l.rtc.sdp_api();
298 let _ = change.add_channel("test".into());
299 change.apply().unwrap()
300 });
301 
302 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
303 l.span
304 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
305 
306 // Track transmit times from L only (don't deliver to R, so L will retransmit)
307 let mut transmit_times: Vec<Instant> = Vec::new();
308 
309 // Progress L only, capture STUN transmit times
310 for _ in 0..30 {
311 l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap();
312 
313 loop {
314 match l.rtc.poll_output()? {
315 Output::Transmit(_) => {
316 transmit_times.push(l.last);
317 }
318 Output::Timeout(t) => {
319 l.last = t;
320 break;
321 }
322 Output::Event(_) => {}
323 }
324 }
325 
326 // Stop once we have enough samples
327 if transmit_times.len() >= 3 {
328 break;
329 }
330 }
331 
332 assert!(
333 transmit_times.len() >= 2,
334 "Should have at least 2 STUN transmissions, got {}",
335 transmit_times.len()
336 );
337 
338 // Check the interval between first and second transmit matches initial RTO
339 let first_interval = transmit_times[1].duration_since(transmit_times[0]);
340 
341 // Allow some tolerance (35-65ms for 50ms RTO)
342 let min_expected = custom_rto - Duration::from_millis(15);
343 let max_expected = custom_rto + Duration::from_millis(15);
344 
345 assert!(
346 first_interval >= min_expected && first_interval <= max_expected,
347 "First retransmit interval should be ~{}ms (initial RTO), got {}ms",
348 custom_rto.as_millis(),
349 first_interval.as_millis()
350 );
351 
352 Ok(())
353}
354 
355/// Test set_max_stun_rto() configuration by verifying retransmit intervals are capped.
356#[test]
357fn ice_stun_timeout_max_rto() -> Result<(), RtcError> {
358 use str0m::Output;
359 
360 init_log();
361 init_crypto_default();
362 
363 // Set initial RTO to 100ms and max RTO to 150ms
364 // Without max cap, RTO would double: 100 -> 200 -> 400...
365 // With max 150ms, it should cap at: 100 -> 150 -> 150...
366 let initial_rto = Duration::from_millis(100);
367 let max_rto = Duration::from_millis(150);
368 
369 let mut config = RtcConfig::new();
370 config.set_initial_stun_rto(initial_rto);
371 config.set_max_stun_rto(max_rto);
372 let start = Instant::now();
373 let rtc = config.build(start);
374 
375 let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);
376 let mut r = TestRtc::new(Peer::Right);
377 
378 // Sync TestRtc time with Rtc creation time
379 l.start = start;
380 l.last = start;
381 
382 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
383 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
384 
385 let (offer, pending) = l.span.in_scope(|| {
386 let mut change = l.rtc.sdp_api();
387 let _ = change.add_channel("test".into());
388 change.apply().unwrap()
389 });
390 
391 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
392 l.span
393 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
394 
395 // Track transmit times from L only (don't deliver to R)
396 let mut transmit_times: Vec<Instant> = Vec::new();
397 
398 for _ in 0..50 {
399 l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap();
400 
401 loop {
402 match l.rtc.poll_output()? {
403 Output::Transmit(_) => {
404 transmit_times.push(l.last);
405 }
406 Output::Timeout(t) => {
407 l.last = t;
408 break;
409 }
410 Output::Event(_) => {}
411 }
412 }
413 
414 if transmit_times.len() >= 4 {
415 break;
416 }
417 }
418 
419 assert!(
420 transmit_times.len() >= 4,
421 "Should have at least 4 transmissions, got {}",
422 transmit_times.len()
423 );
424 
425 // Check intervals - first should be ~100ms, subsequent should be capped at ~150ms
426 let interval_1_2 = transmit_times[1].duration_since(transmit_times[0]);
427 let interval_2_3 = transmit_times[2].duration_since(transmit_times[1]);
428 let interval_3_4 = transmit_times[3].duration_since(transmit_times[2]);
429 
430 // First interval should be around initial RTO (100ms)
431 assert!(
432 interval_1_2 >= Duration::from_millis(85) && interval_1_2 <= Duration::from_millis(115),
433 "First interval should be ~100ms (initial RTO), got {}ms",
434 interval_1_2.as_millis()
435 );
436 
437 // Later intervals should be capped at max RTO (150ms), not doubled (200ms)
438 // Allow tolerance for timing
439 let max_allowed = max_rto + Duration::from_millis(20);
440 assert!(
441 interval_2_3 <= max_allowed,
442 "Second interval should be capped at ~{}ms (max RTO), got {}ms",
443 max_rto.as_millis(),
444 interval_2_3.as_millis()
445 );
446 assert!(
447 interval_3_4 <= max_allowed,
448 "Third interval should be capped at ~{}ms (max RTO), got {}ms",
449 max_rto.as_millis(),
450 interval_3_4.as_millis()
451 );
452 
453 Ok(())
454}
455 
456/// Test set_max_stun_retransmits() configuration by counting actual retransmissions.
457#[test]
458fn ice_stun_max_retransmits() -> Result<(), RtcError> {
459 use str0m::Output;
460 
461 init_log();
462 init_crypto_default();
463 
464 // Set max retransmits to 3 (default is 9) and short RTOs for faster test
465 let max_retransmits = 3;
466 let mut config = RtcConfig::new();
467 config.set_max_stun_retransmits(max_retransmits);
468 config.set_initial_stun_rto(Duration::from_millis(20));
469 config.set_max_stun_rto(Duration::from_millis(40));
470 let start = Instant::now();
471 let rtc = config.build(start);
472 
473 let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);
474 let mut r = TestRtc::new(Peer::Right);
475 
476 // Sync TestRtc time with Rtc creation time
477 l.start = start;
478 l.last = start;
479 
480 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
481 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
482 
483 let (offer, pending) = l.span.in_scope(|| {
484 let mut change = l.rtc.sdp_api();
485 let _ = change.add_channel("test".into());
486 change.apply().unwrap()
487 });
488 
489 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
490 l.span
491 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
492 
493 // Count transmissions from L (without delivering to R)
494 let mut transmit_count = 0;
495 
496 for _ in 0..100 {
497 l.rtc.handle_input(str0m::Input::Timeout(l.last)).unwrap();
498 
499 loop {
500 match l.rtc.poll_output()? {
501 Output::Transmit(_) => {
502 transmit_count += 1;
503 }
504 Output::Timeout(t) => {
505 l.last = t;
506 break;
507 }
508 Output::Event(_) => {}
509 }
510 }
511 
512 // Give enough time for all retransmits to happen
513 if l.duration() > Duration::from_millis(500) {
514 break;
515 }
516 }
517 
518 // With max_retransmits=3, we should see limited retransmissions
519 // The exact count depends on implementation details, but should be bounded
520 // Key verification: count should be <= max_retransmits + 1 (initial + retransmits)
521 let expected_max = (max_retransmits + 1) as usize + 2; // +2 for timing tolerance
522 
523 assert!(
524 transmit_count <= expected_max,
525 "Transmit count should be bounded by max_retransmits setting. \
526 Expected at most {} (initial + {} retransmits + tolerance), got {}",
527 expected_max,
528 max_retransmits,
529 transmit_count
530 );
531 
532 // Should have at least some transmissions
533 assert!(
534 transmit_count >= 2,
535 "Should have at least 2 transmissions (initial + at least 1 retransmit), got {}",
536 transmit_count
537 );
538 
539 Ok(())
540}
541 
542/// Test ICE lite mode connectivity.
543#[test]
544fn ice_lite_mode() -> Result<(), RtcError> {
545 init_log();
546 init_crypto_default();
547 
548 let mut l = TestRtc::new(Peer::Left);
549 let rtc = RtcConfig::new().set_ice_lite(true).build(Instant::now());
550 let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc);
551 
552 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
553 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
554 
555 let (offer, pending) = l.span.in_scope(|| {
556 let mut change = l.rtc.sdp_api();
557 let _ = change.add_channel("test".into());
558 change.apply().unwrap()
559 });
560 
561 let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
562 l.span
563 .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;
564 
565 loop {
566 if l.is_connected() && r.is_connected() {
567 break;
568 }
569 if l.duration() > Duration::from_secs(5) {
570 panic!("Failed to connect with ICE lite");
571 }
572 progress(&mut l, &mut r)?;
573 }
574 
575 Ok(())
576}