@desklink/host 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +45 -18
  2. package/bin/desklink-host.mjs +5 -1
  3. package/dist/bridge.d.ts +7 -0
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +55 -7
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts +9 -2
  8. package/dist/engineProcess.d.ts.map +1 -1
  9. package/dist/engineProcess.js +26 -15
  10. package/dist/engineProcess.js.map +1 -1
  11. package/dist/protocol.d.ts +1 -1
  12. package/dist/protocol.d.ts.map +1 -1
  13. package/dist/protocol.js +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/docs/BOUNDARY.md +4 -1
  16. package/docs/PROTOCOL.md +20 -5
  17. package/engine/Cargo.lock +6 -8
  18. package/engine/Cargo.toml +13 -1
  19. package/engine/src/clipboard.rs +0 -34
  20. package/engine/src/convert.rs +0 -170
  21. package/engine/src/cursor.rs +0 -23
  22. package/engine/src/input.rs +0 -65
  23. package/engine/src/input_mac.rs +0 -11
  24. package/engine/src/keeper.rs +0 -137
  25. package/engine/src/keymap.rs +0 -107
  26. package/engine/src/keymap_mac.rs +0 -25
  27. package/engine/src/mac.rs +0 -28
  28. package/engine/src/main.rs +3 -2
  29. package/engine/src/peer.rs +231 -1
  30. package/engine/src/portal.rs +0 -18
  31. package/engine/src/protocol.rs +0 -162
  32. package/engine/src/session.rs +37 -1
  33. package/engine/src/x11.rs +0 -13
  34. package/engine/vendor/rtc-ice/Cargo.toml +137 -0
  35. package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +59 -0
  36. package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
  37. package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
  38. package/engine/vendor/rtc-ice/README.md +30 -0
  39. package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
  40. package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
  41. package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
  42. package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +566 -0
  43. package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
  44. package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3388 -0
  45. package/engine/vendor/rtc-ice/src/agent/mod.rs +1662 -0
  46. package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
  47. package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
  48. package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
  49. package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
  50. package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
  51. package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
  52. package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
  53. package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
  54. package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
  55. package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
  56. package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
  57. package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
  58. package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
  59. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
  60. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
  61. package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
  62. package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
  63. package/engine/vendor/rtc-ice/src/lib.rs +85 -0
  64. package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
  65. package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
  66. package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
  67. package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
  68. package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
  69. package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
  70. package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
  71. package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
  72. package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
  73. package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
  74. package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
  75. package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
  76. package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
  77. package/package.json +8 -3
@@ -0,0 +1,3388 @@
1
+ use std::str::FromStr;
2
+ use std::time::Instant;
3
+ use stun::message::*;
4
+ use stun::textattrs::Username;
5
+
6
+ use super::*;
7
+ use crate::attributes::{
8
+ control::AttrControlling, priority::PriorityAttr, use_candidate::UseCandidateAttr,
9
+ };
10
+ use crate::candidate::candidate_host::*;
11
+ use crate::candidate::candidate_peer_reflexive::*;
12
+ use crate::candidate::candidate_relay::CandidateRelayConfig;
13
+ use crate::candidate::candidate_server_reflexive::*;
14
+ use crate::candidate::*;
15
+
16
+ /// Explicit provider for tests. The default-resolving STUN constructors were removed before 1.0,
17
+ /// so every `MessageIntegrity` now names its provider.
18
+ fn test_crypto_provider() -> std::sync::Arc<dyn crypto::RTCCryptoProvider> {
19
+ crypto::default_provider().expect("a built-in crypto provider must be enabled for tests")
20
+ }
21
+
22
+ #[test]
23
+ fn test_pair_search() -> Result<()> {
24
+ let config = Arc::new(AgentConfig::default());
25
+ let mut a = Agent::new(Instant::now(), config, test_crypto_provider())?;
26
+
27
+ assert!(
28
+ a.candidate_pairs.is_empty(),
29
+ "TestPairSearch is only a valid test if a.validPairs is empty on construction"
30
+ );
31
+
32
+ let cp = a.get_best_available_pair();
33
+ assert!(cp.is_none(), "No Candidate pairs should exist");
34
+
35
+ a.close()?;
36
+
37
+ Ok(())
38
+ }
39
+
40
+ #[test]
41
+ fn test_pair_priority() -> Result<()> {
42
+ let mut a = Agent::new(
43
+ Instant::now(),
44
+ Arc::new(AgentConfig::default()),
45
+ test_crypto_provider(),
46
+ )?;
47
+
48
+ let host_config = CandidateHostConfig {
49
+ base_config: CandidateConfig {
50
+ network: "udp".to_owned(),
51
+ address: "192.168.1.1".to_owned(),
52
+ port: 19216,
53
+ component: 1,
54
+ ..Default::default()
55
+ },
56
+ ..Default::default()
57
+ };
58
+ let host_local = host_config.new_candidate_host()?;
59
+ a.local_candidates.push(host_local);
60
+
61
+ let relay_config = CandidateRelayConfig {
62
+ base_config: CandidateConfig {
63
+ network: "udp".to_owned(),
64
+ address: "1.2.3.4".to_owned(),
65
+ port: 12340,
66
+ component: 1,
67
+ ..Default::default()
68
+ },
69
+ rel_addr: "4.3.2.1".to_owned(),
70
+ rel_port: 43210,
71
+ ..Default::default()
72
+ };
73
+
74
+ let relay_remote = relay_config.new_candidate_relay()?;
75
+
76
+ let srflx_config = CandidateServerReflexiveConfig {
77
+ base_config: CandidateConfig {
78
+ network: "udp".to_owned(),
79
+ address: "10.10.10.2".to_owned(),
80
+ port: 19218,
81
+ component: 1,
82
+ ..Default::default()
83
+ },
84
+ rel_addr: "4.3.2.1".to_owned(),
85
+ rel_port: 43212,
86
+ ..Default::default()
87
+ };
88
+
89
+ let srflx_remote = srflx_config.new_candidate_server_reflexive()?;
90
+
91
+ let prflx_config = CandidatePeerReflexiveConfig {
92
+ base_config: CandidateConfig {
93
+ network: "udp".to_owned(),
94
+ address: "10.10.10.2".to_owned(),
95
+ port: 19217,
96
+ component: 1,
97
+ ..Default::default()
98
+ },
99
+ rel_addr: "4.3.2.1".to_owned(),
100
+ rel_port: 43211,
101
+ ..Default::default()
102
+ };
103
+
104
+ let prflx_remote = prflx_config.new_candidate_peer_reflexive()?;
105
+
106
+ let host_config = CandidateHostConfig {
107
+ base_config: CandidateConfig {
108
+ network: "udp".to_owned(),
109
+ address: "1.2.3.5".to_owned(),
110
+ port: 12350,
111
+ component: 1,
112
+ ..Default::default()
113
+ },
114
+ ..Default::default()
115
+ };
116
+ let host_remote = host_config.new_candidate_host()?;
117
+
118
+ let remotes: Vec<Candidate> = vec![relay_remote, srflx_remote, prflx_remote, host_remote];
119
+ for remote in remotes {
120
+ a.remote_candidates.push(remote);
121
+ }
122
+
123
+ {
124
+ let local = 0;
125
+ for remote in 0..a.remote_candidates.len() {
126
+ if a.find_pair(local, remote).is_none() {
127
+ a.add_pair(local, remote);
128
+ }
129
+
130
+ if let Some(p) = a.find_pair(local, remote) {
131
+ a.candidate_pairs[p].state = CandidatePairState::Succeeded;
132
+ }
133
+
134
+ if let Some(best_pair) = a.get_best_available_pair() {
135
+ assert_eq!(
136
+ a.candidate_pairs[best_pair].to_string(),
137
+ CandidatePair::new(
138
+ local,
139
+ remote,
140
+ a.local_candidates[local].priority(),
141
+ a.remote_candidates[remote].priority(),
142
+ a.is_controlling,
143
+ )
144
+ .to_string(),
145
+ "Unexpected bestPair {best_pair} (expected remote: {remote})",
146
+ );
147
+ } else {
148
+ panic!("expected Some, but got None");
149
+ }
150
+ }
151
+ }
152
+
153
+ a.close()?;
154
+ Ok(())
155
+ }
156
+
157
+ fn pipe(
158
+ default_config0: Option<AgentConfig>,
159
+ default_config1: Option<AgentConfig>,
160
+ ) -> Result<(Agent, Agent)> {
161
+ let mut cfg0 = if let Some(cfg) = default_config0 {
162
+ cfg
163
+ } else {
164
+ AgentConfig::default()
165
+ };
166
+ cfg0.urls = vec![];
167
+
168
+ let a_agent = Agent::new(Instant::now(), Arc::new(cfg0), test_crypto_provider())?;
169
+
170
+ let mut cfg1 = if let Some(cfg) = default_config1 {
171
+ cfg
172
+ } else {
173
+ AgentConfig::default()
174
+ };
175
+ cfg1.urls = vec![];
176
+
177
+ let b_agent = Agent::new(Instant::now(), Arc::new(cfg1), test_crypto_provider())?;
178
+
179
+ Ok((a_agent, b_agent))
180
+ }
181
+
182
+ #[test]
183
+ fn test_on_selected_candidate_pair_change() -> Result<()> {
184
+ let mut a = Agent::new(
185
+ Instant::now(),
186
+ Arc::new(AgentConfig::default()),
187
+ test_crypto_provider(),
188
+ )?;
189
+
190
+ let host_config = CandidateHostConfig {
191
+ base_config: CandidateConfig {
192
+ network: "udp".to_owned(),
193
+ address: "192.168.1.1".to_owned(),
194
+ port: 19216,
195
+ component: 1,
196
+ ..Default::default()
197
+ },
198
+ ..Default::default()
199
+ };
200
+ let host_local = host_config.new_candidate_host()?;
201
+ a.add_local_candidate(host_local)?;
202
+
203
+ let relay_config = CandidateRelayConfig {
204
+ base_config: CandidateConfig {
205
+ network: "udp".to_owned(),
206
+ address: "1.2.3.4".to_owned(),
207
+ port: 12340,
208
+ component: 1,
209
+ ..Default::default()
210
+ },
211
+ rel_addr: "4.3.2.1".to_owned(),
212
+ rel_port: 43210,
213
+ ..Default::default()
214
+ };
215
+ let relay_remote = relay_config.new_candidate_relay()?;
216
+ a.add_remote_candidate(relay_remote)?;
217
+
218
+ // select the pair
219
+ let (local, remote) = (0, 0);
220
+ a.add_pair(local, remote);
221
+ a.set_selected_pair(Some(Instant::now()), Some(0));
222
+
223
+ // ensure that the callback fired on setting the pair
224
+ let mut is_selected_candidate_pair_change_event_fired = false;
225
+ while let Some(event) = a.poll_event() {
226
+ if let Event::SelectedCandidatePairChange(_, _) = event.event {
227
+ is_selected_candidate_pair_change_event_fired = true;
228
+ }
229
+ }
230
+
231
+ assert!(is_selected_candidate_pair_change_event_fired);
232
+
233
+ a.close()?;
234
+ Ok(())
235
+ }
236
+
237
+ #[test]
238
+ fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> {
239
+ let mut a = Agent::new(
240
+ Instant::now(),
241
+ Arc::new(AgentConfig::default()),
242
+ test_crypto_provider(),
243
+ )?;
244
+
245
+ let host_config = CandidateHostConfig {
246
+ base_config: CandidateConfig {
247
+ network: "udp".to_owned(),
248
+ address: "192.168.0.2".to_owned(),
249
+ port: 777,
250
+ component: 1,
251
+ ..Default::default()
252
+ },
253
+ ..Default::default()
254
+ };
255
+
256
+ let local_candidate = host_config.new_candidate_host()?;
257
+ let local = 0;
258
+ let local_priority = local_candidate.priority();
259
+ let local_network_type = local_candidate.network_type();
260
+ a.add_local_candidate(local_candidate)?;
261
+
262
+ let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
263
+
264
+ let (username, local_pwd, tie_breaker) = {
265
+ a.ufrag_pwd.remote_credentials = Some(Credentials {
266
+ ufrag: "".to_string(),
267
+ pwd: "".to_string(),
268
+ });
269
+ (
270
+ a.ufrag_pwd.local_credentials.ufrag.to_owned() + ":",
271
+ a.ufrag_pwd.local_credentials.pwd.clone(),
272
+ a.tie_breaker,
273
+ )
274
+ };
275
+
276
+ let mut msg = Message::new();
277
+ msg.build(&[
278
+ Box::new(BINDING_REQUEST),
279
+ Box::new(TransactionId::new()),
280
+ Box::new(Username::new(ATTR_USERNAME, username)),
281
+ Box::new(UseCandidateAttr::new()),
282
+ Box::new(AttrControlling(tie_breaker)),
283
+ Box::new(PriorityAttr(local_priority)),
284
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
285
+ local_pwd,
286
+ test_crypto_provider().crypto(),
287
+ )),
288
+ Box::new(FINGERPRINT),
289
+ ])?;
290
+
291
+ {
292
+ a.handle_inbound(Instant::now(), &mut msg, local, remote_addr)?;
293
+
294
+ // length of remote candidate list must be one now
295
+ assert_eq!(
296
+ a.remote_candidates.len(),
297
+ 1,
298
+ "failed to add a network type to the remote candidate list"
299
+ );
300
+
301
+ // length of remote candidate list for a network type must be 1
302
+ if let Some(remote_index) = a.find_remote_candidate(remote_addr) {
303
+ let c = &a.remote_candidates[remote_index];
304
+
305
+ assert_eq!(
306
+ c.candidate_type(),
307
+ CandidateType::PeerReflexive,
308
+ "candidate type must be prflx"
309
+ );
310
+
311
+ assert_eq!(c.address(), "172.17.0.3", "IP address mismatch");
312
+
313
+ assert_eq!(c.port(), 999, "Port number mismatch");
314
+ } else {
315
+ assert!(
316
+ false,
317
+ "expected non-empty remote candidate for network type {}",
318
+ local_network_type,
319
+ );
320
+ }
321
+ }
322
+
323
+ a.close()?;
324
+ Ok(())
325
+ }
326
+
327
+ #[test]
328
+ fn test_handle_peer_reflexive_unknown_remote() -> Result<()> {
329
+ let mut a = Agent::new(
330
+ Instant::now(),
331
+ Arc::new(AgentConfig::default()),
332
+ test_crypto_provider(),
333
+ )?;
334
+
335
+ let mut tid = TransactionId::default();
336
+ tid.0[..3].copy_from_slice("ABC".as_bytes());
337
+
338
+ let remote_pwd = {
339
+ a.pending_binding_requests = vec![BindingRequest {
340
+ timestamp: Instant::now(),
341
+ transaction_id: tid,
342
+ destination: SocketAddr::from_str("0.0.0.0:0")?,
343
+ is_use_candidate: false,
344
+ }];
345
+ a.ufrag_pwd.remote_credentials = Some(Credentials {
346
+ ufrag: "".to_string(),
347
+ pwd: "".to_string(),
348
+ });
349
+ "".to_string()
350
+ };
351
+
352
+ let host_config = CandidateHostConfig {
353
+ base_config: CandidateConfig {
354
+ network: "udp".to_owned(),
355
+ address: "192.168.0.2".to_owned(),
356
+ port: 777,
357
+ component: 1,
358
+ ..Default::default()
359
+ },
360
+ ..Default::default()
361
+ };
362
+
363
+ let local = host_config.new_candidate_host()?;
364
+ let local_index = 0;
365
+ a.add_local_candidate(local)?;
366
+ let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
367
+
368
+ let mut msg = Message::new();
369
+ msg.build(&[
370
+ Box::new(BINDING_SUCCESS),
371
+ Box::new(tid),
372
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
373
+ remote_pwd,
374
+ test_crypto_provider().crypto(),
375
+ )),
376
+ Box::new(FINGERPRINT),
377
+ ])?;
378
+
379
+ let result = a.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
380
+ assert!(result.is_err());
381
+
382
+ assert_eq!(
383
+ a.remote_candidates.len(),
384
+ 0,
385
+ "unknown remote was able to create a candidate"
386
+ );
387
+
388
+ a.close()?;
389
+ Ok(())
390
+ }
391
+
392
+ /* TODO:
393
+ fn gather_and_exchange_candidates(a_agent: &mut Agent, b_agent: &mut Agent) -> Result<()> {
394
+ let wg = WaitGroup::new();
395
+
396
+ let w1 = Arc::new(Mutex::new(Some(wg.worker())));
397
+ a_agent.on_candidate(Box::new(
398
+ move |candidate: Option<Arc<dyn Candidate + Send + Sync>>| {
399
+ let w3 = Arc::clone(&w1);
400
+ Box::pin(async move {
401
+ if candidate.is_none() {
402
+ let mut w = w3.lock().await;
403
+ w.take();
404
+ }
405
+ })
406
+ },
407
+ ));
408
+ a_agent.gather_candidates()?;
409
+
410
+ let w2 = Arc::new(Mutex::new(Some(wg.worker())));
411
+ b_agent.on_candidate(Box::new(
412
+ move |candidate: Option<Arc<dyn Candidate + Send + Sync>>| {
413
+ let w3 = Arc::clone(&w2);
414
+ Box::pin(async move {
415
+ if candidate.is_none() {
416
+ let mut w = w3.lock().await;
417
+ w.take();
418
+ }
419
+ })
420
+ },
421
+ ));
422
+ b_agent.gather_candidates()?;
423
+
424
+ wg.wait().await;
425
+
426
+ let candidates = a_agent.get_local_candidates().await?;
427
+ for c in candidates {
428
+ let c2: Arc<dyn Candidate + Send + Sync> =
429
+ Arc::new(unmarshal_candidate(c.marshal().as_str())?);
430
+ b_agent.add_remote_candidate(&c2)?;
431
+ }
432
+
433
+ let candidates = b_agent.get_local_candidates().await?;
434
+ for c in candidates {
435
+ let c2: Arc<dyn Candidate + Send + Sync> =
436
+ Arc::new(unmarshal_candidate(c.marshal().as_str())?);
437
+ a_agent.add_remote_candidate(&c2)?;
438
+ }
439
+
440
+ Ok(())
441
+ }
442
+
443
+ // Assert that Agent on startup sends message, and doesn't wait for connectivityTicker to fire
444
+ #[test]
445
+ fn test_connectivity_on_startup() -> Result<()> {
446
+ let keepalive_interval = Some(Duration::from_secs(3600)); //time.Hour
447
+ let check_interval = Duration::from_secs(3600); //time.Hour
448
+ let cfg0 = AgentConfig {
449
+ keepalive_interval,
450
+ check_interval,
451
+ ..Default::default()
452
+ };
453
+
454
+ let mut a_agent = Agent::new(Instant::now(), cfg0, test_crypto_provider())?;
455
+
456
+ let cfg1 = AgentConfig {
457
+ keepalive_interval,
458
+ check_interval,
459
+ ..Default::default()
460
+ };
461
+
462
+ let mut b_agent = Agent::new(Instant::now(), cfg1, test_crypto_provider())?;
463
+
464
+ // Manual signaling
465
+ let (a_ufrag, a_pwd) = a_agent.get_local_user_credentials();
466
+ let (b_ufrag, b_pwd) = b_agent.get_local_user_credentials();
467
+
468
+ gather_and_exchange_candidates(&mut a_agent, &mut b_agent)?;
469
+
470
+ a_agent.on_connection_state_change(Box::new(move |s: ConnectionState| {
471
+ let accepted_tx_clone = Arc::clone(&accepting_tx);
472
+ Box::pin(async move {
473
+ if s == ConnectionState::Checking {
474
+ let mut tx = accepted_tx_clone.lock();
475
+ tx.take();
476
+ }
477
+ })
478
+ }));
479
+
480
+ tokio::spawn(async move {
481
+ let result = a_agent.accept(a_cancel_rx, b_ufrag, b_pwd);
482
+ assert!(result.is_ok(), "agent accept expected OK");
483
+ drop(accepted_tx);
484
+ });
485
+
486
+ let _ = accepting_rx.recv();
487
+
488
+ let _ = b_agent.dial(b_cancel_rx, a_ufrag, a_pwd)?;
489
+
490
+ // Ensure accepted
491
+ let _ = accepted_rx.recv();
492
+
493
+ Ok(())
494
+ }
495
+
496
+
497
+ #[test]
498
+ fn test_connectivity_lite() -> Result<()> {
499
+ let stun_server_url = Url {
500
+ scheme: SchemeType::Stun,
501
+ host: "1.2.3.4".to_owned(),
502
+ port: 3478,
503
+ proto: ProtoType::Udp,
504
+ ..Default::default()
505
+ };
506
+
507
+ let nat_type = nat::NatType {
508
+ mapping_behavior: nat::EndpointDependencyType::EndpointIndependent,
509
+ filtering_behavior: nat::EndpointDependencyType::EndpointIndependent,
510
+ ..Default::default()
511
+ };
512
+
513
+ let v = build_vnet(nat_type, nat_type)?;
514
+
515
+ let (a_notifier, mut a_connected) = on_connected();
516
+ let (b_notifier, mut b_connected) = on_connected();
517
+
518
+ let cfg0 = AgentConfig {
519
+ urls: vec![stun_server_url],
520
+ network_types: supported_network_types(),
521
+ net: Some(Arc::clone(&v.net0)),
522
+ ..Default::default()
523
+ };
524
+
525
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
526
+ a_agent.on_connection_state_change(a_notifier);
527
+
528
+ let cfg1 = AgentConfig {
529
+ urls: vec![],
530
+ lite: true,
531
+ candidate_types: vec![CandidateType::Host],
532
+ network_types: supported_network_types(),
533
+ net: Some(Arc::clone(&v.net1)),
534
+ ..Default::default()
535
+ };
536
+
537
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
538
+ b_agent.on_connection_state_change(b_notifier);
539
+
540
+ let _ = connect_with_vnet(&a_agent, &b_agent)?;
541
+
542
+ // Ensure pair selected
543
+ // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair
544
+ let _ = a_connected.recv();
545
+ let _ = b_connected.recv();
546
+
547
+ v.close()?;
548
+
549
+ Ok(())
550
+ }
551
+
552
+ struct MockPacketConn;
553
+
554
+ #[async_trait]
555
+ impl Conn for MockPacketConn {
556
+ fn connect(&self, _addr: SocketAddr) -> std::result::Result<(), util::Error> {
557
+ Ok(())
558
+ }
559
+
560
+ fn recv(&self, _buf: &mut [u8]) -> std::result::Result<usize, util::Error> {
561
+ Ok(0)
562
+ }
563
+
564
+ fn recv_from(&self, _buf: &mut [u8]) -> std::result::Result<(usize, SocketAddr), util::Error> {
565
+ Ok((0, SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0)))
566
+ }
567
+
568
+ fn send(&self, _buf: &[u8]) -> std::result::Result<usize, util::Error> {
569
+ Ok(0)
570
+ }
571
+
572
+ fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> std::result::Result<usize, util::Error> {
573
+ Ok(0)
574
+ }
575
+
576
+ fn local_addr(&self) -> std::result::Result<SocketAddr, util::Error> {
577
+ Ok(SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0))
578
+ }
579
+
580
+ fn remote_addr(&self) -> Option<SocketAddr> {
581
+ None
582
+ }
583
+
584
+ fn close(&self) -> std::result::Result<(), util::Error> {
585
+ Ok(())
586
+ }
587
+ }
588
+
589
+ fn build_msg(c: MessageClass, username: String, key: String) -> Result<Message> {
590
+ let mut msg = Message::new();
591
+ msg.build(&[
592
+ Box::new(MessageType::new(METHOD_BINDING, c)),
593
+ Box::new(TransactionId::new()),
594
+ Box::new(Username::new(ATTR_USERNAME, username)),
595
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(key, test_crypto_provider().crypto())),
596
+ Box::new(FINGERPRINT),
597
+ ])?;
598
+ Ok(msg)
599
+ }
600
+
601
+ #[test]
602
+ fn test_inbound_validity() -> Result<()> {
603
+ /*env_logger::Builder::new()
604
+ .format(|buf, record| {
605
+ writeln!(
606
+ buf,
607
+ "{}:{} [{}] {} - {}",
608
+ record.file().unwrap_or("unknown"),
609
+ record.line().unwrap_or(0),
610
+ record.level(),
611
+ chrono::Local::now().format("%H:%M:%S.%6f"),
612
+ record.args()
613
+ )
614
+ })
615
+ .filter(None, LevelFilter::Trace)
616
+ .init();*/
617
+
618
+ let remote = SocketAddr::from_str("172.17.0.3:999")?;
619
+ let local: Arc<dyn Candidate + Send + Sync> = Arc::new(
620
+ CandidateHostConfig {
621
+ base_config: CandidateConfig {
622
+ network: "udp".to_owned(),
623
+ address: "192.168.0.2".to_owned(),
624
+ port: 777,
625
+ component: 1,
626
+ conn: Some(Arc::new(MockPacketConn {})),
627
+ ..Default::default()
628
+ },
629
+ ..Default::default()
630
+ }
631
+ .new_candidate_host()?,
632
+ );
633
+
634
+ //"Invalid Binding requests should be discarded"
635
+ {
636
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
637
+
638
+ {
639
+ let local_pwd = {
640
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
641
+ ufrag_pwd.local_pwd.clone()
642
+ };
643
+ a.internal.handle_inbound(
644
+ &mut build_msg(CLASS_REQUEST, "invalid".to_owned(), local_pwd)?,
645
+ &local,
646
+ remote,
647
+ );
648
+ {
649
+ let remote_candidates = a.internal.remote_candidates.lock();
650
+ assert_ne!(
651
+ remote_candidates.len(),
652
+ 1,
653
+ "Binding with invalid Username was able to create prflx candidate"
654
+ );
655
+ }
656
+
657
+ let username = {
658
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
659
+ format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag)
660
+ };
661
+ a.internal.handle_inbound(
662
+ &mut build_msg(CLASS_REQUEST, username, "Invalid".to_owned())?,
663
+ &local,
664
+ remote,
665
+ );
666
+ {
667
+ let remote_candidates = a.internal.remote_candidates.lock();
668
+ assert_ne!(
669
+ remote_candidates.len(),
670
+ 1,
671
+ "Binding with invalid MessageIntegrity was able to create prflx candidate"
672
+ );
673
+ }
674
+ }
675
+
676
+ a.close()?;
677
+ }
678
+
679
+ //"Invalid Binding success responses should be discarded"
680
+ {
681
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
682
+
683
+ {
684
+ let username = {
685
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
686
+ format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag)
687
+ };
688
+ a.internal.handle_inbound(
689
+ &mut build_msg(CLASS_SUCCESS_RESPONSE, username, "Invalid".to_owned())?,
690
+ &local,
691
+ remote,
692
+ );
693
+ {
694
+ let remote_candidates = a.internal.remote_candidates.lock();
695
+ assert_ne!(
696
+ remote_candidates.len(),
697
+ 1,
698
+ "Binding with invalid Username was able to create prflx candidate"
699
+ );
700
+ }
701
+ }
702
+
703
+ a.close()?;
704
+ }
705
+
706
+ //"Discard non-binding messages"
707
+ {
708
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
709
+
710
+ {
711
+ let username = {
712
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
713
+ format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag)
714
+ };
715
+ a.internal.handle_inbound(
716
+ &mut build_msg(CLASS_ERROR_RESPONSE, username, "Invalid".to_owned())?,
717
+ &local,
718
+ remote,
719
+ );
720
+ let remote_candidates = a.internal.remote_candidates.lock();
721
+ assert_ne!(
722
+ remote_candidates.len(),
723
+ 1,
724
+ "non-binding message was able to create prflxRemote"
725
+ );
726
+ }
727
+
728
+ a.close()?;
729
+ }
730
+
731
+ //"Valid bind request"
732
+ {
733
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
734
+
735
+ {
736
+ let (username, local_pwd) = {
737
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
738
+ (
739
+ format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag),
740
+ ufrag_pwd.local_pwd.clone(),
741
+ )
742
+ };
743
+ a.internal.handle_inbound(
744
+ &mut build_msg(CLASS_REQUEST, username, local_pwd)?,
745
+ &local,
746
+ remote,
747
+ );
748
+ let remote_candidates = a.internal.remote_candidates.lock();
749
+ assert_eq!(
750
+ remote_candidates.len(),
751
+ 1,
752
+ "Binding with valid values was unable to create prflx candidate"
753
+ );
754
+ }
755
+
756
+ a.close()?;
757
+ }
758
+
759
+ //"Valid bind without fingerprint"
760
+ {
761
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
762
+
763
+ {
764
+ let (username, local_pwd) = {
765
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
766
+ (
767
+ format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag),
768
+ ufrag_pwd.local_pwd.clone(),
769
+ )
770
+ };
771
+
772
+ let mut msg = Message::new();
773
+ msg.build(&[
774
+ Box::new(BINDING_REQUEST),
775
+ Box::new(TransactionId::new()),
776
+ Box::new(Username::new(ATTR_USERNAME, username)),
777
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(local_pwd, test_crypto_provider())),
778
+ ])?;
779
+
780
+ a.internal.handle_inbound(&mut msg, &local, remote);
781
+ let remote_candidates = a.internal.remote_candidates.lock();
782
+ assert_eq!(
783
+ remote_candidates.len(),
784
+ 1,
785
+ "Binding with valid values (but no fingerprint) was unable to create prflx candidate"
786
+ );
787
+ }
788
+
789
+ a.close()?;
790
+ }
791
+
792
+ //"Success with invalid TransactionID"
793
+ {
794
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
795
+
796
+ {
797
+ let remote = SocketAddr::from_str("172.17.0.3:999")?;
798
+
799
+ let mut t_id = TransactionId::default();
800
+ t_id.0[..3].copy_from_slice(b"ABC");
801
+
802
+ let remote_pwd = {
803
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
804
+ ufrag_pwd.remote_pwd.clone()
805
+ };
806
+
807
+ let mut msg = Message::new();
808
+ msg.build(&[
809
+ Box::new(BINDING_SUCCESS),
810
+ Box::new(t_id),
811
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(remote_pwd, test_crypto_provider())),
812
+ Box::new(FINGERPRINT),
813
+ ])?;
814
+
815
+ a.internal.handle_inbound(&mut msg, &local, remote);
816
+
817
+ {
818
+ let remote_candidates = a.internal.remote_candidates.lock();
819
+ assert_eq!(
820
+ remote_candidates.len(),
821
+ 0,
822
+ "unknown remote was able to create a candidate"
823
+ );
824
+ }
825
+ }
826
+
827
+ a.close()?;
828
+ }
829
+
830
+ Ok(())
831
+ }
832
+
833
+ #[test]
834
+ fn test_invalid_agent_starts() -> Result<()> {
835
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
836
+
837
+ let (_cancel_tx1, cancel_rx1) = mpsc::channel(1);
838
+ let result = a.dial(cancel_rx1, "".to_owned(), "bar".to_owned());
839
+ assert!(result.is_err());
840
+ if let Err(err) = result {
841
+ assert_eq!(Error::ErrRemoteUfragEmpty, err);
842
+ }
843
+
844
+ let (_cancel_tx2, cancel_rx2) = mpsc::channel(1);
845
+ let result = a.dial(cancel_rx2, "foo".to_owned(), "".to_owned());
846
+ assert!(result.is_err());
847
+ if let Err(err) = result {
848
+ assert_eq!(Error::ErrRemotePwdEmpty, err);
849
+ }
850
+
851
+ let (cancel_tx3, cancel_rx3) = mpsc::channel(1);
852
+ tokio::spawn(async move {
853
+ tokio::time::sleep(Duration::from_millis(100));
854
+ drop(cancel_tx3);
855
+ });
856
+
857
+ let result = a.dial(cancel_rx3, "foo".to_owned(), "bar".to_owned());
858
+ assert!(result.is_err());
859
+ if let Err(err) = result {
860
+ assert_eq!(Error::ErrCanceledByCaller, err);
861
+ }
862
+
863
+ let (_cancel_tx4, cancel_rx4) = mpsc::channel(1);
864
+ let result = a.dial(cancel_rx4, "foo".to_owned(), "bar".to_owned());
865
+ assert!(result.is_err());
866
+ if let Err(err) = result {
867
+ assert_eq!(Error::ErrMultipleStart, err);
868
+ }
869
+
870
+ a.close()?;
871
+
872
+ Ok(())
873
+ }
874
+
875
+ //use std::io::Write;
876
+
877
+ // Assert that Agent emits Connecting/Connected/Disconnected/Failed/Closed messages
878
+ #[test]
879
+ fn test_connection_state_callback() -> Result<()> {
880
+ /*env_logger::Builder::new()
881
+ .format(|buf, record| {
882
+ writeln!(
883
+ buf,
884
+ "{}:{} [{}] {} - {}",
885
+ record.file().unwrap_or("unknown"),
886
+ record.line().unwrap_or(0),
887
+ record.level(),
888
+ chrono::Local::now().format("%H:%M:%S.%6f"),
889
+ record.args()
890
+ )
891
+ })
892
+ .filter(None, LevelFilter::Trace)
893
+ .init();*/
894
+
895
+ let disconnected_duration = Duration::from_secs(1);
896
+ let failed_duration = Duration::from_secs(1);
897
+ let keepalive_interval = Duration::from_secs(0);
898
+
899
+ let cfg0 = AgentConfig {
900
+ urls: vec![],
901
+ network_types: supported_network_types(),
902
+ disconnected_timeout: Some(disconnected_duration),
903
+ failed_timeout: Some(failed_duration),
904
+ keepalive_interval: Some(keepalive_interval),
905
+ ..Default::default()
906
+ };
907
+
908
+ let cfg1 = AgentConfig {
909
+ urls: vec![],
910
+ network_types: supported_network_types(),
911
+ disconnected_timeout: Some(disconnected_duration),
912
+ failed_timeout: Some(failed_duration),
913
+ keepalive_interval: Some(keepalive_interval),
914
+ ..Default::default()
915
+ };
916
+
917
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
918
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
919
+
920
+ let (is_checking_tx, mut is_checking_rx) = mpsc::channel::<()>(1);
921
+ let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1);
922
+ let (is_disconnected_tx, mut is_disconnected_rx) = mpsc::channel::<()>(1);
923
+ let (is_failed_tx, mut is_failed_rx) = mpsc::channel::<()>(1);
924
+ let (is_closed_tx, mut is_closed_rx) = mpsc::channel::<()>(1);
925
+
926
+ let is_checking_tx = Arc::new(Mutex::new(Some(is_checking_tx)));
927
+ let is_connected_tx = Arc::new(Mutex::new(Some(is_connected_tx)));
928
+ let is_disconnected_tx = Arc::new(Mutex::new(Some(is_disconnected_tx)));
929
+ let is_failed_tx = Arc::new(Mutex::new(Some(is_failed_tx)));
930
+ let is_closed_tx = Arc::new(Mutex::new(Some(is_closed_tx)));
931
+
932
+ a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| {
933
+ let is_checking_tx_clone = Arc::clone(&is_checking_tx);
934
+ let is_connected_tx_clone = Arc::clone(&is_connected_tx);
935
+ let is_disconnected_tx_clone = Arc::clone(&is_disconnected_tx);
936
+ let is_failed_tx_clone = Arc::clone(&is_failed_tx);
937
+ let is_closed_tx_clone = Arc::clone(&is_closed_tx);
938
+ Box::pin(async move {
939
+ match c {
940
+ ConnectionState::Checking => {
941
+ debug!("drop is_checking_tx");
942
+ let mut tx = is_checking_tx_clone.lock();
943
+ tx.take();
944
+ }
945
+ ConnectionState::Connected => {
946
+ debug!("drop is_connected_tx");
947
+ let mut tx = is_connected_tx_clone.lock();
948
+ tx.take();
949
+ }
950
+ ConnectionState::Disconnected => {
951
+ debug!("drop is_disconnected_tx");
952
+ let mut tx = is_disconnected_tx_clone.lock();
953
+ tx.take();
954
+ }
955
+ ConnectionState::Failed => {
956
+ debug!("drop is_failed_tx");
957
+ let mut tx = is_failed_tx_clone.lock();
958
+ tx.take();
959
+ }
960
+ ConnectionState::Closed => {
961
+ debug!("drop is_closed_tx");
962
+ let mut tx = is_closed_tx_clone.lock();
963
+ tx.take();
964
+ }
965
+ _ => {}
966
+ };
967
+ })
968
+ }));
969
+
970
+ connect_with_vnet(&a_agent, &b_agent)?;
971
+
972
+ debug!("wait is_checking_tx");
973
+ let _ = is_checking_rx.recv();
974
+ debug!("wait is_connected_rx");
975
+ let _ = is_connected_rx.recv();
976
+ debug!("wait is_disconnected_rx");
977
+ let _ = is_disconnected_rx.recv();
978
+ debug!("wait is_failed_rx");
979
+ let _ = is_failed_rx.recv();
980
+
981
+ a_agent.close()?;
982
+ b_agent.close()?;
983
+
984
+ debug!("wait is_closed_rx");
985
+ let _ = is_closed_rx.recv();
986
+
987
+ Ok(())
988
+ }
989
+
990
+ #[test]
991
+ fn test_invalid_gather() -> Result<()> {
992
+ //"Gather with no OnCandidate should error"
993
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
994
+
995
+ if let Err(err) = a.gather_candidates() {
996
+ assert_eq!(
997
+ Error::ErrNoOnCandidateHandler,
998
+ err,
999
+ "trickle GatherCandidates succeeded without OnCandidate"
1000
+ );
1001
+ }
1002
+
1003
+ a.close()?;
1004
+
1005
+ Ok(())
1006
+ }
1007
+
1008
+ #[test]
1009
+ fn test_candidate_pair_stats() -> Result<()> {
1010
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
1011
+
1012
+ let host_local: Arc<dyn Candidate + Send + Sync> = Arc::new(
1013
+ CandidateHostConfig {
1014
+ base_config: CandidateConfig {
1015
+ network: "udp".to_owned(),
1016
+ address: "192.168.1.1".to_owned(),
1017
+ port: 19216,
1018
+ component: 1,
1019
+ ..Default::default()
1020
+ },
1021
+ ..Default::default()
1022
+ }
1023
+ .new_candidate_host()?,
1024
+ );
1025
+
1026
+ let relay_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1027
+ CandidateRelayConfig {
1028
+ base_config: CandidateConfig {
1029
+ network: "udp".to_owned(),
1030
+ address: "1.2.3.4".to_owned(),
1031
+ port: 2340,
1032
+ component: 1,
1033
+ ..Default::default()
1034
+ },
1035
+ rel_addr: "4.3.2.1".to_owned(),
1036
+ rel_port: 43210,
1037
+ ..Default::default()
1038
+ }
1039
+ .new_candidate_relay()?,
1040
+ );
1041
+
1042
+ let srflx_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1043
+ CandidateServerReflexiveConfig {
1044
+ base_config: CandidateConfig {
1045
+ network: "udp".to_owned(),
1046
+ address: "10.10.10.2".to_owned(),
1047
+ port: 19218,
1048
+ component: 1,
1049
+ ..Default::default()
1050
+ },
1051
+ rel_addr: "4.3.2.1".to_owned(),
1052
+ rel_port: 43212,
1053
+ }
1054
+ .new_candidate_server_reflexive()?,
1055
+ );
1056
+
1057
+ let prflx_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1058
+ CandidatePeerReflexiveConfig {
1059
+ base_config: CandidateConfig {
1060
+ network: "udp".to_owned(),
1061
+ address: "10.10.10.2".to_owned(),
1062
+ port: 19217,
1063
+ component: 1,
1064
+ ..Default::default()
1065
+ },
1066
+ rel_addr: "4.3.2.1".to_owned(),
1067
+ rel_port: 43211,
1068
+ }
1069
+ .new_candidate_peer_reflexive()?,
1070
+ );
1071
+
1072
+ let host_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1073
+ CandidateHostConfig {
1074
+ base_config: CandidateConfig {
1075
+ network: "udp".to_owned(),
1076
+ address: "1.2.3.5".to_owned(),
1077
+ port: 12350,
1078
+ component: 1,
1079
+ ..Default::default()
1080
+ },
1081
+ ..Default::default()
1082
+ }
1083
+ .new_candidate_host()?,
1084
+ );
1085
+
1086
+ for remote in &[
1087
+ Arc::clone(&relay_remote),
1088
+ Arc::clone(&srflx_remote),
1089
+ Arc::clone(&prflx_remote),
1090
+ Arc::clone(&host_remote),
1091
+ ] {
1092
+ let p = a.internal.find_pair(&host_local, remote);
1093
+
1094
+ if p.is_none() {
1095
+ a.internal
1096
+ .add_pair(Arc::clone(&host_local), Arc::clone(remote));
1097
+ }
1098
+ }
1099
+
1100
+ {
1101
+ if let Some(p) = a.internal.find_pair(&host_local, &prflx_remote) {
1102
+ p.state
1103
+ .store(CandidatePairState::Failed as u8, Ordering::SeqCst);
1104
+ }
1105
+ }
1106
+
1107
+ let stats = a.get_candidate_pairs_stats();
1108
+ assert_eq!(stats.len(), 4, "expected 4 candidate pairs stats");
1109
+
1110
+ let (mut relay_pair_stat, mut srflx_pair_stat, mut prflx_pair_stat, mut host_pair_stat) = (
1111
+ CandidatePairStats::default(),
1112
+ CandidatePairStats::default(),
1113
+ CandidatePairStats::default(),
1114
+ CandidatePairStats::default(),
1115
+ );
1116
+
1117
+ for cps in stats {
1118
+ assert_eq!(
1119
+ cps.local_candidate_id,
1120
+ host_local.id(),
1121
+ "invalid local candidate id"
1122
+ );
1123
+
1124
+ if cps.remote_candidate_id == relay_remote.id() {
1125
+ relay_pair_stat = cps;
1126
+ } else if cps.remote_candidate_id == srflx_remote.id() {
1127
+ srflx_pair_stat = cps;
1128
+ } else if cps.remote_candidate_id == prflx_remote.id() {
1129
+ prflx_pair_stat = cps;
1130
+ } else if cps.remote_candidate_id == host_remote.id() {
1131
+ host_pair_stat = cps;
1132
+ } else {
1133
+ panic!("invalid remote candidate ID");
1134
+ }
1135
+ }
1136
+
1137
+ assert_eq!(
1138
+ relay_pair_stat.remote_candidate_id,
1139
+ relay_remote.id(),
1140
+ "missing host-relay pair stat"
1141
+ );
1142
+ assert_eq!(
1143
+ srflx_pair_stat.remote_candidate_id,
1144
+ srflx_remote.id(),
1145
+ "missing host-srflx pair stat"
1146
+ );
1147
+ assert_eq!(
1148
+ prflx_pair_stat.remote_candidate_id,
1149
+ prflx_remote.id(),
1150
+ "missing host-prflx pair stat"
1151
+ );
1152
+ assert_eq!(
1153
+ host_pair_stat.remote_candidate_id,
1154
+ host_remote.id(),
1155
+ "missing host-host pair stat"
1156
+ );
1157
+ assert_eq!(
1158
+ prflx_pair_stat.state,
1159
+ CandidatePairState::Failed,
1160
+ "expected host-prfflx pair to have state failed, it has state {} instead",
1161
+ prflx_pair_stat.state
1162
+ );
1163
+
1164
+ a.close()?;
1165
+
1166
+ Ok(())
1167
+ }
1168
+
1169
+ #[test]
1170
+ fn test_local_candidate_stats() -> Result<()> {
1171
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
1172
+
1173
+ let host_local: Arc<dyn Candidate + Send + Sync> = Arc::new(
1174
+ CandidateHostConfig {
1175
+ base_config: CandidateConfig {
1176
+ network: "udp".to_owned(),
1177
+ address: "192.168.1.1".to_owned(),
1178
+ port: 19216,
1179
+ component: 1,
1180
+ ..Default::default()
1181
+ },
1182
+ ..Default::default()
1183
+ }
1184
+ .new_candidate_host()?,
1185
+ );
1186
+
1187
+ let srflx_local: Arc<dyn Candidate + Send + Sync> = Arc::new(
1188
+ CandidateServerReflexiveConfig {
1189
+ base_config: CandidateConfig {
1190
+ network: "udp".to_owned(),
1191
+ address: "192.168.1.1".to_owned(),
1192
+ port: 19217,
1193
+ component: 1,
1194
+ ..Default::default()
1195
+ },
1196
+ rel_addr: "4.3.2.1".to_owned(),
1197
+ rel_port: 43212,
1198
+ }
1199
+ .new_candidate_server_reflexive()?,
1200
+ );
1201
+
1202
+ {
1203
+ let mut local_candidates = a.internal.local_candidates.lock();
1204
+ local_candidates.insert(
1205
+ NetworkType::Udp4,
1206
+ vec![Arc::clone(&host_local), Arc::clone(&srflx_local)],
1207
+ );
1208
+ }
1209
+
1210
+ let local_stats = a.get_local_candidates_stats();
1211
+ assert_eq!(
1212
+ local_stats.len(),
1213
+ 2,
1214
+ "expected 2 local candidates stats, got {} instead",
1215
+ local_stats.len()
1216
+ );
1217
+
1218
+ let (mut host_local_stat, mut srflx_local_stat) =
1219
+ (CandidateStats::default(), CandidateStats::default());
1220
+ for stats in local_stats {
1221
+ let candidate = if stats.id == host_local.id() {
1222
+ host_local_stat = stats.clone();
1223
+ Arc::clone(&host_local)
1224
+ } else if stats.id == srflx_local.id() {
1225
+ srflx_local_stat = stats.clone();
1226
+ Arc::clone(&srflx_local)
1227
+ } else {
1228
+ panic!("invalid local candidate ID");
1229
+ };
1230
+
1231
+ assert_eq!(
1232
+ stats.candidate_type,
1233
+ candidate.candidate_type(),
1234
+ "invalid stats CandidateType"
1235
+ );
1236
+ assert_eq!(
1237
+ stats.priority,
1238
+ candidate.priority(),
1239
+ "invalid stats CandidateType"
1240
+ );
1241
+ assert_eq!(stats.ip, candidate.address(), "invalid stats IP");
1242
+ }
1243
+
1244
+ assert_eq!(
1245
+ host_local_stat.id,
1246
+ host_local.id(),
1247
+ "missing host local stat"
1248
+ );
1249
+ assert_eq!(
1250
+ srflx_local_stat.id,
1251
+ srflx_local.id(),
1252
+ "missing srflx local stat"
1253
+ );
1254
+
1255
+ a.close()?;
1256
+
1257
+ Ok(())
1258
+ }
1259
+
1260
+ #[test]
1261
+ fn test_remote_candidate_stats() -> Result<()> {
1262
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
1263
+
1264
+ let relay_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1265
+ CandidateRelayConfig {
1266
+ base_config: CandidateConfig {
1267
+ network: "udp".to_owned(),
1268
+ address: "1.2.3.4".to_owned(),
1269
+ port: 12340,
1270
+ component: 1,
1271
+ ..Default::default()
1272
+ },
1273
+ rel_addr: "4.3.2.1".to_owned(),
1274
+ rel_port: 43210,
1275
+ ..Default::default()
1276
+ }
1277
+ .new_candidate_relay()?,
1278
+ );
1279
+
1280
+ let srflx_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1281
+ CandidateServerReflexiveConfig {
1282
+ base_config: CandidateConfig {
1283
+ network: "udp".to_owned(),
1284
+ address: "10.10.10.2".to_owned(),
1285
+ port: 19218,
1286
+ component: 1,
1287
+ ..Default::default()
1288
+ },
1289
+ rel_addr: "4.3.2.1".to_owned(),
1290
+ rel_port: 43212,
1291
+ }
1292
+ .new_candidate_server_reflexive()?,
1293
+ );
1294
+
1295
+ let prflx_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1296
+ CandidatePeerReflexiveConfig {
1297
+ base_config: CandidateConfig {
1298
+ network: "udp".to_owned(),
1299
+ address: "10.10.10.2".to_owned(),
1300
+ port: 19217,
1301
+ component: 1,
1302
+ ..Default::default()
1303
+ },
1304
+ rel_addr: "4.3.2.1".to_owned(),
1305
+ rel_port: 43211,
1306
+ }
1307
+ .new_candidate_peer_reflexive()?,
1308
+ );
1309
+
1310
+ let host_remote: Arc<dyn Candidate + Send + Sync> = Arc::new(
1311
+ CandidateHostConfig {
1312
+ base_config: CandidateConfig {
1313
+ network: "udp".to_owned(),
1314
+ address: "1.2.3.5".to_owned(),
1315
+ port: 12350,
1316
+ component: 1,
1317
+ ..Default::default()
1318
+ },
1319
+ ..Default::default()
1320
+ }
1321
+ .new_candidate_host()?,
1322
+ );
1323
+
1324
+ {
1325
+ let mut remote_candidates = a.internal.remote_candidates.lock();
1326
+ remote_candidates.insert(
1327
+ NetworkType::Udp4,
1328
+ vec![
1329
+ Arc::clone(&relay_remote),
1330
+ Arc::clone(&srflx_remote),
1331
+ Arc::clone(&prflx_remote),
1332
+ Arc::clone(&host_remote),
1333
+ ],
1334
+ );
1335
+ }
1336
+
1337
+ let remote_stats = a.get_remote_candidates_stats();
1338
+ assert_eq!(
1339
+ remote_stats.len(),
1340
+ 4,
1341
+ "expected 4 remote candidates stats, got {} instead",
1342
+ remote_stats.len()
1343
+ );
1344
+
1345
+ let (mut relay_remote_stat, mut srflx_remote_stat, mut prflx_remote_stat, mut host_remote_stat) = (
1346
+ CandidateStats::default(),
1347
+ CandidateStats::default(),
1348
+ CandidateStats::default(),
1349
+ CandidateStats::default(),
1350
+ );
1351
+ for stats in remote_stats {
1352
+ let candidate = if stats.id == relay_remote.id() {
1353
+ relay_remote_stat = stats.clone();
1354
+ Arc::clone(&relay_remote)
1355
+ } else if stats.id == srflx_remote.id() {
1356
+ srflx_remote_stat = stats.clone();
1357
+ Arc::clone(&srflx_remote)
1358
+ } else if stats.id == prflx_remote.id() {
1359
+ prflx_remote_stat = stats.clone();
1360
+ Arc::clone(&prflx_remote)
1361
+ } else if stats.id == host_remote.id() {
1362
+ host_remote_stat = stats.clone();
1363
+ Arc::clone(&host_remote)
1364
+ } else {
1365
+ panic!("invalid remote candidate ID");
1366
+ };
1367
+
1368
+ assert_eq!(
1369
+ stats.candidate_type,
1370
+ candidate.candidate_type(),
1371
+ "invalid stats CandidateType"
1372
+ );
1373
+ assert_eq!(
1374
+ stats.priority,
1375
+ candidate.priority(),
1376
+ "invalid stats CandidateType"
1377
+ );
1378
+ assert_eq!(stats.ip, candidate.address(), "invalid stats IP");
1379
+ }
1380
+
1381
+ assert_eq!(
1382
+ relay_remote_stat.id,
1383
+ relay_remote.id(),
1384
+ "missing relay remote stat"
1385
+ );
1386
+ assert_eq!(
1387
+ srflx_remote_stat.id,
1388
+ srflx_remote.id(),
1389
+ "missing srflx remote stat"
1390
+ );
1391
+ assert_eq!(
1392
+ prflx_remote_stat.id,
1393
+ prflx_remote.id(),
1394
+ "missing prflx remote stat"
1395
+ );
1396
+ assert_eq!(
1397
+ host_remote_stat.id,
1398
+ host_remote.id(),
1399
+ "missing host remote stat"
1400
+ );
1401
+
1402
+ a.close()?;
1403
+
1404
+ Ok(())
1405
+ }
1406
+
1407
+ #[test]
1408
+ fn test_binding_request_timeout() -> Result<()> {
1409
+ const EXPECTED_REMOVAL_COUNT: usize = 2;
1410
+
1411
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
1412
+
1413
+ let now = Instant::now();
1414
+ {
1415
+ {
1416
+ let mut pending_binding_requests = a.internal.pending_binding_requests.lock();
1417
+ pending_binding_requests.push(BindingRequest {
1418
+ timestamp: now, // valid
1419
+ ..Default::default()
1420
+ });
1421
+ pending_binding_requests.push(BindingRequest {
1422
+ timestamp: now.sub(Duration::from_millis(3900)), // valid
1423
+ ..Default::default()
1424
+ });
1425
+ pending_binding_requests.push(BindingRequest {
1426
+ timestamp: now.sub(Duration::from_millis(4100)), // invalid
1427
+ ..Default::default()
1428
+ });
1429
+ pending_binding_requests.push(BindingRequest {
1430
+ timestamp: now.sub(Duration::from_secs(75)), // invalid
1431
+ ..Default::default()
1432
+ });
1433
+ }
1434
+
1435
+ a.internal.invalidate_pending_binding_requests(now);
1436
+ {
1437
+ let pending_binding_requests = a.internal.pending_binding_requests.lock();
1438
+ assert_eq!(pending_binding_requests.len(), EXPECTED_REMOVAL_COUNT, "Binding invalidation due to timeout did not remove the correct number of binding requests")
1439
+ }
1440
+ }
1441
+
1442
+ a.close()?;
1443
+
1444
+ Ok(())
1445
+ }
1446
+
1447
+ // test_agent_credentials checks if local username fragments and passwords (if set) meet RFC standard
1448
+ // and ensure it's backwards compatible with previous versions of the rtc/ice
1449
+ #[test]
1450
+ fn test_agent_credentials() -> Result<()> {
1451
+ // Agent should not require any of the usernames and password to be set
1452
+ // If set, they should follow the default 16/128 bits random number generator strategy
1453
+
1454
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
1455
+ {
1456
+ let ufrag_pwd = a.internal.ufrag_pwd.lock();
1457
+ assert!(ufrag_pwd.local_ufrag.as_bytes().len() * 8 >= 24);
1458
+ assert!(ufrag_pwd.local_pwd.as_bytes().len() * 8 >= 128);
1459
+ }
1460
+ a.close()?;
1461
+
1462
+ // Should honor RFC standards
1463
+ // Local values MUST be unguessable, with at least 128 bits of
1464
+ // random number generator output used to generate the password, and
1465
+ // at least 24 bits of output to generate the username fragment.
1466
+
1467
+ if let Err(err) = Agent::new(Instant::now(), AgentConfig {
1468
+ local_ufrag: "xx".to_owned(),
1469
+ ..Default::default()
1470
+ }) {
1471
+ assert_eq!(Error::ErrLocalUfragInsufficientBits, err);
1472
+ } else {
1473
+ panic!("expected error, but got ok");
1474
+ }
1475
+
1476
+ if let Err(err) = Agent::new(Instant::now(), AgentConfig {
1477
+ local_pwd: "xxxxxx".to_owned(),
1478
+ ..Default::default()
1479
+ }) {
1480
+ assert_eq!(Error::ErrLocalPwdInsufficientBits, err);
1481
+ } else {
1482
+ panic!("expected error, but got ok");
1483
+ }
1484
+
1485
+ Ok(())
1486
+ }
1487
+
1488
+ // Assert that Agent on Failure deletes all existing candidates
1489
+ // User can then do an ICE Restart to bring agent back
1490
+ #[test]
1491
+ fn test_connection_state_failed_delete_all_candidates() -> Result<()> {
1492
+ let one_second = Duration::from_secs(1);
1493
+ let keepalive_interval = Duration::from_secs(0);
1494
+
1495
+ let cfg0 = AgentConfig {
1496
+ network_types: supported_network_types(),
1497
+ disconnected_timeout: Some(one_second),
1498
+ failed_timeout: Some(one_second),
1499
+ keepalive_interval: Some(keepalive_interval),
1500
+ ..Default::default()
1501
+ };
1502
+ let cfg1 = AgentConfig {
1503
+ network_types: supported_network_types(),
1504
+ disconnected_timeout: Some(one_second),
1505
+ failed_timeout: Some(one_second),
1506
+ keepalive_interval: Some(keepalive_interval),
1507
+ ..Default::default()
1508
+ };
1509
+
1510
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
1511
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
1512
+
1513
+ let (is_failed_tx, mut is_failed_rx) = mpsc::channel::<()>(1);
1514
+ let is_failed_tx = Arc::new(Mutex::new(Some(is_failed_tx)));
1515
+ a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| {
1516
+ let is_failed_tx_clone = Arc::clone(&is_failed_tx);
1517
+ Box::pin(async move {
1518
+ if c == ConnectionState::Failed {
1519
+ let mut tx = is_failed_tx_clone.lock();
1520
+ tx.take();
1521
+ }
1522
+ })
1523
+ }));
1524
+
1525
+ connect_with_vnet(&a_agent, &b_agent)?;
1526
+ let _ = is_failed_rx.recv();
1527
+
1528
+ {
1529
+ {
1530
+ let remote_candidates = a_agent.internal.remote_candidates.lock();
1531
+ assert_eq!(remote_candidates.len(), 0);
1532
+ }
1533
+ {
1534
+ let local_candidates = a_agent.internal.local_candidates.lock();
1535
+ assert_eq!(local_candidates.len(), 0);
1536
+ }
1537
+ }
1538
+
1539
+ a_agent.close()?;
1540
+ b_agent.close()?;
1541
+
1542
+ Ok(())
1543
+ }
1544
+
1545
+ // Assert that the ICE Agent can go directly from Connecting -> Failed on both sides
1546
+ #[test]
1547
+ fn test_connection_state_connecting_to_failed() -> Result<()> {
1548
+ let one_second = Duration::from_secs(1);
1549
+ let keepalive_interval = Duration::from_secs(0);
1550
+
1551
+ let cfg0 = AgentConfig {
1552
+ disconnected_timeout: Some(one_second),
1553
+ failed_timeout: Some(one_second),
1554
+ keepalive_interval: Some(keepalive_interval),
1555
+ ..Default::default()
1556
+ };
1557
+ let cfg1 = AgentConfig {
1558
+ disconnected_timeout: Some(one_second),
1559
+ failed_timeout: Some(one_second),
1560
+ keepalive_interval: Some(keepalive_interval),
1561
+ ..Default::default()
1562
+ };
1563
+
1564
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
1565
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
1566
+
1567
+ let is_failed = WaitGroup::new();
1568
+ let is_checking = WaitGroup::new();
1569
+
1570
+ let connection_state_check = move |wf: Worker, wc: Worker| {
1571
+ let wf = Arc::new(Mutex::new(Some(wf)));
1572
+ let wc = Arc::new(Mutex::new(Some(wc)));
1573
+ let hdlr_fn: OnConnectionStateChangeHdlrFn = Box::new(move |c: ConnectionState| {
1574
+ let wf_clone = Arc::clone(&wf);
1575
+ let wc_clone = Arc::clone(&wc);
1576
+ Box::pin(async move {
1577
+ if c == ConnectionState::Failed {
1578
+ let mut f = wf_clone.lock();
1579
+ f.take();
1580
+ } else if c == ConnectionState::Checking {
1581
+ let mut c = wc_clone.lock();
1582
+ c.take();
1583
+ } else if c == ConnectionState::Connected || c == ConnectionState::Completed {
1584
+ panic!("Unexpected ConnectionState: {c}");
1585
+ }
1586
+ })
1587
+ });
1588
+ hdlr_fn
1589
+ };
1590
+
1591
+ let (wf1, wc1) = (is_failed.worker(), is_checking.worker());
1592
+ a_agent.on_connection_state_change(connection_state_check(wf1, wc1));
1593
+
1594
+ let (wf2, wc2) = (is_failed.worker(), is_checking.worker());
1595
+ b_agent.on_connection_state_change(connection_state_check(wf2, wc2));
1596
+
1597
+ let agent_a = Arc::clone(&a_agent);
1598
+ tokio::spawn(async move {
1599
+ let (_cancel_tx, cancel_rx) = mpsc::channel(1);
1600
+ let result = agent_a.accept(cancel_rx, "InvalidFrag".to_owned(), "InvalidPwd".to_owned());
1601
+ assert!(result.is_err());
1602
+ });
1603
+
1604
+ let agent_b = Arc::clone(&b_agent);
1605
+ tokio::spawn(async move {
1606
+ let (_cancel_tx, cancel_rx) = mpsc::channel(1);
1607
+ let result = agent_b.dial(cancel_rx, "InvalidFrag".to_owned(), "InvalidPwd".to_owned());
1608
+ assert!(result.is_err());
1609
+ });
1610
+
1611
+ is_checking.wait();
1612
+ is_failed.wait();
1613
+
1614
+ a_agent.close()?;
1615
+ b_agent.close()?;
1616
+
1617
+ Ok(())
1618
+ }
1619
+
1620
+ #[test]
1621
+ fn test_agent_restart_during_gather() -> Result<()> {
1622
+ //"Restart During Gather"
1623
+
1624
+ let agent = Agent::new(Instant::now(), AgentConfig::default())?;
1625
+
1626
+ agent
1627
+ .gathering_state
1628
+ .store(GatheringState::Gathering as u8, Ordering::SeqCst);
1629
+
1630
+ if let Err(err) = agent.restart(Instant::now(), "".to_owned(), "".to_owned()) {
1631
+ assert_eq!(Error::ErrRestartWhenGathering, err);
1632
+ } else {
1633
+ panic!("expected error, but got ok");
1634
+ }
1635
+
1636
+ agent.close()?;
1637
+
1638
+ Ok(())
1639
+ }
1640
+
1641
+ #[test]
1642
+ fn test_agent_restart_when_closed() -> Result<()> {
1643
+ //"Restart When Closed"
1644
+
1645
+ let agent = Agent::new(Instant::now(), AgentConfig::default())?;
1646
+ agent.close()?;
1647
+
1648
+ if let Err(err) = agent.restart(Instant::now(), "".to_owned(), "".to_owned()) {
1649
+ assert_eq!(Error::ErrClosed, err);
1650
+ } else {
1651
+ panic!("expected error, but got ok");
1652
+ }
1653
+
1654
+ Ok(())
1655
+ }
1656
+
1657
+ #[test]
1658
+ fn test_agent_restart_one_side() -> Result<()> {
1659
+ let one_second = Duration::from_secs(1);
1660
+
1661
+ //"Restart One Side"
1662
+ let (_, _, agent_a, agent_b) = pipe(
1663
+ Some(AgentConfig {
1664
+ disconnected_timeout: Some(one_second),
1665
+ failed_timeout: Some(one_second),
1666
+ ..Default::default()
1667
+ }),
1668
+ Some(AgentConfig {
1669
+ disconnected_timeout: Some(one_second),
1670
+ failed_timeout: Some(one_second),
1671
+ ..Default::default()
1672
+ }),
1673
+ )?;
1674
+
1675
+ let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
1676
+ let cancel_tx = Arc::new(Mutex::new(Some(cancel_tx)));
1677
+ agent_b.on_connection_state_change(Box::new(move |c: ConnectionState| {
1678
+ let cancel_tx_clone = Arc::clone(&cancel_tx);
1679
+ Box::pin(async move {
1680
+ if c == ConnectionState::Failed || c == ConnectionState::Disconnected {
1681
+ let mut tx = cancel_tx_clone.lock();
1682
+ tx.take();
1683
+ }
1684
+ })
1685
+ }));
1686
+
1687
+ agent_a.restart(Instant::now(), "".to_owned(), "".to_owned())?;
1688
+
1689
+ let _ = cancel_rx.recv();
1690
+
1691
+ agent_a.close()?;
1692
+ agent_b.close()?;
1693
+
1694
+ Ok(())
1695
+ }
1696
+
1697
+ #[test]
1698
+ fn test_agent_restart_both_side() -> Result<()> {
1699
+ let one_second = Duration::from_secs(1);
1700
+ //"Restart Both Sides"
1701
+
1702
+ // Get all addresses of candidates concatenated
1703
+ let generate_candidate_address_strings =
1704
+ |res: Result<Vec<Arc<dyn Candidate + Send + Sync>>>| -> String {
1705
+ assert!(res.is_ok());
1706
+
1707
+ let mut out = String::new();
1708
+ if let Ok(candidates) = res {
1709
+ for c in candidates {
1710
+ out += c.address().as_str();
1711
+ out += ":";
1712
+ out += c.port().to_string().as_str();
1713
+ }
1714
+ }
1715
+ out
1716
+ };
1717
+
1718
+ // Store the original candidates, confirm that after we reconnect we have new pairs
1719
+ let (_, _, agent_a, agent_b) = pipe(
1720
+ Some(AgentConfig {
1721
+ disconnected_timeout: Some(one_second),
1722
+ failed_timeout: Some(one_second),
1723
+ ..Default::default()
1724
+ }),
1725
+ Some(AgentConfig {
1726
+ disconnected_timeout: Some(one_second),
1727
+ failed_timeout: Some(one_second),
1728
+ ..Default::default()
1729
+ }),
1730
+ )?;
1731
+
1732
+ let conn_afirst_candidates = generate_candidate_address_strings(agent_a.get_local_candidates());
1733
+ let conn_bfirst_candidates = generate_candidate_address_strings(agent_b.get_local_candidates());
1734
+
1735
+ let (a_notifier, mut a_connected) = on_connected();
1736
+ agent_a.on_connection_state_change(a_notifier);
1737
+
1738
+ let (b_notifier, mut b_connected) = on_connected();
1739
+ agent_b.on_connection_state_change(b_notifier);
1740
+
1741
+ // Restart and Re-Signal
1742
+ agent_a.restart(Instant::now(), "".to_owned(), "".to_owned())?;
1743
+ agent_b.restart(Instant::now(), "".to_owned(), "".to_owned())?;
1744
+
1745
+ // Exchange Candidates and Credentials
1746
+ let (ufrag, pwd) = agent_b.get_local_user_credentials();
1747
+ agent_a.set_remote_credentials(ufrag, pwd)?;
1748
+
1749
+ let (ufrag, pwd) = agent_a.get_local_user_credentials();
1750
+ agent_b.set_remote_credentials(ufrag, pwd)?;
1751
+
1752
+ gather_and_exchange_candidates(&agent_a, &agent_b)?;
1753
+
1754
+ // Wait until both have gone back to connected
1755
+ let _ = a_connected.recv();
1756
+ let _ = b_connected.recv();
1757
+
1758
+ // Assert that we have new candiates each time
1759
+ assert_ne!(
1760
+ conn_afirst_candidates,
1761
+ generate_candidate_address_strings(agent_a.get_local_candidates())
1762
+ );
1763
+ assert_ne!(
1764
+ conn_bfirst_candidates,
1765
+ generate_candidate_address_strings(agent_b.get_local_candidates())
1766
+ );
1767
+
1768
+ agent_a.close()?;
1769
+ agent_b.close()?;
1770
+
1771
+ Ok(())
1772
+ }
1773
+
1774
+ #[test]
1775
+ fn test_get_remote_credentials() -> Result<()> {
1776
+ let a = Agent::new(Instant::now(), AgentConfig::default())?;
1777
+
1778
+ let (remote_ufrag, remote_pwd) = {
1779
+ let mut ufrag_pwd = a.internal.ufrag_pwd.lock();
1780
+ ufrag_pwd.remote_ufrag = "remoteUfrag".to_owned();
1781
+ ufrag_pwd.remote_pwd = "remotePwd".to_owned();
1782
+ (
1783
+ ufrag_pwd.remote_ufrag.to_owned(),
1784
+ ufrag_pwd.remote_pwd.to_owned(),
1785
+ )
1786
+ };
1787
+
1788
+ let (actual_ufrag, actual_pwd) = a.get_remote_user_credentials();
1789
+
1790
+ assert_eq!(actual_ufrag, remote_ufrag);
1791
+ assert_eq!(actual_pwd, remote_pwd);
1792
+
1793
+ a.close()?;
1794
+
1795
+ Ok(())
1796
+ }
1797
+
1798
+ #[test]
1799
+ fn test_close_in_connection_state_callback() -> Result<()> {
1800
+ let disconnected_duration = Duration::from_secs(1);
1801
+ let failed_duration = Duration::from_secs(1);
1802
+ let keepalive_interval = Duration::from_secs(0);
1803
+
1804
+ let cfg0 = AgentConfig {
1805
+ urls: vec![],
1806
+ network_types: supported_network_types(),
1807
+ disconnected_timeout: Some(disconnected_duration),
1808
+ failed_timeout: Some(failed_duration),
1809
+ keepalive_interval: Some(keepalive_interval),
1810
+ check_interval: Duration::from_millis(500),
1811
+ ..Default::default()
1812
+ };
1813
+
1814
+ let cfg1 = AgentConfig {
1815
+ urls: vec![],
1816
+ network_types: supported_network_types(),
1817
+ disconnected_timeout: Some(disconnected_duration),
1818
+ failed_timeout: Some(failed_duration),
1819
+ keepalive_interval: Some(keepalive_interval),
1820
+ check_interval: Duration::from_millis(500),
1821
+ ..Default::default()
1822
+ };
1823
+
1824
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
1825
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
1826
+
1827
+ let (is_closed_tx, mut is_closed_rx) = mpsc::channel::<()>(1);
1828
+ let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1);
1829
+ let is_closed_tx = Arc::new(Mutex::new(Some(is_closed_tx)));
1830
+ let is_connected_tx = Arc::new(Mutex::new(Some(is_connected_tx)));
1831
+ a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| {
1832
+ let is_closed_tx_clone = Arc::clone(&is_closed_tx);
1833
+ let is_connected_tx_clone = Arc::clone(&is_connected_tx);
1834
+ Box::pin(async move {
1835
+ if c == ConnectionState::Connected {
1836
+ let mut tx = is_connected_tx_clone.lock();
1837
+ tx.take();
1838
+ } else if c == ConnectionState::Closed {
1839
+ let mut tx = is_closed_tx_clone.lock();
1840
+ tx.take();
1841
+ }
1842
+ })
1843
+ }));
1844
+
1845
+ connect_with_vnet(&a_agent, &b_agent)?;
1846
+
1847
+ let _ = is_connected_rx.recv();
1848
+ a_agent.close()?;
1849
+
1850
+ let _ = is_closed_rx.recv();
1851
+ b_agent.close()?;
1852
+
1853
+ Ok(())
1854
+ }
1855
+
1856
+ #[test]
1857
+ fn test_run_task_in_connection_state_callback() -> Result<()> {
1858
+ let one_second = Duration::from_secs(1);
1859
+ let keepalive_interval = Duration::from_secs(0);
1860
+
1861
+ let cfg0 = AgentConfig {
1862
+ urls: vec![],
1863
+ network_types: supported_network_types(),
1864
+ disconnected_timeout: Some(one_second),
1865
+ failed_timeout: Some(one_second),
1866
+ keepalive_interval: Some(keepalive_interval),
1867
+ check_interval: Duration::from_millis(50),
1868
+ ..Default::default()
1869
+ };
1870
+
1871
+ let cfg1 = AgentConfig {
1872
+ urls: vec![],
1873
+ network_types: supported_network_types(),
1874
+ disconnected_timeout: Some(one_second),
1875
+ failed_timeout: Some(one_second),
1876
+ keepalive_interval: Some(keepalive_interval),
1877
+ check_interval: Duration::from_millis(50),
1878
+ ..Default::default()
1879
+ };
1880
+
1881
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
1882
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
1883
+
1884
+ let (is_complete_tx, mut is_complete_rx) = mpsc::channel::<()>(1);
1885
+ let is_complete_tx = Arc::new(Mutex::new(Some(is_complete_tx)));
1886
+ a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| {
1887
+ let is_complete_tx_clone = Arc::clone(&is_complete_tx);
1888
+ Box::pin(async move {
1889
+ if c == ConnectionState::Connected {
1890
+ let mut tx = is_complete_tx_clone.lock();
1891
+ tx.take();
1892
+ }
1893
+ })
1894
+ }));
1895
+
1896
+ connect_with_vnet(&a_agent, &b_agent)?;
1897
+
1898
+ let _ = is_complete_rx.recv();
1899
+ let _ = a_agent.get_local_user_credentials();
1900
+ a_agent.restart(Instant::now(), "".to_owned(), "".to_owned())?;
1901
+
1902
+ a_agent.close()?;
1903
+ b_agent.close()?;
1904
+
1905
+ Ok(())
1906
+ }
1907
+
1908
+ #[test]
1909
+ fn test_run_task_in_selected_candidate_pair_change_callback() -> Result<()> {
1910
+ let one_second = Duration::from_secs(1);
1911
+ let keepalive_interval = Duration::from_secs(0);
1912
+
1913
+ let cfg0 = AgentConfig {
1914
+ urls: vec![],
1915
+ network_types: supported_network_types(),
1916
+ disconnected_timeout: Some(one_second),
1917
+ failed_timeout: Some(one_second),
1918
+ keepalive_interval: Some(keepalive_interval),
1919
+ check_interval: Duration::from_millis(50),
1920
+ ..Default::default()
1921
+ };
1922
+
1923
+ let cfg1 = AgentConfig {
1924
+ urls: vec![],
1925
+ network_types: supported_network_types(),
1926
+ disconnected_timeout: Some(one_second),
1927
+ failed_timeout: Some(one_second),
1928
+ keepalive_interval: Some(keepalive_interval),
1929
+ check_interval: Duration::from_millis(50),
1930
+ ..Default::default()
1931
+ };
1932
+
1933
+ let a_agent = Arc::new(Agent::new(Instant::now(), cfg0, test_crypto_provider())?);
1934
+ let b_agent = Arc::new(Agent::new(Instant::now(), cfg1, test_crypto_provider())?);
1935
+
1936
+ let (is_tested_tx, mut is_tested_rx) = mpsc::channel::<()>(1);
1937
+ let is_tested_tx = Arc::new(Mutex::new(Some(is_tested_tx)));
1938
+ a_agent.on_selected_candidate_pair_change(Box::new(
1939
+ move |_: &Arc<dyn Candidate + Send + Sync>, _: &Arc<dyn Candidate + Send + Sync>| {
1940
+ let is_tested_tx_clone = Arc::clone(&is_tested_tx);
1941
+ Box::pin(async move {
1942
+ let mut tx = is_tested_tx_clone.lock();
1943
+ tx.take();
1944
+ })
1945
+ },
1946
+ ));
1947
+
1948
+ let (is_complete_tx, mut is_complete_rx) = mpsc::channel::<()>(1);
1949
+ let is_complete_tx = Arc::new(Mutex::new(Some(is_complete_tx)));
1950
+ a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| {
1951
+ let is_complete_tx_clone = Arc::clone(&is_complete_tx);
1952
+ Box::pin(async move {
1953
+ if c == ConnectionState::Connected {
1954
+ let mut tx = is_complete_tx_clone.lock();
1955
+ tx.take();
1956
+ }
1957
+ })
1958
+ }));
1959
+
1960
+ connect_with_vnet(&a_agent, &b_agent)?;
1961
+
1962
+ let _ = is_complete_rx.recv();
1963
+ let _ = is_tested_rx.recv();
1964
+
1965
+ let _ = a_agent.get_local_user_credentials();
1966
+
1967
+ a_agent.close()?;
1968
+ b_agent.close()?;
1969
+
1970
+ Ok(())
1971
+ }
1972
+
1973
+ // Assert that a Lite agent goes to disconnected and failed
1974
+ #[test]
1975
+ fn test_lite_lifecycle() -> Result<()> {
1976
+ let (a_notifier, mut a_connected_rx) = on_connected();
1977
+
1978
+ let a_agent = Arc::new(Agent::new(Instant::now(), AgentConfig {
1979
+ network_types: supported_network_types(),
1980
+ ..Default::default()
1981
+ })?);
1982
+
1983
+ a_agent.on_connection_state_change(a_notifier);
1984
+
1985
+ let disconnected_duration = Duration::from_secs(1);
1986
+ let failed_duration = Duration::from_secs(1);
1987
+ let keepalive_interval = Duration::from_secs(0);
1988
+
1989
+ let b_agent = Arc::new(Agent::new(Instant::now(), AgentConfig {
1990
+ lite: true,
1991
+ candidate_types: vec![CandidateType::Host],
1992
+ network_types: supported_network_types(),
1993
+ disconnected_timeout: Some(disconnected_duration),
1994
+ failed_timeout: Some(failed_duration),
1995
+ keepalive_interval: Some(keepalive_interval),
1996
+ check_interval: Duration::from_millis(500),
1997
+ ..Default::default()
1998
+ })?);
1999
+
2000
+ let (b_connected_tx, mut b_connected_rx) = mpsc::channel::<()>(1);
2001
+ let (b_disconnected_tx, mut b_disconnected_rx) = mpsc::channel::<()>(1);
2002
+ let (b_failed_tx, mut b_failed_rx) = mpsc::channel::<()>(1);
2003
+ let b_connected_tx = Arc::new(Mutex::new(Some(b_connected_tx)));
2004
+ let b_disconnected_tx = Arc::new(Mutex::new(Some(b_disconnected_tx)));
2005
+ let b_failed_tx = Arc::new(Mutex::new(Some(b_failed_tx)));
2006
+
2007
+ b_agent.on_connection_state_change(Box::new(move |c: ConnectionState| {
2008
+ let b_connected_tx_clone = Arc::clone(&b_connected_tx);
2009
+ let b_disconnected_tx_clone = Arc::clone(&b_disconnected_tx);
2010
+ let b_failed_tx_clone = Arc::clone(&b_failed_tx);
2011
+
2012
+ Box::pin(async move {
2013
+ if c == ConnectionState::Connected {
2014
+ let mut tx = b_connected_tx_clone.lock();
2015
+ tx.take();
2016
+ } else if c == ConnectionState::Disconnected {
2017
+ let mut tx = b_disconnected_tx_clone.lock();
2018
+ tx.take();
2019
+ } else if c == ConnectionState::Failed {
2020
+ let mut tx = b_failed_tx_clone.lock();
2021
+ tx.take();
2022
+ }
2023
+ })
2024
+ }));
2025
+
2026
+ connect_with_vnet(&b_agent, &a_agent)?;
2027
+
2028
+ let _ = a_connected_rx.recv();
2029
+ let _ = b_connected_rx.recv();
2030
+ a_agent.close()?;
2031
+
2032
+ let _ = b_disconnected_rx.recv();
2033
+ let _ = b_failed_rx.recv();
2034
+
2035
+ b_agent.close()?;
2036
+
2037
+ Ok(())
2038
+ }
2039
+ */
2040
+
2041
+ #[test]
2042
+ fn test_zero_failed_timeout_keeps_checking_agent_recoverable() -> Result<()> {
2043
+ let base = Instant::now();
2044
+ let mut agent = Agent::new(
2045
+ base,
2046
+ Arc::new(AgentConfig {
2047
+ disconnected_timeout: Some(Duration::from_secs(5)),
2048
+ failed_timeout: Some(Duration::ZERO),
2049
+ ..Default::default()
2050
+ }),
2051
+ test_crypto_provider(),
2052
+ )?;
2053
+
2054
+ agent.connection_state = ConnectionState::Checking;
2055
+ agent.last_connection_state = ConnectionState::Checking;
2056
+ agent.checking_duration = base;
2057
+ agent.contact(base + Duration::from_secs(3600));
2058
+
2059
+ assert_eq!(
2060
+ agent.connection_state,
2061
+ ConnectionState::Checking,
2062
+ "a zero failed timeout is documented to disable the terminal transition"
2063
+ );
2064
+ Ok(())
2065
+ }
2066
+
2067
+ /// Test role conflict when both agents are controlling
2068
+ /// RFC 8445 Section 7.3.1.1 - Agent with smaller tiebreaker should switch to controlled
2069
+ #[test]
2070
+ fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<()> {
2071
+ // Create agent with controlling role
2072
+ let mut config = AgentConfig::default();
2073
+ config.is_controlling = true;
2074
+ let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
2075
+
2076
+ // Set a specific tiebreaker value
2077
+ agent.tie_breaker = 100;
2078
+
2079
+ // Add local candidate
2080
+ let host_config = CandidateHostConfig {
2081
+ base_config: CandidateConfig {
2082
+ network: "udp".to_owned(),
2083
+ address: "192.168.0.2".to_owned(),
2084
+ port: 7777,
2085
+ component: 1,
2086
+ ..Default::default()
2087
+ },
2088
+ ..Default::default()
2089
+ };
2090
+ let local_candidate = host_config.new_candidate_host()?;
2091
+ let local_index = 0;
2092
+ agent.add_local_candidate(local_candidate)?;
2093
+
2094
+ // Add remote candidate
2095
+ let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
2096
+ let remote_host_config = CandidateHostConfig {
2097
+ base_config: CandidateConfig {
2098
+ network: "udp".to_owned(),
2099
+ address: "172.17.0.3".to_owned(),
2100
+ port: 9999,
2101
+ component: 1,
2102
+ ..Default::default()
2103
+ },
2104
+ ..Default::default()
2105
+ };
2106
+ let remote_candidate = remote_host_config.new_candidate_host()?;
2107
+ agent.add_remote_candidate(remote_candidate)?;
2108
+
2109
+ // Set remote credentials
2110
+ agent.ufrag_pwd.remote_credentials = Some(Credentials {
2111
+ ufrag: "remote_ufrag".to_string(),
2112
+ pwd: "remote_pwd".to_string(),
2113
+ });
2114
+
2115
+ let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
2116
+ let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
2117
+
2118
+ // Create STUN binding request from remote agent (also controlling, with larger tiebreaker)
2119
+ let remote_tiebreaker = 200; // Larger than ours (100)
2120
+ let mut msg = Message::new();
2121
+ msg.build(&[
2122
+ Box::new(BINDING_REQUEST),
2123
+ Box::new(TransactionId::new()),
2124
+ Box::new(Username::new(ATTR_USERNAME, username)),
2125
+ Box::new(AttrControlling(remote_tiebreaker)), // Remote is also controlling
2126
+ Box::new(PriorityAttr(1000)),
2127
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
2128
+ local_pwd,
2129
+ test_crypto_provider().crypto(),
2130
+ )),
2131
+ Box::new(FINGERPRINT),
2132
+ ])?;
2133
+
2134
+ // Agent should be controlling before handling the message
2135
+ assert!(
2136
+ agent.is_controlling,
2137
+ "Agent should be controlling before role conflict"
2138
+ );
2139
+
2140
+ // Store initial pair count and priorities
2141
+ let initial_pair_count = agent.candidate_pairs.len();
2142
+ let initial_priorities: Vec<u64> = agent.candidate_pairs.iter().map(|p| p.priority()).collect();
2143
+
2144
+ // Handle the inbound message - should detect role conflict
2145
+ let result = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
2146
+
2147
+ // The message should be processed (role conflict handling doesn't stop processing)
2148
+ // It may return an error for other reasons, but not for role conflict itself
2149
+ let _ = result; // We don't care about the exact error, just that role switching happened
2150
+
2151
+ // Agent should have switched to controlled (because our tiebreaker 100 < 200)
2152
+ assert!(
2153
+ !agent.is_controlling,
2154
+ "Agent should have switched to controlled role (smaller tiebreaker)"
2155
+ );
2156
+
2157
+ // Verify that candidate pair priorities were recomputed
2158
+ if !agent.candidate_pairs.is_empty() {
2159
+ // Check that ice_role_controlling was updated in pairs
2160
+ for pair in &agent.candidate_pairs {
2161
+ assert!(
2162
+ !pair.ice_role_controlling,
2163
+ "Candidate pair should have controlling role set to false"
2164
+ );
2165
+ }
2166
+
2167
+ // Priorities should change when role changes (unless there were no pairs initially)
2168
+ if initial_pair_count > 0 && initial_priorities.len() > 0 {
2169
+ // At least verify the role was updated in pairs
2170
+ assert_eq!(
2171
+ agent.candidate_pairs.len(),
2172
+ initial_pair_count,
2173
+ "Number of pairs should remain the same"
2174
+ );
2175
+ }
2176
+ }
2177
+
2178
+ // Verify nominated pair was cleared
2179
+ assert!(
2180
+ agent.nominated_pair.is_none(),
2181
+ "Nominated pair should be cleared after role switch"
2182
+ );
2183
+
2184
+ agent.close()?;
2185
+ Ok(())
2186
+ }
2187
+
2188
+ /// Test role conflict when both agents are controlling
2189
+ /// RFC 8445 Section 7.3.1.1 - Agent with larger tiebreaker should keep controlling role
2190
+ #[test]
2191
+ fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> {
2192
+ // Create agent with controlling role
2193
+ let mut config = AgentConfig::default();
2194
+ config.is_controlling = true;
2195
+ let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
2196
+
2197
+ // Set a larger tiebreaker value
2198
+ agent.tie_breaker = 500;
2199
+
2200
+ // Add local candidate
2201
+ let host_config = CandidateHostConfig {
2202
+ base_config: CandidateConfig {
2203
+ network: "udp".to_owned(),
2204
+ address: "192.168.0.2".to_owned(),
2205
+ port: 7777,
2206
+ component: 1,
2207
+ ..Default::default()
2208
+ },
2209
+ ..Default::default()
2210
+ };
2211
+ let local_candidate = host_config.new_candidate_host()?;
2212
+ let local_index = 0;
2213
+ agent.add_local_candidate(local_candidate)?;
2214
+
2215
+ // Add remote candidate
2216
+ let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
2217
+ let remote_host_config = CandidateHostConfig {
2218
+ base_config: CandidateConfig {
2219
+ network: "udp".to_owned(),
2220
+ address: "172.17.0.3".to_owned(),
2221
+ port: 9999,
2222
+ component: 1,
2223
+ ..Default::default()
2224
+ },
2225
+ ..Default::default()
2226
+ };
2227
+ let remote_candidate = remote_host_config.new_candidate_host()?;
2228
+ agent.add_remote_candidate(remote_candidate)?;
2229
+
2230
+ // Set remote credentials
2231
+ agent.ufrag_pwd.remote_credentials = Some(Credentials {
2232
+ ufrag: "remote_ufrag".to_string(),
2233
+ pwd: "remote_pwd".to_string(),
2234
+ });
2235
+
2236
+ let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
2237
+ let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
2238
+
2239
+ // Create STUN binding request from remote agent (also controlling, with smaller tiebreaker)
2240
+ let remote_tiebreaker = 100; // Smaller than ours (500)
2241
+ let mut msg = Message::new();
2242
+ msg.build(&[
2243
+ Box::new(BINDING_REQUEST),
2244
+ Box::new(TransactionId::new()),
2245
+ Box::new(Username::new(ATTR_USERNAME, username)),
2246
+ Box::new(AttrControlling(remote_tiebreaker)), // Remote is also controlling
2247
+ Box::new(PriorityAttr(1000)),
2248
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
2249
+ local_pwd,
2250
+ test_crypto_provider().crypto(),
2251
+ )),
2252
+ Box::new(FINGERPRINT),
2253
+ ])?;
2254
+
2255
+ // Agent should be controlling before handling the message
2256
+ assert!(
2257
+ agent.is_controlling,
2258
+ "Agent should be controlling before role conflict"
2259
+ );
2260
+
2261
+ // Handle the inbound message - should detect role conflict but NOT switch
2262
+ let _ = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
2263
+
2264
+ // Agent should remain controlling (because our tiebreaker 500 > 100)
2265
+ assert!(
2266
+ agent.is_controlling,
2267
+ "Agent should remain controlling (larger tiebreaker)"
2268
+ );
2269
+
2270
+ // Verify candidate pairs still have controlling role
2271
+ for pair in &agent.candidate_pairs {
2272
+ assert!(
2273
+ pair.ice_role_controlling,
2274
+ "Candidate pair should still have controlling role set to true"
2275
+ );
2276
+ }
2277
+
2278
+ agent.close()?;
2279
+ Ok(())
2280
+ }
2281
+
2282
+ /// Test role conflict when both agents are controlled
2283
+ /// RFC 8445 Section 7.3.1.1 - Agent with larger tiebreaker should switch to controlling
2284
+ #[test]
2285
+ fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> {
2286
+ use crate::attributes::control::AttrControlled;
2287
+
2288
+ // Create agent with controlled role
2289
+ let mut config = AgentConfig::default();
2290
+ config.is_controlling = false; // Controlled
2291
+ let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
2292
+
2293
+ // Set a larger tiebreaker value
2294
+ agent.tie_breaker = 500;
2295
+
2296
+ // Add local candidate
2297
+ let host_config = CandidateHostConfig {
2298
+ base_config: CandidateConfig {
2299
+ network: "udp".to_owned(),
2300
+ address: "192.168.0.2".to_owned(),
2301
+ port: 7777,
2302
+ component: 1,
2303
+ ..Default::default()
2304
+ },
2305
+ ..Default::default()
2306
+ };
2307
+ let local_candidate = host_config.new_candidate_host()?;
2308
+ let local_index = 0;
2309
+ agent.add_local_candidate(local_candidate)?;
2310
+
2311
+ // Add remote candidate
2312
+ let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
2313
+ let remote_host_config = CandidateHostConfig {
2314
+ base_config: CandidateConfig {
2315
+ network: "udp".to_owned(),
2316
+ address: "172.17.0.3".to_owned(),
2317
+ port: 9999,
2318
+ component: 1,
2319
+ ..Default::default()
2320
+ },
2321
+ ..Default::default()
2322
+ };
2323
+ let remote_candidate = remote_host_config.new_candidate_host()?;
2324
+ agent.add_remote_candidate(remote_candidate)?;
2325
+
2326
+ // Set remote credentials
2327
+ agent.ufrag_pwd.remote_credentials = Some(Credentials {
2328
+ ufrag: "remote_ufrag".to_string(),
2329
+ pwd: "remote_pwd".to_string(),
2330
+ });
2331
+
2332
+ let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
2333
+ let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
2334
+
2335
+ // Create STUN binding request from remote agent (also controlled, with smaller tiebreaker)
2336
+ let remote_tiebreaker = 100; // Smaller than ours (500)
2337
+ let mut msg = Message::new();
2338
+ msg.build(&[
2339
+ Box::new(BINDING_REQUEST),
2340
+ Box::new(TransactionId::new()),
2341
+ Box::new(Username::new(ATTR_USERNAME, username)),
2342
+ Box::new(AttrControlled(remote_tiebreaker)), // Remote is also controlled
2343
+ Box::new(PriorityAttr(1000)),
2344
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
2345
+ local_pwd,
2346
+ test_crypto_provider().crypto(),
2347
+ )),
2348
+ Box::new(FINGERPRINT),
2349
+ ])?;
2350
+
2351
+ // Agent should be controlled before handling the message
2352
+ assert!(
2353
+ !agent.is_controlling,
2354
+ "Agent should be controlled before role conflict"
2355
+ );
2356
+
2357
+ // Handle the inbound message - should detect role conflict
2358
+ let _ = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
2359
+
2360
+ // Agent should have switched to controlling (because our tiebreaker 500 > 100)
2361
+ assert!(
2362
+ agent.is_controlling,
2363
+ "Agent should have switched to controlling role (larger tiebreaker)"
2364
+ );
2365
+
2366
+ // Verify that candidate pair priorities were recomputed
2367
+ for pair in &agent.candidate_pairs {
2368
+ assert!(
2369
+ pair.ice_role_controlling,
2370
+ "Candidate pair should have controlling role set to true"
2371
+ );
2372
+ }
2373
+
2374
+ // Verify nominated pair was cleared
2375
+ assert!(
2376
+ agent.nominated_pair.is_none(),
2377
+ "Nominated pair should be cleared after role switch"
2378
+ );
2379
+
2380
+ agent.close()?;
2381
+ Ok(())
2382
+ }
2383
+
2384
+ /// Test role conflict when both agents are controlled
2385
+ /// RFC 8445 Section 7.3.1.1 - Agent with smaller tiebreaker should stay controlled
2386
+ #[test]
2387
+ fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> {
2388
+ use crate::attributes::control::AttrControlled;
2389
+
2390
+ // Create agent with controlled role
2391
+ let mut config = AgentConfig::default();
2392
+ config.is_controlling = false; // Controlled
2393
+ let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
2394
+
2395
+ // Set a smaller tiebreaker value
2396
+ agent.tie_breaker = 100;
2397
+
2398
+ // Add local candidate
2399
+ let host_config = CandidateHostConfig {
2400
+ base_config: CandidateConfig {
2401
+ network: "udp".to_owned(),
2402
+ address: "192.168.0.2".to_owned(),
2403
+ port: 7777,
2404
+ component: 1,
2405
+ ..Default::default()
2406
+ },
2407
+ ..Default::default()
2408
+ };
2409
+ let local_candidate = host_config.new_candidate_host()?;
2410
+ let local_index = 0;
2411
+ agent.add_local_candidate(local_candidate)?;
2412
+
2413
+ // Add remote candidate
2414
+ let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
2415
+ let remote_host_config = CandidateHostConfig {
2416
+ base_config: CandidateConfig {
2417
+ network: "udp".to_owned(),
2418
+ address: "172.17.0.3".to_owned(),
2419
+ port: 9999,
2420
+ component: 1,
2421
+ ..Default::default()
2422
+ },
2423
+ ..Default::default()
2424
+ };
2425
+ let remote_candidate = remote_host_config.new_candidate_host()?;
2426
+ agent.add_remote_candidate(remote_candidate)?;
2427
+
2428
+ // Set remote credentials
2429
+ agent.ufrag_pwd.remote_credentials = Some(Credentials {
2430
+ ufrag: "remote_ufrag".to_string(),
2431
+ pwd: "remote_pwd".to_string(),
2432
+ });
2433
+
2434
+ let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
2435
+ let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
2436
+
2437
+ // Create STUN binding request from remote agent (also controlled, with larger tiebreaker)
2438
+ let remote_tiebreaker = 500; // Larger than ours (100)
2439
+ let mut msg = Message::new();
2440
+ msg.build(&[
2441
+ Box::new(BINDING_REQUEST),
2442
+ Box::new(TransactionId::new()),
2443
+ Box::new(Username::new(ATTR_USERNAME, username)),
2444
+ Box::new(AttrControlled(remote_tiebreaker)), // Remote is also controlled
2445
+ Box::new(PriorityAttr(1000)),
2446
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
2447
+ local_pwd,
2448
+ test_crypto_provider().crypto(),
2449
+ )),
2450
+ Box::new(FINGERPRINT),
2451
+ ])?;
2452
+
2453
+ // Agent should be controlled before handling the message
2454
+ assert!(
2455
+ !agent.is_controlling,
2456
+ "Agent should be controlled before role conflict"
2457
+ );
2458
+
2459
+ // Handle the inbound message - should detect role conflict but NOT switch
2460
+ let _ = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
2461
+
2462
+ // Agent should remain controlled (because our tiebreaker 100 < 500)
2463
+ assert!(
2464
+ !agent.is_controlling,
2465
+ "Agent should remain controlled (smaller tiebreaker)"
2466
+ );
2467
+
2468
+ // Verify candidate pairs still have controlled role
2469
+ for pair in &agent.candidate_pairs {
2470
+ assert!(
2471
+ !pair.ice_role_controlling,
2472
+ "Candidate pair should still have controlling role set to false"
2473
+ );
2474
+ }
2475
+
2476
+ agent.close()?;
2477
+ Ok(())
2478
+ }
2479
+
2480
+ #[test]
2481
+ fn test_candidate_type_filtering() -> Result<()> {
2482
+ // Create an agent with only Relay candidate type allowed (simulates RTCIceTransportPolicy::Relay)
2483
+ let config = Arc::new(AgentConfig {
2484
+ candidate_types: vec![CandidateType::Relay],
2485
+ ..Default::default()
2486
+ });
2487
+ let mut agent = Agent::new(Instant::now(), config, test_crypto_provider())?;
2488
+
2489
+ // Host local candidate should be rejected
2490
+ let host_local = CandidateHostConfig {
2491
+ base_config: CandidateConfig {
2492
+ network: "udp".to_owned(),
2493
+ address: "192.168.1.1".to_owned(),
2494
+ port: 5000,
2495
+ component: 1,
2496
+ ..Default::default()
2497
+ },
2498
+ ..Default::default()
2499
+ }
2500
+ .new_candidate_host()?;
2501
+ assert!(
2502
+ !agent.add_local_candidate(host_local)?,
2503
+ "Host local candidate should be filtered out"
2504
+ );
2505
+
2506
+ // Remote candidates of any type should still be accepted (relay policy only filters local)
2507
+ let host_remote = CandidateHostConfig {
2508
+ base_config: CandidateConfig {
2509
+ network: "udp".to_owned(),
2510
+ address: "192.168.1.3".to_owned(),
2511
+ port: 5003,
2512
+ component: 1,
2513
+ ..Default::default()
2514
+ },
2515
+ ..Default::default()
2516
+ }
2517
+ .new_candidate_host()?;
2518
+ assert!(
2519
+ agent.add_remote_candidate(host_remote)?,
2520
+ "Remote candidates should be accepted regardless of candidate_types"
2521
+ );
2522
+
2523
+ // Relay local candidate should be accepted
2524
+ let relay_local = CandidateRelayConfig {
2525
+ base_config: CandidateConfig {
2526
+ network: "udp".to_owned(),
2527
+ address: "1.2.3.4".to_owned(),
2528
+ port: 5004,
2529
+ component: 1,
2530
+ ..Default::default()
2531
+ },
2532
+ rel_addr: "4.3.2.1".to_owned(),
2533
+ rel_port: 5005,
2534
+ ..Default::default()
2535
+ }
2536
+ .new_candidate_relay()?;
2537
+ assert!(
2538
+ agent.add_local_candidate(relay_local)?,
2539
+ "Relay local candidate should be accepted"
2540
+ );
2541
+
2542
+ assert_eq!(
2543
+ agent.local_candidates.len(),
2544
+ 1,
2545
+ "Only the relay candidate should be stored"
2546
+ );
2547
+ assert_eq!(
2548
+ agent.remote_candidates.len(),
2549
+ 1,
2550
+ "Remote host candidate should be stored"
2551
+ );
2552
+
2553
+ agent.close()?;
2554
+ Ok(())
2555
+ }
2556
+
2557
+ // Verify that check_keepalive sends a STUN ping even when media traffic has
2558
+ // recently updated the candidate timestamps (RFC 7675).
2559
+ #[test]
2560
+ fn test_keepalive_sent_during_media_flow() -> Result<()> {
2561
+ let mut a = Agent::new(
2562
+ Instant::now(),
2563
+ Arc::new(AgentConfig::default()),
2564
+ test_crypto_provider(),
2565
+ )?;
2566
+
2567
+ // Set up a selected pair
2568
+ let host_local = CandidateHostConfig {
2569
+ base_config: CandidateConfig {
2570
+ network: "udp".to_owned(),
2571
+ address: "192.168.1.1".to_owned(),
2572
+ port: 19216,
2573
+ component: 1,
2574
+ ..Default::default()
2575
+ },
2576
+ ..Default::default()
2577
+ }
2578
+ .new_candidate_host()?;
2579
+ a.add_local_candidate(host_local)?;
2580
+
2581
+ let relay_remote = CandidateRelayConfig {
2582
+ base_config: CandidateConfig {
2583
+ network: "udp".to_owned(),
2584
+ address: "1.2.3.4".to_owned(),
2585
+ port: 12340,
2586
+ component: 1,
2587
+ ..Default::default()
2588
+ },
2589
+ rel_addr: "4.3.2.1".to_owned(),
2590
+ rel_port: 43210,
2591
+ ..Default::default()
2592
+ }
2593
+ .new_candidate_relay()?;
2594
+ a.add_remote_candidate(relay_remote)?;
2595
+
2596
+ a.ufrag_pwd.remote_credentials = Some(Credentials {
2597
+ ufrag: "remoteufrag".to_string(),
2598
+ pwd: "remotepwd".to_string(),
2599
+ });
2600
+ a.is_controlling = true;
2601
+
2602
+ a.add_pair(0, 0);
2603
+ a.set_selected_pair(Some(Instant::now()), Some(0));
2604
+
2605
+ // Simulate recent media activity on both candidates
2606
+ a.local_candidates[0].seen(Instant::now(), true);
2607
+ a.remote_candidates[0].seen(Instant::now(), false);
2608
+
2609
+ // Virtual time: the agent is told the instant, so nothing here sleeps or samples a clock.
2610
+ let base = Instant::now();
2611
+ let keepalive_interval = a.keepalive_interval;
2612
+
2613
+ // Pretend the last consent ping was long ago such that the interval has elapsed
2614
+ a.last_consent_sent = base - Duration::from_secs(10);
2615
+
2616
+ // Drain any events/writes from setup
2617
+ a.write_outs.clear();
2618
+
2619
+ // First call should send a consent ping immediately
2620
+ a.check_keepalive(base);
2621
+ assert!(
2622
+ !a.write_outs.is_empty(),
2623
+ "check_keepalive must send a STUN ping even when media timestamps are fresh"
2624
+ );
2625
+
2626
+ // Drain and call again at the *same* instant; the interval has not elapsed, so no second ping.
2627
+ // Before clock injection this relied on the wall clock not advancing between two calls.
2628
+ a.write_outs.clear();
2629
+ a.check_keepalive(base);
2630
+ assert!(
2631
+ a.write_outs.is_empty(),
2632
+ "check_keepalive must not send again before keepalive_interval elapses"
2633
+ );
2634
+
2635
+ // Advance virtual time past the interval: the next ping goes out, instantly and reproducibly.
2636
+ a.check_keepalive(base + keepalive_interval + Duration::from_millis(1));
2637
+ assert!(
2638
+ !a.write_outs.is_empty(),
2639
+ "check_keepalive must send again once keepalive_interval has elapsed"
2640
+ );
2641
+
2642
+ a.close()?;
2643
+ Ok(())
2644
+ }
2645
+
2646
+ #[test]
2647
+ fn test_pair_network_type_mismatch() -> Result<()> {
2648
+ let mut a = Agent::new(
2649
+ Instant::now(),
2650
+ Arc::new(AgentConfig::default()),
2651
+ test_crypto_provider(),
2652
+ )?;
2653
+
2654
+ // UDP: IPv4 local should not pair with IPv6 remote.
2655
+ let local_v4 = CandidateHostConfig {
2656
+ base_config: CandidateConfig {
2657
+ network: "udp".to_owned(),
2658
+ address: "192.168.1.1".to_owned(),
2659
+ port: 7777,
2660
+ component: 1,
2661
+ ..Default::default()
2662
+ },
2663
+ ..Default::default()
2664
+ }
2665
+ .new_candidate_host()?;
2666
+ a.add_local_candidate(local_v4)?;
2667
+
2668
+ let remote_v6 = CandidateHostConfig {
2669
+ base_config: CandidateConfig {
2670
+ network: "udp".to_owned(),
2671
+ address: "2001:db8::1".to_owned(),
2672
+ port: 8888,
2673
+ component: 1,
2674
+ ..Default::default()
2675
+ },
2676
+ ..Default::default()
2677
+ }
2678
+ .new_candidate_host()?;
2679
+ a.add_remote_candidate(remote_v6)?;
2680
+
2681
+ assert!(
2682
+ a.candidate_pairs.is_empty(),
2683
+ "IPv4 local and IPv6 remote candidates should not be paired"
2684
+ );
2685
+
2686
+ // UDP: matching IPv4 should pair.
2687
+ let remote_v4 = CandidateHostConfig {
2688
+ base_config: CandidateConfig {
2689
+ network: "udp".to_owned(),
2690
+ address: "192.168.1.2".to_owned(),
2691
+ port: 8888,
2692
+ component: 1,
2693
+ ..Default::default()
2694
+ },
2695
+ ..Default::default()
2696
+ }
2697
+ .new_candidate_host()?;
2698
+ a.add_remote_candidate(remote_v4)?;
2699
+
2700
+ assert_eq!(
2701
+ a.candidate_pairs.len(),
2702
+ 1,
2703
+ "IPv4 local and IPv4 remote candidates should form exactly one pair"
2704
+ );
2705
+
2706
+ // TCP: IPv4 active local should not pair with IPv6 passive remote.
2707
+ let local_tcp4_active = CandidateHostConfig {
2708
+ base_config: CandidateConfig {
2709
+ network: "tcp".to_owned(),
2710
+ address: "192.168.1.3".to_owned(),
2711
+ port: 7778,
2712
+ component: 1,
2713
+ ..Default::default()
2714
+ },
2715
+ tcp_type: TcpType::Active,
2716
+ }
2717
+ .new_candidate_host()?;
2718
+ a.add_local_candidate(local_tcp4_active)?;
2719
+
2720
+ let remote_tcp6_passive = CandidateHostConfig {
2721
+ base_config: CandidateConfig {
2722
+ network: "tcp".to_owned(),
2723
+ address: "2001:db8::2".to_owned(),
2724
+ port: 8889,
2725
+ component: 1,
2726
+ ..Default::default()
2727
+ },
2728
+ tcp_type: TcpType::Passive,
2729
+ }
2730
+ .new_candidate_host()?;
2731
+ a.add_remote_candidate(remote_tcp6_passive)?;
2732
+
2733
+ assert_eq!(
2734
+ a.candidate_pairs.len(),
2735
+ 1,
2736
+ "TCP4 active local and TCP6 passive remote should not create an additional pair"
2737
+ );
2738
+
2739
+ // TCP: matching IPv4 active/passive should pair.
2740
+ let remote_tcp4_passive = CandidateHostConfig {
2741
+ base_config: CandidateConfig {
2742
+ network: "tcp".to_owned(),
2743
+ address: "192.168.1.4".to_owned(),
2744
+ port: 8890,
2745
+ component: 1,
2746
+ ..Default::default()
2747
+ },
2748
+ tcp_type: TcpType::Passive,
2749
+ }
2750
+ .new_candidate_host()?;
2751
+ a.add_remote_candidate(remote_tcp4_passive)?;
2752
+
2753
+ assert_eq!(
2754
+ a.candidate_pairs.len(),
2755
+ 2,
2756
+ "TCP4 active local and TCP4 passive remote should form exactly one pair"
2757
+ );
2758
+
2759
+ a.close()?;
2760
+ Ok(())
2761
+ }
2762
+
2763
+ // Regression test for https://github.com/webrtc-rs/rtc/issues/88 (Issue 1).
2764
+ //
2765
+ // When the agent transitions to `Failed` it calls `delete_all_candidates`, which
2766
+ // empties `local_candidates`/`remote_candidates`. Previously the candidate pairs -
2767
+ // and the `selected_pair`/`nominated_pair` indices into them - were left behind, so
2768
+ // they referenced candidates that no longer existed. Any later access through a pair,
2769
+ // e.g. `get_best_available_candidate_pair`, then indexed out of bounds and panicked:
2770
+ //
2771
+ // thread '...' panicked at 'index out of bounds: the len is 0 but the index is 0'
2772
+ #[test]
2773
+ fn test_transition_to_failed_clears_stale_candidate_pairs() -> Result<()> {
2774
+ let mut a = Agent::new(
2775
+ Instant::now(),
2776
+ Arc::new(AgentConfig::default()),
2777
+ test_crypto_provider(),
2778
+ )?;
2779
+
2780
+ let local = CandidateHostConfig {
2781
+ base_config: CandidateConfig {
2782
+ network: "udp".to_owned(),
2783
+ address: "192.168.0.2".to_owned(),
2784
+ port: 777,
2785
+ component: 1,
2786
+ ..Default::default()
2787
+ },
2788
+ ..Default::default()
2789
+ }
2790
+ .new_candidate_host()?;
2791
+ a.add_local_candidate(local)?;
2792
+
2793
+ let remote = CandidateHostConfig {
2794
+ base_config: CandidateConfig {
2795
+ network: "udp".to_owned(),
2796
+ address: "172.17.0.3".to_owned(),
2797
+ port: 999,
2798
+ component: 1,
2799
+ ..Default::default()
2800
+ },
2801
+ ..Default::default()
2802
+ }
2803
+ .new_candidate_host()?;
2804
+ a.add_remote_candidate(remote)?;
2805
+
2806
+ // A single local/remote pair now exists; mark it selected and nominated so that
2807
+ // both index-into-pairs fields are populated.
2808
+ assert_eq!(a.candidate_pairs.len(), 1);
2809
+ a.set_selected_pair(Some(Instant::now()), Some(0));
2810
+ a.nominated_pair = Some(0);
2811
+
2812
+ // Transition to Failed: this deletes all candidates.
2813
+ a.update_connection_state(Some(Instant::now()), ConnectionState::Failed);
2814
+
2815
+ // The pairs and the indices into them must be gone, otherwise they dangle.
2816
+ assert!(
2817
+ a.candidate_pairs.is_empty(),
2818
+ "candidate pairs must be cleared when candidates are deleted"
2819
+ );
2820
+ assert!(a.selected_pair.is_none(), "selected_pair must be reset");
2821
+ assert!(a.nominated_pair.is_none(), "nominated_pair must be reset");
2822
+ assert!(a.local_candidates.is_empty());
2823
+ assert!(a.remote_candidates.is_empty());
2824
+
2825
+ // Before the fix these accessors dereferenced stale pair indices into the emptied
2826
+ // candidate vectors and panicked; now they must simply return `None`.
2827
+ assert!(a.get_selected_candidate_pair().is_none());
2828
+ assert!(a.get_best_available_candidate_pair().is_none());
2829
+
2830
+ a.close()?;
2831
+ Ok(())
2832
+ }
2833
+
2834
+ // Regression test for https://github.com/webrtc-rs/rtc/issues/88 (Issue 2).
2835
+ //
2836
+ // Handling an inbound binding request from an unknown address adds a peer-reflexive
2837
+ // remote candidate. Previously `add_remote_candidate` ran the connectivity check
2838
+ // inline, which could advance the state to `Failed` and clear `remote_candidates`
2839
+ // mid-call. The caller then computed `remote_candidates.len() - 1` on an empty vector
2840
+ // and panicked ('attempt to subtract with overflow' in debug builds). Deferring the
2841
+ // check to `handle_timeout` keeps candidate mutation free of surprising re-entrant
2842
+ // state changes.
2843
+ #[test]
2844
+ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> {
2845
+ use sansio::Protocol as _;
2846
+
2847
+ let cfg = AgentConfig {
2848
+ disconnected_timeout: Some(Duration::from_secs(1)),
2849
+ failed_timeout: Some(Duration::from_secs(1)),
2850
+ ..Default::default()
2851
+ };
2852
+ let mut a = Agent::new(Instant::now(), Arc::new(cfg), test_crypto_provider())?;
2853
+
2854
+ let local_candidate = CandidateHostConfig {
2855
+ base_config: CandidateConfig {
2856
+ network: "udp".to_owned(),
2857
+ address: "192.168.0.2".to_owned(),
2858
+ port: 777,
2859
+ component: 1,
2860
+ ..Default::default()
2861
+ },
2862
+ ..Default::default()
2863
+ }
2864
+ .new_candidate_host()?;
2865
+ let local_index = 0;
2866
+ let local_priority = local_candidate.priority();
2867
+ a.add_local_candidate(local_candidate)?;
2868
+
2869
+ a.ufrag_pwd.remote_credentials = Some(Credentials {
2870
+ ufrag: "".to_string(),
2871
+ pwd: "".to_string(),
2872
+ });
2873
+ let username = a.ufrag_pwd.local_credentials.ufrag.clone() + ":";
2874
+ let local_pwd = a.ufrag_pwd.local_credentials.pwd.clone();
2875
+ let tie_breaker = a.tie_breaker;
2876
+
2877
+ // Put the agent in a state where running a connectivity check now would time out
2878
+ // and transition to `Failed` (which deletes all candidates).
2879
+ a.connection_state = ConnectionState::Checking;
2880
+ a.last_connection_state = ConnectionState::Checking;
2881
+ a.checking_duration = Instant::now()
2882
+ .checked_sub(Duration::from_secs(3600))
2883
+ .unwrap_or(a.start_time);
2884
+
2885
+ let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
2886
+ let mut msg = Message::new();
2887
+ msg.build(&[
2888
+ Box::new(BINDING_REQUEST),
2889
+ Box::new(TransactionId::new()),
2890
+ Box::new(Username::new(ATTR_USERNAME, username)),
2891
+ Box::new(UseCandidateAttr::new()),
2892
+ Box::new(AttrControlling(tie_breaker)),
2893
+ Box::new(PriorityAttr(local_priority)),
2894
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
2895
+ local_pwd,
2896
+ test_crypto_provider().crypto(),
2897
+ )),
2898
+ Box::new(FINGERPRINT),
2899
+ ])?;
2900
+
2901
+ // Before the fix this call panicked while adding the peer-reflexive candidate.
2902
+ a.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr)?;
2903
+
2904
+ // The candidate was added and no re-entrant state change happened during the call.
2905
+ assert_eq!(
2906
+ a.remote_candidates.len(),
2907
+ 1,
2908
+ "peer-reflexive remote candidate should have been added"
2909
+ );
2910
+ assert_eq!(
2911
+ a.connection_state,
2912
+ ConnectionState::Checking,
2913
+ "connection state must not change while handling the inbound request"
2914
+ );
2915
+ assert!(
2916
+ a.force_candidate_contact,
2917
+ "a connectivity check should have been deferred to the timeout handler"
2918
+ );
2919
+
2920
+ // The deferred check now runs via the timeout handler, fails, and cleans up fully.
2921
+ a.handle_timeout(Instant::now())?;
2922
+ assert_eq!(a.connection_state, ConnectionState::Failed);
2923
+ assert!(a.candidate_pairs.is_empty());
2924
+ assert!(a.local_candidates.is_empty());
2925
+ assert!(a.remote_candidates.is_empty());
2926
+ assert!(a.selected_pair.is_none());
2927
+ assert!(a.nominated_pair.is_none());
2928
+
2929
+ a.close()?;
2930
+ Ok(())
2931
+ }
2932
+
2933
+ // Safari (and Chrome) expose ICE host candidates behind an mDNS "<uuid>.local"
2934
+ // hostname by default. A QueryOnly agent (the default) must RESOLVE such a
2935
+ // candidate -- i.e. issue an mDNS query, which surfaces as an outbound packet to
2936
+ // MDNS_PORT -- whereas a Disabled agent silently drops it. This guards the
2937
+ // behavioral consequence of defaulting the SettingEngine mDNS mode to QueryOnly.
2938
+ //
2939
+ // `add_remote_candidate` takes no instant, so it only *schedules* the query; the
2940
+ // packet is stamped and emitted by the next `handle_timeout`, which does have one.
2941
+ // `poll_timeout` reports an already-due deadline so the driver comes back at once.
2942
+ #[test]
2943
+ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> {
2944
+ // A .local host candidate in Safari's exact format.
2945
+ let cand_line =
2946
+ "1114572465 1 udp 2113939711 61b445d2-6503-41ac-96ce-ee3edac00e9f.local 61163 typ host";
2947
+
2948
+ let base = Instant::now();
2949
+
2950
+ // QueryOnly: adding the candidate issues an mDNS query (not a drop).
2951
+ let mut agent = Agent::new(
2952
+ base,
2953
+ Arc::new(AgentConfig {
2954
+ multicast_dns_mode: crate::mdns::MulticastDnsMode::QueryOnly,
2955
+ ..Default::default()
2956
+ }),
2957
+ test_crypto_provider(),
2958
+ )?;
2959
+ let added = agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?;
2960
+ assert!(
2961
+ !added,
2962
+ "an mDNS candidate is not immediately usable; resolution is async"
2963
+ );
2964
+ let deadline = agent
2965
+ .poll_timeout()
2966
+ .expect("a scheduled mDNS query must ask the driver for a timeout");
2967
+ assert!(
2968
+ deadline <= base,
2969
+ "the scheduled query is already due, so the driver runs it immediately"
2970
+ );
2971
+ agent.handle_timeout(deadline)?;
2972
+ let pkt = agent
2973
+ .poll_write()
2974
+ .expect("QueryOnly must emit an mDNS query for a .local remote candidate");
2975
+ assert_eq!(
2976
+ pkt.transport.peer_addr.port(),
2977
+ mdns::MDNS_PORT,
2978
+ "the emitted packet must be an mDNS query"
2979
+ );
2980
+ assert_eq!(
2981
+ pkt.now, deadline,
2982
+ "the query is stamped with the instant the driver supplied, not an ambient one"
2983
+ );
2984
+
2985
+ // Disabled: the same candidate is silently dropped -- no mDNS query.
2986
+ let mut agent = Agent::new(
2987
+ base,
2988
+ Arc::new(AgentConfig {
2989
+ multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled,
2990
+ ..Default::default()
2991
+ }),
2992
+ test_crypto_provider(),
2993
+ )?;
2994
+ agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?;
2995
+ assert!(
2996
+ agent.poll_write().is_none(),
2997
+ "Disabled must not emit an mDNS query for a .local remote candidate"
2998
+ );
2999
+
3000
+ Ok(())
3001
+ }
3002
+
3003
+ /// A terminal ICE agent must not keep advertising its last connectivity-check deadline.
3004
+ ///
3005
+ /// `contact` intentionally returns without updating `last_checking_time` after the agent fails.
3006
+ /// If `poll_timeout` continues to derive a deadline from that value, every call after failure
3007
+ /// returns the same instant in the past and a Sans-I/O driver can spin indefinitely.
3008
+ #[test]
3009
+ fn test_failed_agent_stops_and_restart_resumes_connectivity_check_timer() -> Result<()> {
3010
+ let base = Instant::now();
3011
+ let mut agent = Agent::new(
3012
+ base,
3013
+ Arc::new(AgentConfig {
3014
+ multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled,
3015
+ ..Default::default()
3016
+ }),
3017
+ test_crypto_provider(),
3018
+ )?;
3019
+
3020
+ agent.start_connectivity_checks(
3021
+ base,
3022
+ true,
3023
+ "remote-ufrag".to_owned(),
3024
+ "remote-password".to_owned(),
3025
+ )?;
3026
+ agent.update_connection_state(
3027
+ Some(base + Duration::from_secs(190)),
3028
+ ConnectionState::Failed,
3029
+ );
3030
+
3031
+ assert_eq!(
3032
+ agent.poll_timeout(),
3033
+ None,
3034
+ "a failed agent has no more connectivity checks to schedule"
3035
+ );
3036
+ agent.handle_timeout(base + Duration::from_secs(191))?;
3037
+ assert_eq!(
3038
+ agent.poll_timeout(),
3039
+ None,
3040
+ "handling time after failure must not recreate the stale deadline"
3041
+ );
3042
+
3043
+ let restart_time = base + Duration::from_secs(200);
3044
+ agent.apply_restart(restart_time, true)?;
3045
+ agent.start_connectivity_checks(
3046
+ restart_time,
3047
+ true,
3048
+ "new-remote-ufrag".to_owned(),
3049
+ "new-remote-password".to_owned(),
3050
+ )?;
3051
+
3052
+ assert!(
3053
+ agent.poll_timeout().is_some(),
3054
+ "an ICE restart must resume connectivity-check scheduling"
3055
+ );
3056
+
3057
+ agent.close()?;
3058
+ assert_eq!(
3059
+ agent.poll_timeout(),
3060
+ None,
3061
+ "a closed agent has no more connectivity checks to schedule"
3062
+ );
3063
+
3064
+ Ok(())
3065
+ }
3066
+
3067
+ /// Regression test: connectivity checks for a server-reflexive local candidate
3068
+ /// must be tagged with the candidate's base (the bound host socket), not the
3069
+ /// NAT-mapped srflx address (RFC 8445 §6.1.2). Drivers that route outbound
3070
+ /// transmits by `transport.local_addr` otherwise have no socket to send from
3071
+ /// and drop the packet.
3072
+ #[test]
3073
+ fn test_send_stun_from_srflx_uses_base_addr() -> Result<()> {
3074
+ let mut a = Agent::new(
3075
+ Instant::now(),
3076
+ Arc::new(AgentConfig::default()),
3077
+ test_crypto_provider(),
3078
+ )?;
3079
+
3080
+ let srflx_local = CandidateServerReflexiveConfig {
3081
+ base_config: CandidateConfig {
3082
+ network: "udp".to_owned(),
3083
+ address: "10.79.12.1".to_owned(), // NAT mapping; no local socket here
3084
+ port: 60823,
3085
+ component: 1,
3086
+ ..Default::default()
3087
+ },
3088
+ rel_addr: "192.168.0.2".to_owned(), // base: bound host socket
3089
+ rel_port: 5000,
3090
+ ..Default::default()
3091
+ }
3092
+ .new_candidate_server_reflexive()?;
3093
+ a.add_local_candidate(srflx_local)?;
3094
+
3095
+ let host_remote = CandidateHostConfig {
3096
+ base_config: CandidateConfig {
3097
+ network: "udp".to_owned(),
3098
+ address: "10.79.11.1".to_owned(),
3099
+ port: 37983,
3100
+ component: 1,
3101
+ ..Default::default()
3102
+ },
3103
+ ..Default::default()
3104
+ }
3105
+ .new_candidate_host()?;
3106
+ a.add_remote_candidate(host_remote)?;
3107
+
3108
+ a.write_outs.clear();
3109
+
3110
+ let msg = Message::new();
3111
+ a.send_stun(Instant::now(), &msg, 0, 0);
3112
+
3113
+ let transmit = a.write_outs.pop_front().expect("send_stun must emit");
3114
+ assert_eq!(
3115
+ transmit.transport.local_addr,
3116
+ "192.168.0.2:5000".parse().unwrap(),
3117
+ "srflx checks must be sent from the candidate's base, not the mapped address"
3118
+ );
3119
+ assert_eq!(
3120
+ transmit.transport.peer_addr,
3121
+ "10.79.11.1:37983".parse().unwrap()
3122
+ );
3123
+
3124
+ a.close()?;
3125
+ Ok(())
3126
+ }
3127
+
3128
+ /// Staging an ICE restart must not disturb the live session.
3129
+ ///
3130
+ /// JSEP requires `createOffer` to be free of side effects, so the credentials an ICE-restart offer
3131
+ /// advertises are generated but *not* installed until the local description is applied. If they
3132
+ /// were installed at offer time, inbound STUN would immediately start failing its USERNAME and
3133
+ /// MESSAGE-INTEGRITY checks against the new pair — and permanently so if the offer were discarded,
3134
+ /// which JSEP explicitly permits.
3135
+ #[test]
3136
+ fn test_staged_ice_restart_keeps_live_session_authenticating() -> Result<()> {
3137
+ let mut a = Agent::new(
3138
+ Instant::now(),
3139
+ Arc::new(AgentConfig::default()),
3140
+ test_crypto_provider(),
3141
+ )?;
3142
+
3143
+ let local_candidate = CandidateHostConfig {
3144
+ base_config: CandidateConfig {
3145
+ network: "udp".to_owned(),
3146
+ address: "192.168.0.2".to_owned(),
3147
+ port: 777,
3148
+ component: 1,
3149
+ ..Default::default()
3150
+ },
3151
+ ..Default::default()
3152
+ }
3153
+ .new_candidate_host()?;
3154
+ let local_priority = local_candidate.priority();
3155
+ a.add_local_candidate(local_candidate)?;
3156
+
3157
+ a.ufrag_pwd.remote_credentials = Some(Credentials {
3158
+ ufrag: String::new(),
3159
+ pwd: String::new(),
3160
+ });
3161
+
3162
+ let original = a.ufrag_pwd.local_credentials.clone();
3163
+ let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
3164
+
3165
+ // Builds a binding request authenticated with `pwd`, as the remote peer would send it.
3166
+ let inbound = |ufrag: &str, pwd: String, tie_breaker: u64| -> Result<Message> {
3167
+ let mut msg = Message::new();
3168
+ msg.build(&[
3169
+ Box::new(BINDING_REQUEST),
3170
+ Box::new(TransactionId::new()),
3171
+ Box::new(Username::new(ATTR_USERNAME, format!("{ufrag}:"))),
3172
+ Box::new(AttrControlling(tie_breaker)),
3173
+ Box::new(PriorityAttr(local_priority)),
3174
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
3175
+ pwd,
3176
+ test_crypto_provider().crypto(),
3177
+ )),
3178
+ Box::new(FINGERPRINT),
3179
+ ])?;
3180
+ Ok(msg)
3181
+ };
3182
+
3183
+ // Baseline: the peer's checks are accepted.
3184
+ let mut msg = inbound(&original.ufrag, original.pwd.clone(), a.tie_breaker)?;
3185
+ a.handle_inbound(Instant::now(), &mut msg, 0, remote_addr)?;
3186
+
3187
+ // What `create_offer` does for an ICE restart: generate credentials for the offer to carry.
3188
+ a.generate_restart_credentials(String::new(), String::new())?;
3189
+
3190
+ // The offer advertises the new pair...
3191
+ let advertised = a.get_local_credentials().clone();
3192
+ assert_ne!(
3193
+ advertised.ufrag, original.ufrag,
3194
+ "staging a restart must generate a fresh ufrag for the offer"
3195
+ );
3196
+ assert_ne!(advertised.pwd, original.pwd);
3197
+
3198
+ // ...while the live session still authenticates with the old one.
3199
+ // (`Credentials` deliberately implements neither `Debug` nor `PartialEq`: `pwd` is the
3200
+ // MESSAGE-INTEGRITY key and must not reach a log line, so compare the fields directly.)
3201
+ assert_eq!(
3202
+ a.ufrag_pwd.local_credentials.ufrag, original.ufrag,
3203
+ "staging must not install the new ufrag into the live session"
3204
+ );
3205
+ assert_eq!(
3206
+ a.ufrag_pwd.local_credentials.pwd, original.pwd,
3207
+ "staging must not install the new pwd into the live session"
3208
+ );
3209
+
3210
+ // The guarantee: the peer is still sending checks keyed on the original credentials, and they
3211
+ // must keep being accepted while the offer is in flight.
3212
+ let mut msg = inbound(&original.ufrag, original.pwd.clone(), a.tie_breaker)?;
3213
+ a.handle_inbound(Instant::now(), &mut msg, 0, remote_addr)
3214
+ .expect("inbound STUN keyed on the pre-restart credentials must still validate");
3215
+
3216
+ // The application discards the offer and never sets a local description. The connection must
3217
+ // be exactly as it was.
3218
+ assert!(a.has_pending_restart());
3219
+ let mut msg = inbound(&original.ufrag, original.pwd.clone(), a.tie_breaker)?;
3220
+ a.handle_inbound(Instant::now(), &mut msg, 0, remote_addr)
3221
+ .expect("a discarded ICE-restart offer must leave the session working");
3222
+
3223
+ // Applying it — what `set_local_description` does — installs the staged pair.
3224
+ a.apply_restart(Instant::now(), true)?;
3225
+ assert!(!a.has_pending_restart());
3226
+ assert_eq!(
3227
+ a.ufrag_pwd.local_credentials.ufrag, advertised.ufrag,
3228
+ "apply_restart must install exactly the ufrag the offer advertised"
3229
+ );
3230
+ assert_eq!(
3231
+ a.ufrag_pwd.local_credentials.pwd, advertised.pwd,
3232
+ "apply_restart must install exactly the pwd the offer advertised"
3233
+ );
3234
+
3235
+ a.close()?;
3236
+ Ok(())
3237
+ }
3238
+
3239
+ /// Agent events carry the instant their condition was observed at, so a consumer draining
3240
+ /// `poll_event` is *told* the time rather than having to retain one and guess. `close` has no
3241
+ /// caller-supplied instant, so it stamps with the newest one the agent was given.
3242
+ #[test]
3243
+ fn test_agent_events_carry_the_instant_they_were_observed_at() -> Result<()> {
3244
+ let base = Instant::now();
3245
+ let t = |secs| base + Duration::from_secs(secs);
3246
+
3247
+ let mut a = Agent::new(
3248
+ t(0),
3249
+ Arc::new(AgentConfig::default()),
3250
+ test_crypto_provider(),
3251
+ )?;
3252
+
3253
+ let host_local = CandidateHostConfig {
3254
+ base_config: CandidateConfig {
3255
+ network: "udp".to_owned(),
3256
+ address: "192.168.0.2".to_owned(),
3257
+ port: 1000,
3258
+ component: 1,
3259
+ ..Default::default()
3260
+ },
3261
+ ..Default::default()
3262
+ }
3263
+ .new_candidate_host()?;
3264
+ a.add_local_candidate(host_local)?;
3265
+
3266
+ let relay_remote = CandidateRelayConfig {
3267
+ base_config: CandidateConfig {
3268
+ network: "udp".to_owned(),
3269
+ address: "1.2.3.4".to_owned(),
3270
+ port: 12340,
3271
+ component: 1,
3272
+ ..Default::default()
3273
+ },
3274
+ rel_addr: "4.3.2.1".to_owned(),
3275
+ rel_port: 43210,
3276
+ ..Default::default()
3277
+ }
3278
+ .new_candidate_relay()?;
3279
+ a.add_remote_candidate(relay_remote)?;
3280
+
3281
+ a.add_pair(0, 0);
3282
+ a.set_selected_pair(Some(t(5)), Some(0));
3283
+
3284
+ // Selecting a pair emits Connected *and* SelectedCandidatePairChange; both are stamped
3285
+ // with the instant that caused them, not with a reading taken at drain time.
3286
+ let stamped: Vec<_> = std::iter::from_fn(|| a.poll_event()).collect();
3287
+ assert!(!stamped.is_empty(), "selecting a pair emits events");
3288
+ for e in &stamped {
3289
+ assert_eq!(
3290
+ e.now,
3291
+ t(5),
3292
+ "event {:?} must carry the instant its condition was observed at",
3293
+ std::mem::discriminant(&e.event)
3294
+ );
3295
+ }
3296
+
3297
+ a.handle_timeout(t(30))?;
3298
+ while a.poll_event().is_some() {}
3299
+
3300
+ // `close` is a drain with no caller-supplied instant, and the agent retains none, so the
3301
+ // Closed transition is applied to `connection_state` but emits no event — there is no
3302
+ // honest instant to stamp one with. A consumer that needs to observe the close should read
3303
+ // the state rather than wait for an event.
3304
+ a.close()?;
3305
+ assert_eq!(a.connection_state, ConnectionState::Closed);
3306
+ assert!(
3307
+ a.poll_event().is_none(),
3308
+ "close emits no event, having no instant to stamp one with"
3309
+ );
3310
+
3311
+ Ok(())
3312
+ }
3313
+
3314
+ /// A `getStats` snapshot reports the instant the caller asked at, throughout — including the
3315
+ /// fields that default rather than being populated from live state. Mixing virtual protocol time
3316
+ /// with wall-clock timestamps in one report is worse than no report: the reader cannot tell which
3317
+ /// numbers are comparable.
3318
+ #[test]
3319
+ fn test_stats_snapshot_reports_the_callers_instant_throughout() -> Result<()> {
3320
+ let base = Instant::now();
3321
+ let t = |secs| base + Duration::from_secs(secs);
3322
+
3323
+ let mut a = Agent::new(
3324
+ t(0),
3325
+ Arc::new(AgentConfig::default()),
3326
+ test_crypto_provider(),
3327
+ )?;
3328
+
3329
+ let host_local = CandidateHostConfig {
3330
+ base_config: CandidateConfig {
3331
+ network: "udp".to_owned(),
3332
+ address: "192.168.0.2".to_owned(),
3333
+ port: 1000,
3334
+ component: 1,
3335
+ ..Default::default()
3336
+ },
3337
+ ..Default::default()
3338
+ }
3339
+ .new_candidate_host()?;
3340
+ a.add_local_candidate(host_local)?;
3341
+
3342
+ let host_remote = CandidateHostConfig {
3343
+ base_config: CandidateConfig {
3344
+ network: "udp".to_owned(),
3345
+ address: "192.168.0.3".to_owned(),
3346
+ port: 1001,
3347
+ component: 1,
3348
+ ..Default::default()
3349
+ },
3350
+ ..Default::default()
3351
+ }
3352
+ .new_candidate_host()?;
3353
+ a.add_remote_candidate(host_remote)?;
3354
+
3355
+ // Take the snapshot at an instant far from "now", so a stray wall-clock read is obvious.
3356
+ let pairs = a.get_candidate_pairs_stats(t(600));
3357
+ assert!(!pairs.is_empty(), "adding a remote candidate forms a pair");
3358
+ for p in &pairs {
3359
+ for (field, stamp) in [
3360
+ ("timestamp", p.timestamp),
3361
+ ("last_packet_sent_timestamp", p.last_packet_sent_timestamp),
3362
+ (
3363
+ "last_packet_received_timestamp",
3364
+ p.last_packet_received_timestamp,
3365
+ ),
3366
+ ("first_request_timestamp", p.first_request_timestamp),
3367
+ ("last_request_timestamp", p.last_request_timestamp),
3368
+ ("last_response_timestamp", p.last_response_timestamp),
3369
+ ("consent_expired_timestamp", p.consent_expired_timestamp),
3370
+ ] {
3371
+ assert_eq!(stamp, t(600), "{field} must report the caller's instant");
3372
+ }
3373
+ }
3374
+
3375
+ let locals = a.get_local_candidates_stats(t(600));
3376
+ assert_eq!(locals.len(), 1);
3377
+ assert_eq!(locals[0].timestamp, t(600));
3378
+
3379
+ let remotes = a.get_remote_candidates_stats(t(600));
3380
+ assert_eq!(remotes.len(), 1);
3381
+ assert_eq!(remotes[0].timestamp, t(600));
3382
+
3383
+ // A second snapshot at a different instant reports that one, with no wall-clock time having
3384
+ // passed between the two calls.
3385
+ assert_eq!(a.get_candidate_pairs_stats(t(900))[0].timestamp, t(900));
3386
+
3387
+ Ok(())
3388
+ }