@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,1662 @@
1
+ //! The Sans-I/O ICE agent.
2
+ //!
3
+ //! An [`Agent`] is given local and remote candidates and inbound datagrams; it produces the
4
+ //! connectivity checks to send, the state transitions to report, and eventually a selected
5
+ //! candidate pair. It owns no sockets and no clock — the caller drives time with
6
+ //! `handle_timeout`.
7
+ //!
8
+ //! Two roles exist: the controlling agent nominates the pair that will carry media, the
9
+ //! controlled agent accepts that choice. Which side controls is decided by which offered, and the
10
+ //! two must not agree — see [`Credentials`] and [`Agent::set_role`].
11
+ #[cfg(test)]
12
+ mod agent_test;
13
+
14
+ /// Configuration for a new [`Agent`]: servers, timeouts and role.
15
+ pub mod agent_config;
16
+ mod agent_proto;
17
+ /// Pair selection and nomination — which candidate pair becomes the selected one.
18
+ pub mod agent_selector;
19
+ /// Snapshot statistics for an agent's candidates and pairs.
20
+ pub mod agent_stats;
21
+
22
+ use agent_config::*;
23
+ use bytes::BytesMut;
24
+ use crypto::{RTCCrypto, RTCCryptoProvider};
25
+ use log::{debug, error, info, trace, warn};
26
+ use mdns::{Mdns, QueryId};
27
+ use sansio::Protocol;
28
+ use std::collections::{HashMap, VecDeque};
29
+ use std::net::{IpAddr, SocketAddr};
30
+ use std::sync::Arc;
31
+ use std::time::{Duration, Instant};
32
+ use stun::attributes::*;
33
+ use stun::fingerprint::*;
34
+ use stun::integrity::*;
35
+ use stun::message::*;
36
+ use stun::textattrs::*;
37
+ use stun::xoraddr::*;
38
+
39
+ use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig;
40
+ use crate::candidate::{candidate_pair::*, *};
41
+ use crate::mdns::{MulticastDnsMode, create_multicast_dns, generate_multicast_dns_name};
42
+ use crate::network_type::NetworkType;
43
+ use crate::rand::*;
44
+ use crate::state::*;
45
+ use crate::tcp_type::TcpType;
46
+ use crate::url::*;
47
+ use shared::error::*;
48
+ use shared::{TaggedBytesMut, TransportContext, TransportProtocol};
49
+
50
+ const ZERO_DURATION: Duration = Duration::from_secs(0);
51
+
52
+ #[derive(Debug, Clone)]
53
+ pub(crate) struct BindingRequest {
54
+ pub(crate) timestamp: Instant,
55
+ pub(crate) transaction_id: TransactionId,
56
+ pub(crate) destination: SocketAddr,
57
+ pub(crate) is_use_candidate: bool,
58
+ }
59
+
60
+ #[derive(Default, Clone)]
61
+ /// The ICE credentials for one side of a session, exchanged in SDP.
62
+ pub struct Credentials {
63
+ /// The username fragment, echoed in every STUN check so a receiver can demultiplex.
64
+ pub ufrag: String,
65
+ /// The password, used as the `MESSAGE-INTEGRITY` key.
66
+ pub pwd: String,
67
+ }
68
+
69
+ #[derive(Default, Clone)]
70
+ pub(crate) struct UfragPwd {
71
+ pub(crate) local_credentials: Credentials,
72
+ pub(crate) remote_credentials: Option<Credentials>,
73
+ /// Credentials generated for an ICE restart that has not been applied yet.
74
+ ///
75
+ /// JSEP requires `createOffer` to be free of side effects, so the offer advertises these
76
+ /// while the live session keeps authenticating with `local_credentials`. They are installed
77
+ /// by [`Agent::apply_restart`] when the local description is set. If the offer is discarded,
78
+ /// they are simply dropped and the existing session is undisturbed.
79
+ pub(crate) pending_local_credentials: Option<Credentials>,
80
+ }
81
+
82
+ fn assert_inbound_username(m: &Message, expected_username: &str) -> Result<()> {
83
+ let mut username = Username::new(ATTR_USERNAME, String::new());
84
+ username.get_from(m)?;
85
+
86
+ if username.to_string() != expected_username {
87
+ return Err(Error::Other(format!(
88
+ "{:?} expected({}) actual({})",
89
+ Error::ErrMismatchUsername,
90
+ expected_username,
91
+ username,
92
+ )));
93
+ }
94
+
95
+ Ok(())
96
+ }
97
+
98
+ fn assert_inbound_message_integrity(
99
+ m: &mut Message,
100
+ key: &[u8],
101
+ crypto: &dyn RTCCrypto,
102
+ ) -> Result<()> {
103
+ MessageIntegrity::check(m, key, crypto)
104
+ }
105
+
106
+ /// What the agent reports to its caller.
107
+ #[non_exhaustive]
108
+ pub enum Event {
109
+ /// The agent's connection state changed.
110
+ ConnectionStateChange(ConnectionState),
111
+ /// A new pair was selected, carrying the local and remote candidates.
112
+ ///
113
+ /// Media should be sent on this pair from now on.
114
+ SelectedCandidatePairChange(Box<Candidate>, Box<Candidate>),
115
+ /// Emitted when the ICE role switches due to a role conflict (RFC 8445 §7.3.1.1).
116
+ /// The bool is `true` if the agent is now controlling, `false` if now controlled.
117
+ RoleChange(bool),
118
+ }
119
+
120
+ /// An [`Event`] together with the instant its condition was observed at.
121
+ ///
122
+ /// The agent's events are produced inside `handle_read` / `handle_timeout` / `close`, all of
123
+ /// which know the time, but they are consumed from `poll_event`, which does not. Carrying the
124
+ /// instant in the payload means the consumer is *told* when the condition happened rather than
125
+ /// having to retain an instant of its own and guess — the same channel `TaggedBytesMut` already
126
+ /// provides on the read path.
127
+ pub struct TaggedEvent {
128
+ /// When the condition this event reports was observed.
129
+ pub now: Instant,
130
+ /// The event itself.
131
+ pub event: Event,
132
+ }
133
+
134
+ /// Represents the ICE agent.
135
+ pub struct Agent {
136
+ pub(crate) crypto_provider: Arc<dyn RTCCryptoProvider>,
137
+ pub(crate) tie_breaker: u64,
138
+ pub(crate) is_controlling: bool,
139
+ pub(crate) lite: bool,
140
+
141
+ pub(crate) start_time: Instant,
142
+
143
+ pub(crate) connection_state: ConnectionState,
144
+ pub(crate) last_connection_state: ConnectionState,
145
+
146
+ //pub(crate) started_ch_tx: Mutex<Option<broadcast::Sender<()>>>,
147
+ pub(crate) ufrag_pwd: UfragPwd,
148
+
149
+ pub(crate) local_candidates: Vec<Candidate>,
150
+ pub(crate) remote_candidates: Vec<Candidate>,
151
+ pub(crate) candidate_pairs: Vec<CandidatePair>,
152
+ pub(crate) nominated_pair: Option<usize>,
153
+ pub(crate) selected_pair: Option<usize>,
154
+
155
+ // desklink patch: the selected pair media keeps using through an ICE
156
+ // restart, until the restarted generation nominates a new one. Held as
157
+ // owned clones because `apply_restart` clears `candidate_pairs`,
158
+ // `local_candidates` and `remote_candidates`, so the old indexes dangle.
159
+ // See DESKLINK-PATCH.md.
160
+ pub(crate) restart_media_pair: Option<(Candidate, Candidate)>,
161
+
162
+ // LRU of outbound Binding request Transaction IDs
163
+ pub(crate) pending_binding_requests: Vec<BindingRequest>,
164
+
165
+ // the following variables won't be changed after init_with_defaults()
166
+ pub(crate) insecure_skip_verify: bool,
167
+ pub(crate) max_binding_requests: u16,
168
+ pub(crate) host_acceptance_min_wait: Duration,
169
+ pub(crate) srflx_acceptance_min_wait: Duration,
170
+ pub(crate) prflx_acceptance_min_wait: Duration,
171
+ pub(crate) relay_acceptance_min_wait: Duration,
172
+ // How long connectivity checks can fail before the ICE Agent
173
+ // goes to disconnected
174
+ pub(crate) disconnected_timeout: Duration,
175
+ // How long connectivity checks can fail before the ICE Agent
176
+ // goes to failed
177
+ pub(crate) failed_timeout: Duration,
178
+ // How often should we send keepalive packets?
179
+ // 0 means never
180
+ pub(crate) keepalive_interval: Duration,
181
+ // When the last STUN consent ping was sent.
182
+ pub(crate) last_consent_sent: Instant,
183
+ // How often should we run our internal taskLoop to check for state changes when connecting
184
+ pub(crate) check_interval: Duration,
185
+ pub(crate) checking_duration: Instant,
186
+ pub(crate) last_checking_time: Instant,
187
+ // When set, a connectivity check has been requested and must be run on the next
188
+ // `handle_timeout`. Checks are deferred (rather than run inline) so that adding a
189
+ // candidate can never re-enter `contact()` and mutate agent state - e.g. transition
190
+ // to `Failed` and drop candidates - while a caller still holds indices into the
191
+ // candidate vectors. See issue #88.
192
+ pub(crate) force_candidate_contact: bool,
193
+
194
+ pub(crate) mdns: Option<Mdns>,
195
+ pub(crate) mdns_queries: HashMap<QueryId, Candidate>,
196
+
197
+ pub(crate) mdns_mode: MulticastDnsMode,
198
+ pub(crate) mdns_local_name: String,
199
+ pub(crate) mdns_local_ip: Option<IpAddr>,
200
+
201
+ pub(crate) candidate_types: Vec<CandidateType>,
202
+ pub(crate) network_types: Vec<NetworkType>,
203
+ pub(crate) urls: Vec<Url>,
204
+
205
+ pub(crate) write_outs: VecDeque<TaggedBytesMut>,
206
+ pub(crate) event_outs: VecDeque<TaggedEvent>,
207
+ }
208
+
209
+ impl Agent {
210
+ /// Creates a new Agent.
211
+ ///
212
+ /// The crypto provider is supplied by the caller; this crate never resolves a default.
213
+ /// Builds an agent whose clock starts at `now`.
214
+ ///
215
+ /// `now` is a constructor argument rather than an `AgentConfig` field because it is a value
216
+ /// consumed once at construction, not a setting that persists for the agent's lifetime — and
217
+ /// because `Instant` has no `Default`, so a config field would force `AgentConfig` to drop
218
+ /// `#[derive(Default)]` and break every `..Default::default()` in its 31 call sites.
219
+ pub fn new(
220
+ now: Instant,
221
+ config: Arc<AgentConfig>,
222
+ crypto_provider: Arc<dyn RTCCryptoProvider>,
223
+ ) -> Result<Self> {
224
+ let tie_breaker = generate_tie_breaker(crypto_provider.random())?;
225
+
226
+ let mut mdns_local_name = config.multicast_dns_local_name.clone();
227
+ if mdns_local_name.is_empty() {
228
+ mdns_local_name = generate_multicast_dns_name();
229
+ }
230
+
231
+ if !mdns_local_name.ends_with(".local") || mdns_local_name.split('.').count() != 2 {
232
+ return Err(Error::ErrInvalidMulticastDnshostName);
233
+ }
234
+
235
+ let mdns_mode = config.multicast_dns_mode;
236
+ let mdns = create_multicast_dns(
237
+ now,
238
+ mdns_mode,
239
+ &mdns_local_name,
240
+ &config.multicast_dns_local_ip,
241
+ &config.multicast_dns_query_timeout,
242
+ )
243
+ .unwrap_or_else(|err| {
244
+ // Opportunistic mDNS: If we can't open the connection, that's ok: we
245
+ // can continue without it.
246
+ warn!("Failed to initialize mDNS {mdns_local_name}: {err}");
247
+ None
248
+ });
249
+
250
+ let candidate_types = if config.candidate_types.is_empty() {
251
+ default_candidate_types()
252
+ } else {
253
+ config.candidate_types.clone()
254
+ };
255
+
256
+ if config.lite && (candidate_types.len() != 1 || candidate_types[0] != CandidateType::Host)
257
+ {
258
+ return Err(Error::ErrLiteUsingNonHostCandidates);
259
+ }
260
+
261
+ if !config.urls.is_empty()
262
+ && !contains_candidate_type(CandidateType::ServerReflexive, &candidate_types)
263
+ && !contains_candidate_type(CandidateType::Relay, &candidate_types)
264
+ {
265
+ return Err(Error::ErrUselessUrlsProvided);
266
+ }
267
+
268
+ let mut agent = Self {
269
+ crypto_provider,
270
+ tie_breaker,
271
+ is_controlling: config.is_controlling,
272
+ lite: config.lite,
273
+
274
+ start_time: now,
275
+
276
+ nominated_pair: None,
277
+ selected_pair: None,
278
+ // desklink patch: nothing is preserved across a restart yet.
279
+ restart_media_pair: None,
280
+ candidate_pairs: vec![],
281
+
282
+ connection_state: ConnectionState::New,
283
+
284
+ insecure_skip_verify: config.insecure_skip_verify,
285
+
286
+ //started_ch_tx: MuteSome(started_ch_tx)),
287
+
288
+ //won't change after init_with_defaults()
289
+ max_binding_requests: if let Some(max_binding_requests) = config.max_binding_requests {
290
+ max_binding_requests
291
+ } else {
292
+ DEFAULT_MAX_BINDING_REQUESTS
293
+ },
294
+ host_acceptance_min_wait: if let Some(host_acceptance_min_wait) =
295
+ config.host_acceptance_min_wait
296
+ {
297
+ host_acceptance_min_wait
298
+ } else {
299
+ DEFAULT_HOST_ACCEPTANCE_MIN_WAIT
300
+ },
301
+ srflx_acceptance_min_wait: if let Some(srflx_acceptance_min_wait) =
302
+ config.srflx_acceptance_min_wait
303
+ {
304
+ srflx_acceptance_min_wait
305
+ } else {
306
+ DEFAULT_SRFLX_ACCEPTANCE_MIN_WAIT
307
+ },
308
+ prflx_acceptance_min_wait: if let Some(prflx_acceptance_min_wait) =
309
+ config.prflx_acceptance_min_wait
310
+ {
311
+ prflx_acceptance_min_wait
312
+ } else {
313
+ DEFAULT_PRFLX_ACCEPTANCE_MIN_WAIT
314
+ },
315
+ relay_acceptance_min_wait: if let Some(relay_acceptance_min_wait) =
316
+ config.relay_acceptance_min_wait
317
+ {
318
+ relay_acceptance_min_wait
319
+ } else {
320
+ DEFAULT_RELAY_ACCEPTANCE_MIN_WAIT
321
+ },
322
+
323
+ // How long connectivity checks can fail before the ICE Agent
324
+ // goes to disconnected
325
+ disconnected_timeout: if let Some(disconnected_timeout) = config.disconnected_timeout {
326
+ disconnected_timeout
327
+ } else {
328
+ DEFAULT_DISCONNECTED_TIMEOUT
329
+ },
330
+
331
+ // How long connectivity checks can fail before the ICE Agent
332
+ // goes to failed
333
+ failed_timeout: if let Some(failed_timeout) = config.failed_timeout {
334
+ failed_timeout
335
+ } else {
336
+ DEFAULT_FAILED_TIMEOUT
337
+ },
338
+
339
+ // How often should we send keepalive packets?
340
+ // 0 means never
341
+ keepalive_interval: if let Some(keepalive_interval) = config.keepalive_interval {
342
+ keepalive_interval
343
+ } else {
344
+ DEFAULT_KEEPALIVE_INTERVAL
345
+ },
346
+
347
+ // How often should we run our internal taskLoop to check for state changes when connecting
348
+ check_interval: if config.check_interval == Duration::from_secs(0) {
349
+ DEFAULT_CHECK_INTERVAL
350
+ } else {
351
+ config.check_interval
352
+ },
353
+ last_consent_sent: now,
354
+ checking_duration: now,
355
+ last_checking_time: now,
356
+ force_candidate_contact: false,
357
+ last_connection_state: ConnectionState::Unspecified,
358
+
359
+ mdns,
360
+ mdns_queries: HashMap::new(),
361
+
362
+ mdns_mode,
363
+ mdns_local_name,
364
+ mdns_local_ip: config.multicast_dns_local_ip,
365
+
366
+ ufrag_pwd: UfragPwd::default(),
367
+
368
+ local_candidates: vec![],
369
+ remote_candidates: vec![],
370
+
371
+ // LRU of outbound Binding request Transaction IDs
372
+ pending_binding_requests: vec![],
373
+
374
+ candidate_types,
375
+ network_types: config.network_types.clone(),
376
+ urls: config.urls.clone(),
377
+
378
+ write_outs: VecDeque::new(),
379
+ event_outs: VecDeque::new(),
380
+ };
381
+
382
+ // Restart is also used to initialize the agent for the first time
383
+ if let Err(err) = agent.restart(
384
+ now,
385
+ config.local_ufrag.clone(),
386
+ config.local_pwd.clone(),
387
+ false,
388
+ ) {
389
+ let _ = agent.close();
390
+ return Err(err);
391
+ }
392
+
393
+ Ok(agent)
394
+ }
395
+
396
+ /// Adds a new local candidate.
397
+ pub fn add_local_candidate(&mut self, mut c: Candidate) -> Result<bool> {
398
+ // Filter by network type if network_types is configured
399
+ if !self.network_types.is_empty() {
400
+ let candidate_network_type = c.network_type();
401
+ if !self.network_types.contains(&candidate_network_type) {
402
+ debug!(
403
+ "Ignoring local candidate with network type {:?} (not in configured network types: {:?})",
404
+ candidate_network_type, self.network_types
405
+ );
406
+ return Ok(false);
407
+ }
408
+ }
409
+
410
+ // Filter by candidate type if candidate_types is configured.
411
+ let candidate_type = c.candidate_type();
412
+ if !self.candidate_types.is_empty() && !self.candidate_types.contains(&candidate_type) {
413
+ debug!(
414
+ "Ignoring local candidate with type {:?} (not in configured candidate types: {:?})",
415
+ candidate_type, self.candidate_types
416
+ );
417
+ return Ok(false);
418
+ }
419
+
420
+ if c.candidate_type() == CandidateType::Host
421
+ && self.mdns_mode == MulticastDnsMode::QueryAndGather
422
+ && c.network_type == NetworkType::Udp4
423
+ && self
424
+ .mdns_local_ip
425
+ .is_some_and(|local_ip| local_ip == c.addr().ip())
426
+ {
427
+ // only one .local mDNS host candidate per IPv4 is supported
428
+ // when registered local ip matches, use mdns_local_name to hide local host ip
429
+ trace!(
430
+ "mDNS hides local ip {} with local name {}",
431
+ c.address, self.mdns_local_name
432
+ );
433
+ c.address = self.mdns_local_name.clone();
434
+ }
435
+
436
+ for cand in &self.local_candidates {
437
+ if cand.equal(&c) {
438
+ return Ok(false);
439
+ }
440
+ }
441
+
442
+ self.local_candidates.push(c);
443
+ let local_index = self.local_candidates.len() - 1;
444
+
445
+ for remote_index in 0..self.remote_candidates.len() {
446
+ if self.local_candidates[local_index]
447
+ .can_pair_with(&self.remote_candidates[remote_index])
448
+ {
449
+ self.add_pair(local_index, remote_index);
450
+ }
451
+ }
452
+
453
+ self.request_connectivity_check();
454
+
455
+ Ok(true)
456
+ }
457
+
458
+ /// Adds a new remote candidate.
459
+ pub fn add_remote_candidate(&mut self, c: Candidate) -> Result<bool> {
460
+ // Filter by network type if network_types is configured
461
+ if !self.network_types.is_empty() {
462
+ let candidate_network_type = c.network_type();
463
+ if !self.network_types.contains(&candidate_network_type) {
464
+ debug!(
465
+ "Ignoring remote candidate with network type {:?} (not in configured network types: {:?})",
466
+ candidate_network_type, self.network_types
467
+ );
468
+ return Ok(false);
469
+ }
470
+ }
471
+
472
+ // TCP active candidates don't have a listening port - they initiate connections.
473
+ // The remote active side will probe our passive candidates, so we don't need
474
+ // to do anything with remote active candidates.
475
+ if c.tcp_type() == TcpType::Active {
476
+ debug!(
477
+ "Ignoring remote candidate with tcptype active: {}",
478
+ c.address()
479
+ );
480
+ return Ok(false);
481
+ }
482
+
483
+ // If we have a mDNS Candidate lets fully resolve it before adding it locally
484
+ if c.candidate_type() == CandidateType::Host && c.address().ends_with(".local") {
485
+ if self.mdns_mode == MulticastDnsMode::Disabled {
486
+ warn!(
487
+ "remote mDNS candidate added, but mDNS is disabled: ({})",
488
+ c.address()
489
+ );
490
+ return Ok(false);
491
+ }
492
+
493
+ if c.candidate_type() != CandidateType::Host {
494
+ return Err(Error::ErrAddressParseFailed);
495
+ }
496
+
497
+ if let Some(mdns_conn) = &mut self.mdns {
498
+ let query_id = mdns_conn.schedule_query(c.address());
499
+ self.mdns_queries.insert(query_id, c);
500
+ }
501
+
502
+ return Ok(false);
503
+ }
504
+
505
+ self.trigger_request_connectivity_check(vec![c]);
506
+ Ok(true)
507
+ }
508
+
509
+ fn trigger_request_connectivity_check(&mut self, remote_candidates: Vec<Candidate>) {
510
+ for c in remote_candidates {
511
+ if !self.remote_candidates.iter().any(|cand| cand.equal(&c)) {
512
+ self.remote_candidates.push(c);
513
+ let remote_index = self.remote_candidates.len() - 1;
514
+
515
+ for local_index in 0..self.local_candidates.len() {
516
+ if self.local_candidates[local_index]
517
+ .can_pair_with(&self.remote_candidates[remote_index])
518
+ {
519
+ self.add_pair(local_index, remote_index);
520
+ }
521
+ }
522
+
523
+ self.request_connectivity_check();
524
+ }
525
+ }
526
+ }
527
+
528
+ /// Sets the credentials of the remote agent.
529
+ pub fn set_remote_credentials(
530
+ &mut self,
531
+ remote_ufrag: String,
532
+ remote_pwd: String,
533
+ ) -> Result<()> {
534
+ if remote_ufrag.is_empty() {
535
+ return Err(Error::ErrRemoteUfragEmpty);
536
+ } else if remote_pwd.is_empty() {
537
+ return Err(Error::ErrRemotePwdEmpty);
538
+ }
539
+
540
+ self.ufrag_pwd.remote_credentials = Some(Credentials {
541
+ ufrag: remote_ufrag,
542
+ pwd: remote_pwd,
543
+ });
544
+
545
+ Ok(())
546
+ }
547
+
548
+ /// Returns the remote credentials.
549
+ pub fn get_remote_credentials(&self) -> Option<&Credentials> {
550
+ self.ufrag_pwd.remote_credentials.as_ref()
551
+ }
552
+
553
+ /// Returns the local credentials.
554
+ /// The credentials to advertise, which is what SDP generation wants.
555
+ ///
556
+ /// Returns credentials staged by [`Agent::generate_restart_credentials`] when an ICE restart
557
+ /// is pending, so an offer carries the new ufrag/pwd. Inbound STUN validation deliberately
558
+ /// does *not* go through here — it reads `local_credentials` directly, so the live session
559
+ /// keeps working until [`Agent::apply_restart`] installs the new pair.
560
+ pub fn get_local_credentials(&self) -> &Credentials {
561
+ self.ufrag_pwd
562
+ .pending_local_credentials
563
+ .as_ref()
564
+ .unwrap_or(&self.ufrag_pwd.local_credentials)
565
+ }
566
+
567
+ /// Whether this agent is the controlling one, which decides nomination.
568
+ pub fn role(&self) -> bool {
569
+ self.is_controlling
570
+ }
571
+
572
+ /// Sets the controlling role.
573
+ ///
574
+ /// Determined by which side offered; the two agents must not agree, or nomination stalls.
575
+ pub fn set_role(&mut self, is_controlling: bool) {
576
+ self.is_controlling = is_controlling;
577
+ }
578
+
579
+ /// The agent's current connection state.
580
+ pub fn state(&self) -> ConnectionState {
581
+ self.connection_state
582
+ }
583
+
584
+ /// Whether a non-STUN datagram on `transport` should be accepted as media.
585
+ ///
586
+ /// Guards against accepting media from an address that has not passed a connectivity check.
587
+ pub fn is_valid_non_stun_traffic(&mut self, now: Instant, transport: TransportContext) -> bool {
588
+ self.find_local_candidate(transport.local_addr, transport.transport_protocol)
589
+ .is_some()
590
+ && self.validate_non_stun_traffic(now, transport.peer_addr)
591
+ }
592
+
593
+ fn get_timeout_interval(&self) -> Duration {
594
+ let (check_interval, keepalive_interval, disconnected_timeout, failed_timeout) = (
595
+ self.check_interval,
596
+ self.keepalive_interval,
597
+ self.disconnected_timeout,
598
+ self.failed_timeout,
599
+ );
600
+ let mut interval = DEFAULT_CHECK_INTERVAL;
601
+
602
+ let mut update_interval = |x: Duration| {
603
+ if x != ZERO_DURATION && (interval == ZERO_DURATION || interval > x) {
604
+ interval = x;
605
+ }
606
+ };
607
+
608
+ match self.last_connection_state {
609
+ ConnectionState::New | ConnectionState::Checking => {
610
+ // While connecting, check candidates more frequently
611
+ update_interval(check_interval);
612
+ }
613
+ ConnectionState::Connected | ConnectionState::Disconnected => {
614
+ update_interval(keepalive_interval);
615
+ }
616
+ _ => {}
617
+ };
618
+ // Ensure we run our task loop as quickly as the minimum of our various configured timeouts
619
+ update_interval(disconnected_timeout);
620
+ update_interval(failed_timeout);
621
+ interval
622
+ }
623
+
624
+ /// Returns the selected pair (local_candidate, remote_candidate) or none
625
+ pub fn get_selected_candidate_pair(&self) -> Option<(&Candidate, &Candidate)> {
626
+ if let Some(pair_index) = self.get_selected_pair() {
627
+ let candidate_pair = &self.candidate_pairs[pair_index];
628
+ Some((
629
+ &self.local_candidates[candidate_pair.local_index],
630
+ &self.remote_candidates[candidate_pair.remote_index],
631
+ ))
632
+ } else if let Some((local, remote)) = &self.restart_media_pair {
633
+ // desklink patch: an ICE restart is in flight and has not nominated
634
+ // its new pair yet, so media keeps flowing on the pair it was on.
635
+ // RFC 8445 keeps the previous selected pair until the new one is
636
+ // validated. The transport layer reads this to stamp outgoing
637
+ // DTLS/SRTP and to pass up inbound packets.
638
+ Some((local, remote))
639
+ } else {
640
+ None
641
+ }
642
+ }
643
+
644
+ /// The highest-priority pair that is still usable, whether or not it has been nominated.
645
+ pub fn get_best_available_candidate_pair(&self) -> Option<(&Candidate, &Candidate)> {
646
+ if let Some(pair_index) = self.get_best_available_pair() {
647
+ let candidate_pair = &self.candidate_pairs[pair_index];
648
+ Some((
649
+ &self.local_candidates[candidate_pair.local_index],
650
+ &self.remote_candidates[candidate_pair.remote_index],
651
+ ))
652
+ } else {
653
+ None
654
+ }
655
+ }
656
+
657
+ /// start connectivity checks
658
+ pub fn start_connectivity_checks(
659
+ &mut self,
660
+ now: Instant,
661
+ is_controlling: bool,
662
+ remote_ufrag: String,
663
+ remote_pwd: String,
664
+ ) -> Result<()> {
665
+ debug!(
666
+ "Started agent: isControlling? {}, remoteUfrag: {}, remotePwd: {}",
667
+ is_controlling, remote_ufrag, remote_pwd
668
+ );
669
+ self.set_remote_credentials(remote_ufrag, remote_pwd)?;
670
+ self.is_controlling = is_controlling;
671
+ self.start(now);
672
+
673
+ self.update_connection_state(Some(now), ConnectionState::Checking);
674
+ self.request_connectivity_check();
675
+
676
+ Ok(())
677
+ }
678
+
679
+ /// Restarts the ICE Agent with the provided ufrag/pwd
680
+ /// If no ufrag/pwd is provided the Agent will generate one itself.
681
+ /// Stages new local credentials for an ICE restart, without disturbing the live session.
682
+ ///
683
+ /// This is the half of an ICE restart that an offerer needs *before* generating SDP: the offer
684
+ /// must carry the new ufrag/pwd. It deliberately does not touch `local_credentials`,
685
+ /// candidate pairs, or connection state, because JSEP requires `createOffer` to be free of
686
+ /// side effects — and because inbound STUN is still being validated against the current
687
+ /// credentials until the local description is applied.
688
+ ///
689
+ /// Empty `ufrag` / `pwd` are filled from the crypto provider's RNG. Call
690
+ /// [`Agent::apply_restart`] to install them.
691
+ pub fn generate_restart_credentials(
692
+ &mut self,
693
+ mut ufrag: String,
694
+ mut pwd: String,
695
+ ) -> Result<()> {
696
+ if ufrag.is_empty() {
697
+ ufrag = generate_ufrag_with_random(self.crypto_provider.random())?;
698
+ }
699
+ if pwd.is_empty() {
700
+ pwd = generate_pwd_with_random(self.crypto_provider.random())?;
701
+ }
702
+
703
+ if ufrag.len() * 8 < 24 {
704
+ return Err(Error::ErrLocalUfragInsufficientBits);
705
+ }
706
+ if pwd.len() * 8 < 128 {
707
+ return Err(Error::ErrLocalPwdInsufficientBits);
708
+ }
709
+
710
+ self.ufrag_pwd.pending_local_credentials = Some(Credentials { ufrag, pwd });
711
+
712
+ Ok(())
713
+ }
714
+
715
+ /// Whether [`Agent::generate_restart_credentials`] has staged a restart that is not yet applied.
716
+ pub fn has_pending_restart(&self) -> bool {
717
+ self.ufrag_pwd.pending_local_credentials.is_some()
718
+ }
719
+
720
+ /// Applies a staged ICE restart, tearing down the old session and starting a new one at `now`.
721
+ ///
722
+ /// Installs the credentials staged by [`Agent::generate_restart_credentials`], if any, then
723
+ /// discards the remote credentials, pending binding requests, candidate pairs and selected
724
+ /// pair, and restarts the agent's timers. With nothing staged, the current credentials are
725
+ /// kept and only the session is restarted.
726
+ pub fn apply_restart(&mut self, now: Instant, keep_local_candidates: bool) -> Result<()> {
727
+ // desklink patch: remember the pair media is on before the teardown
728
+ // below clears it, so the ICE transport keeps sending and receiving on
729
+ // it until the restarted generation nominates a pair (see
730
+ // `get_selected_candidate_pair`). Cleared when a new pair is selected,
731
+ // or when the connection fails or closes.
732
+ let keep_media_on = self
733
+ .get_selected_candidate_pair()
734
+ .map(|(local, remote)| (local.clone(), remote.clone()));
735
+ self.restart_media_pair = keep_media_on;
736
+
737
+ if let Some(credentials) = self.ufrag_pwd.pending_local_credentials.take() {
738
+ self.ufrag_pwd.local_credentials = credentials;
739
+ }
740
+ self.ufrag_pwd.remote_credentials = None;
741
+
742
+ self.pending_binding_requests = vec![];
743
+
744
+ self.candidate_pairs = vec![];
745
+
746
+ self.set_selected_pair(Some(now), None);
747
+ self.delete_all_candidates(keep_local_candidates);
748
+ self.start(now);
749
+
750
+ // Restart is used by NewAgent. Accept/Connect should be used to move to checking
751
+ // for new Agents
752
+ if self.connection_state != ConnectionState::New {
753
+ self.update_connection_state(Some(now), ConnectionState::Checking);
754
+ }
755
+
756
+ Ok(())
757
+ }
758
+
759
+ /// Generates and immediately applies an ICE restart.
760
+ ///
761
+ /// Equivalent to [`Agent::generate_restart_credentials`] followed by
762
+ /// [`Agent::apply_restart`]. A JSEP-conformant offerer wants the two halves separately —
763
+ /// credentials at `createOffer`, application at `setLocalDescription` — but this remains the
764
+ /// right call for anything that restarts in one step.
765
+ pub fn restart(
766
+ &mut self,
767
+ now: Instant,
768
+ ufrag: String,
769
+ pwd: String,
770
+ keep_local_candidates: bool,
771
+ ) -> Result<()> {
772
+ self.generate_restart_credentials(ufrag, pwd)?;
773
+ self.apply_restart(now, keep_local_candidates)
774
+ }
775
+
776
+ /// Returns the local candidates.
777
+ pub fn get_local_candidates(&self) -> &[Candidate] {
778
+ &self.local_candidates
779
+ }
780
+
781
+ /// The remote candidates this agent has been given, in the order they were added.
782
+ pub fn get_remote_candidates(&self) -> &[Candidate] {
783
+ &self.remote_candidates
784
+ }
785
+
786
+ fn contact(&mut self, now: Instant) {
787
+ // Consume any pending deferred-check request now that we are running one.
788
+ // Reset before the early returns below so a failed/settled agent does not
789
+ // keep asking `poll_timeout` for an immediate wake-up.
790
+ self.force_candidate_contact = false;
791
+
792
+ if self.connection_state == ConnectionState::Failed {
793
+ // The connection is currently failed so don't send any checks
794
+ // In the future it may be restarted though
795
+ self.last_connection_state = self.connection_state;
796
+ return;
797
+ }
798
+ if self.connection_state == ConnectionState::Checking {
799
+ // We have just entered checking for the first time so update our checking timer
800
+ if self.last_connection_state != self.connection_state {
801
+ self.checking_duration = now;
802
+ }
803
+
804
+ // We have been in checking longer then Disconnect+Failed timeout, set the connection to Failed
805
+ if self.failed_timeout != ZERO_DURATION
806
+ && now
807
+ .checked_duration_since(self.checking_duration)
808
+ .unwrap_or_else(|| Duration::from_secs(0))
809
+ > self.disconnected_timeout + self.failed_timeout
810
+ {
811
+ self.update_connection_state(Some(now), ConnectionState::Failed);
812
+ self.last_connection_state = self.connection_state;
813
+ return;
814
+ }
815
+ }
816
+
817
+ self.contact_candidates(now);
818
+
819
+ self.last_connection_state = self.connection_state;
820
+ self.last_checking_time = now;
821
+ }
822
+
823
+ pub(crate) fn update_connection_state(
824
+ &mut self,
825
+ now: Option<Instant>,
826
+ new_state: ConnectionState,
827
+ ) {
828
+ if self.connection_state != new_state {
829
+ // Connection has gone to failed, release all gathered candidates
830
+ if new_state == ConnectionState::Failed {
831
+ self.set_selected_pair(now, None);
832
+ self.delete_all_candidates(false);
833
+ }
834
+ // desklink patch: once the connection has failed or closed there is
835
+ // no reachable pair, so media must stop instead of keeping the pair
836
+ // preserved for a restart that will not nominate one.
837
+ if matches!(new_state, ConnectionState::Failed | ConnectionState::Closed) {
838
+ self.restart_media_pair = None;
839
+ }
840
+
841
+ info!(
842
+ "[{}]: Setting new connection state: {}",
843
+ self.get_name(),
844
+ new_state
845
+ );
846
+ self.connection_state = new_state;
847
+ if let Some(now) = now {
848
+ self.event_outs.push_back(TaggedEvent {
849
+ now,
850
+ event: Event::ConnectionStateChange(new_state),
851
+ });
852
+ }
853
+ }
854
+ }
855
+
856
+ pub(crate) fn set_selected_pair(&mut self, now: Option<Instant>, selected_pair: Option<usize>) {
857
+ if let Some(pair_index) = selected_pair {
858
+ trace!(
859
+ "[{}]: Set selected candidate pair: {:?}",
860
+ self.get_name(),
861
+ self.candidate_pairs[pair_index]
862
+ );
863
+
864
+ self.candidate_pairs[pair_index].nominated = true;
865
+ self.selected_pair = Some(pair_index);
866
+ // desklink patch: the restarted generation now has a nominated pair,
867
+ // so media moves onto it and the preserved pair is released.
868
+ self.restart_media_pair = None;
869
+
870
+ self.update_connection_state(now, ConnectionState::Connected);
871
+
872
+ // Notify when the selected pair changes
873
+ let candidate_pair = &self.candidate_pairs[pair_index];
874
+ if let Some(now) = now {
875
+ self.event_outs.push_back(TaggedEvent {
876
+ now,
877
+ event: Event::SelectedCandidatePairChange(
878
+ Box::new(self.local_candidates[candidate_pair.local_index].clone()),
879
+ Box::new(self.remote_candidates[candidate_pair.remote_index].clone()),
880
+ ),
881
+ });
882
+ }
883
+ } else {
884
+ self.selected_pair = None;
885
+ }
886
+ }
887
+
888
+ pub(crate) fn ping_all_candidates(&mut self, now: Instant) {
889
+ let mut pairs: Vec<(usize, usize)> = vec![];
890
+
891
+ let name = self.get_name().to_string();
892
+ if self.candidate_pairs.is_empty() {
893
+ warn!(
894
+ "[{}]: pingAllCandidates called with no candidate pairs. Connection is not possible yet.",
895
+ name,
896
+ );
897
+ }
898
+ for p in &mut self.candidate_pairs {
899
+ if p.state == CandidatePairState::Waiting {
900
+ p.state = CandidatePairState::InProgress;
901
+ } else if p.state != CandidatePairState::InProgress {
902
+ continue;
903
+ }
904
+
905
+ if p.binding_request_count > self.max_binding_requests {
906
+ trace!(
907
+ "[{}]: max requests reached for pair {} (local_addr {} <-> remote_addr {}), marking it as failed",
908
+ name,
909
+ *p,
910
+ self.local_candidates[p.local_index].addr(),
911
+ self.remote_candidates[p.remote_index].addr()
912
+ );
913
+ p.state = CandidatePairState::Failed;
914
+ } else {
915
+ p.binding_request_count += 1;
916
+ let local = p.local_index;
917
+ let remote = p.remote_index;
918
+ pairs.push((local, remote));
919
+ }
920
+ }
921
+
922
+ if !pairs.is_empty() {
923
+ trace!(
924
+ "[{}]: pinging all {} candidates",
925
+ self.get_name(),
926
+ pairs.len()
927
+ );
928
+ }
929
+
930
+ for (local, remote) in pairs {
931
+ self.ping_candidate(now, local, remote);
932
+ }
933
+ }
934
+
935
+ pub(crate) fn add_pair(&mut self, local_index: usize, remote_index: usize) {
936
+ let p = CandidatePair::new(
937
+ local_index,
938
+ remote_index,
939
+ self.local_candidates[local_index].priority(),
940
+ self.remote_candidates[remote_index].priority(),
941
+ self.is_controlling,
942
+ );
943
+ self.candidate_pairs.push(p);
944
+ }
945
+
946
+ pub(crate) fn find_pair(&self, local_index: usize, remote_index: usize) -> Option<usize> {
947
+ for (index, p) in self.candidate_pairs.iter().enumerate() {
948
+ if p.local_index == local_index && p.remote_index == remote_index {
949
+ return Some(index);
950
+ }
951
+ }
952
+ None
953
+ }
954
+
955
+ /// Checks if the selected pair is (still) valid.
956
+ /// Note: the caller should hold the agent lock.
957
+ /// Re-evaluates the selected pair's liveness as of `now`.
958
+ ///
959
+ /// `now` comes from the caller so that this decision and the keepalive decision below are
960
+ /// made against the same instant as the `contact(now)` that reached them.
961
+ pub(crate) fn validate_selected_pair(&mut self, now: Instant) -> bool {
962
+ let (valid, disconnected_time) = {
963
+ self.selected_pair.as_ref().map_or_else(
964
+ || (false, Duration::from_secs(0)),
965
+ |&pair_index| {
966
+ let remote_index = self.candidate_pairs[pair_index].remote_index;
967
+
968
+ let last_received = self.remote_candidates[remote_index]
969
+ .last_received()
970
+ .unwrap_or(self.start_time);
971
+ let disconnected_time = now.saturating_duration_since(last_received);
972
+ (true, disconnected_time)
973
+ },
974
+ )
975
+ };
976
+
977
+ if valid {
978
+ // Only allow transitions to fail if a.failedTimeout is non-zero
979
+ let mut total_time_to_failure = self.failed_timeout;
980
+ if total_time_to_failure != Duration::from_secs(0) {
981
+ total_time_to_failure += self.disconnected_timeout;
982
+ }
983
+
984
+ if total_time_to_failure != Duration::from_secs(0)
985
+ && disconnected_time > total_time_to_failure
986
+ {
987
+ self.update_connection_state(Some(now), ConnectionState::Failed);
988
+ } else if self.disconnected_timeout != Duration::from_secs(0)
989
+ && disconnected_time > self.disconnected_timeout
990
+ {
991
+ self.update_connection_state(Some(now), ConnectionState::Disconnected);
992
+ } else {
993
+ self.update_connection_state(Some(now), ConnectionState::Connected);
994
+ }
995
+ }
996
+
997
+ valid
998
+ }
999
+
1000
+ /// Sends STUN Binding Requests to the selected pair at `keepalive_interval` to
1001
+ /// maintain consent freshness (RFC 7675).
1002
+ pub(crate) fn check_keepalive(&mut self, now: Instant) {
1003
+ let (local_index, remote_index, pair_index) = {
1004
+ self.selected_pair
1005
+ .as_ref()
1006
+ .map_or((None, None, None), |&pair_index| {
1007
+ let p = &self.candidate_pairs[pair_index];
1008
+ (Some(p.local_index), Some(p.remote_index), Some(pair_index))
1009
+ })
1010
+ };
1011
+
1012
+ if let (Some(local_index), Some(remote_index), Some(pair_index)) =
1013
+ (local_index, remote_index, pair_index)
1014
+ && self.keepalive_interval != Duration::from_secs(0)
1015
+ && now.saturating_duration_since(self.last_consent_sent) >= self.keepalive_interval
1016
+ {
1017
+ self.last_consent_sent = now;
1018
+ self.candidate_pairs[pair_index].on_consent_request_sent();
1019
+ self.ping_candidate(now, local_index, remote_index);
1020
+ }
1021
+ }
1022
+
1023
+ fn request_connectivity_check(&mut self) {
1024
+ // Defer the connectivity check to `handle_timeout` instead of running it
1025
+ // inline. Running `contact()` here is re-entrant: it can advance the state
1026
+ // to `Failed`, which calls `delete_all_candidates()` and wipes the candidate
1027
+ // and pair vectors while a caller still holds indices into them (for example
1028
+ // `handle_inbound` while adding a peer-reflexive candidate). See issue #88.
1029
+ self.force_candidate_contact = true;
1030
+ }
1031
+
1032
+ /// Remove all candidates.
1033
+ /// This closes any listening sockets and removes both the local and remote candidate lists.
1034
+ ///
1035
+ /// This is used for restarts, failures and on close.
1036
+ pub(crate) fn delete_all_candidates(&mut self, keep_local_candidates: bool) {
1037
+ if !keep_local_candidates {
1038
+ self.local_candidates.clear();
1039
+ }
1040
+ self.remote_candidates.clear();
1041
+
1042
+ // Candidate pairs reference candidates by index, so once the candidates are
1043
+ // removed every pair - and the `selected_pair`/`nominated_pair` indices into
1044
+ // the pair list - is dangling and must be dropped to avoid out-of-bounds
1045
+ // access. `remote_candidates` is always cleared here, so no pair can remain
1046
+ // valid even when the local candidates are kept. See issue #88.
1047
+ self.candidate_pairs.clear();
1048
+ self.selected_pair = None;
1049
+ self.nominated_pair = None;
1050
+ }
1051
+
1052
+ pub(crate) fn find_remote_candidate(&self, addr: SocketAddr) -> Option<usize> {
1053
+ for (index, c) in self.remote_candidates.iter().enumerate() {
1054
+ if c.addr() == addr {
1055
+ return Some(index);
1056
+ }
1057
+ }
1058
+ None
1059
+ }
1060
+
1061
+ pub(crate) fn find_local_candidate(
1062
+ &self,
1063
+ addr: SocketAddr,
1064
+ transport_protocol: TransportProtocol,
1065
+ ) -> Option<usize> {
1066
+ for (index, c) in self.local_candidates.iter().enumerate() {
1067
+ if c.network_type().to_protocol() != transport_protocol {
1068
+ continue;
1069
+ }
1070
+
1071
+ // For TCP active candidates, match by IP only (ignore port).
1072
+ // TCP active candidates use port 9 as placeholder in signaling,
1073
+ // but the actual connection uses an ephemeral port.
1074
+ if c.tcp_type() == TcpType::Active && transport_protocol == TransportProtocol::TCP {
1075
+ if c.addr().ip() == addr.ip() {
1076
+ return Some(index);
1077
+ }
1078
+ } else if c.addr() == addr {
1079
+ return Some(index);
1080
+ } else if let Some(related_address) = c.related_address()
1081
+ && related_address.address == addr.ip().to_string()
1082
+ && related_address.port == addr.port()
1083
+ {
1084
+ return Some(index);
1085
+ }
1086
+ }
1087
+ None
1088
+ }
1089
+
1090
+ pub(crate) fn send_binding_request(
1091
+ &mut self,
1092
+ now: Instant,
1093
+ m: &Message,
1094
+ local_index: usize,
1095
+ remote_index: usize,
1096
+ ) {
1097
+ trace!(
1098
+ "[{}]: ping STUN from {} to {}",
1099
+ self.get_name(),
1100
+ self.local_candidates[local_index],
1101
+ self.remote_candidates[remote_index],
1102
+ );
1103
+
1104
+ self.invalidate_pending_binding_requests(now);
1105
+
1106
+ self.pending_binding_requests.push(BindingRequest {
1107
+ timestamp: now,
1108
+ transaction_id: m.transaction_id,
1109
+ destination: self.remote_candidates[remote_index].addr(),
1110
+ is_use_candidate: m.contains(ATTR_USE_CANDIDATE),
1111
+ });
1112
+
1113
+ // Track request sent on the candidate pair
1114
+ if let Some(pair_index) = self.find_pair(local_index, remote_index) {
1115
+ self.candidate_pairs[pair_index].on_request_sent();
1116
+ }
1117
+
1118
+ self.send_stun(now, m, local_index, remote_index);
1119
+ }
1120
+
1121
+ pub(crate) fn send_binding_success(
1122
+ &mut self,
1123
+ now: Instant,
1124
+ m: &Message,
1125
+ local_index: usize,
1126
+ remote_index: usize,
1127
+ ) {
1128
+ let addr = self.remote_candidates[remote_index].addr();
1129
+ let (ip, port) = (addr.ip(), addr.port());
1130
+ let local_pwd = self.ufrag_pwd.local_credentials.pwd.clone();
1131
+
1132
+ let (out, result) = {
1133
+ let mut out = Message::new();
1134
+ let result = out.build(&[
1135
+ Box::new(m.clone()),
1136
+ Box::new(BINDING_SUCCESS),
1137
+ Box::new(XorMappedAddress { ip, port }),
1138
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
1139
+ local_pwd,
1140
+ self.crypto_provider.crypto(),
1141
+ )),
1142
+ Box::new(FINGERPRINT),
1143
+ ]);
1144
+ (out, result)
1145
+ };
1146
+
1147
+ if let Err(err) = result {
1148
+ warn!(
1149
+ "[{}]: Failed to handle inbound ICE from: {} to: {} error: {}",
1150
+ self.get_name(),
1151
+ self.local_candidates[local_index],
1152
+ self.remote_candidates[remote_index],
1153
+ err
1154
+ );
1155
+ } else {
1156
+ // Track response sent on the candidate pair
1157
+ if let Some(pair_index) = self.find_pair(local_index, remote_index) {
1158
+ self.candidate_pairs[pair_index].on_response_sent();
1159
+ }
1160
+ self.send_stun(now, &out, local_index, remote_index);
1161
+ }
1162
+ }
1163
+
1164
+ /// Sends a 487 (Role Conflict) error response.
1165
+ /// RFC 8445 Section 7.3.1.1
1166
+ pub(crate) fn send_role_conflict_error(
1167
+ &mut self,
1168
+ now: Instant,
1169
+ m: &Message,
1170
+ local_index: usize,
1171
+ remote_index: usize,
1172
+ ) {
1173
+ use stun::error_code::*;
1174
+
1175
+ let local_pwd = self.ufrag_pwd.local_credentials.pwd.clone();
1176
+
1177
+ let (out, result) = {
1178
+ let mut out = Message::new();
1179
+ let result = out.build(&[
1180
+ Box::new(m.clone()),
1181
+ Box::new(stun::message::BINDING_ERROR),
1182
+ Box::new(CODE_ROLE_CONFLICT),
1183
+ Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
1184
+ local_pwd,
1185
+ self.crypto_provider.crypto(),
1186
+ )),
1187
+ Box::new(FINGERPRINT),
1188
+ ]);
1189
+ (out, result)
1190
+ };
1191
+
1192
+ if let Err(err) = result {
1193
+ warn!(
1194
+ "[{}]: Failed to send role conflict error from: {} to: {} error: {}",
1195
+ self.get_name(),
1196
+ self.local_candidates[local_index],
1197
+ self.remote_candidates[remote_index],
1198
+ err
1199
+ );
1200
+ } else {
1201
+ debug!(
1202
+ "[{}]: Sent 487 Role Conflict error from {} to {}",
1203
+ self.get_name(),
1204
+ self.local_candidates[local_index],
1205
+ self.remote_candidates[remote_index]
1206
+ );
1207
+ self.send_stun(now, &out, local_index, remote_index);
1208
+ }
1209
+ }
1210
+
1211
+ /// Switches the ICE agent role and recomputes all candidate pair priorities.
1212
+ /// RFC 8445 Section 7.3.1.1
1213
+ pub(crate) fn switch_role(&mut self, now: Instant) {
1214
+ self.is_controlling = !self.is_controlling;
1215
+
1216
+ // Recompute priorities for all candidate pairs
1217
+ // The priority calculation depends on ice_role_controlling
1218
+ for pair in &mut self.candidate_pairs {
1219
+ pair.ice_role_controlling = self.is_controlling;
1220
+ }
1221
+
1222
+ // Clear nominated pair when switching roles
1223
+ self.nominated_pair = None;
1224
+
1225
+ info!(
1226
+ "[{}]: Role switched, recomputed {} candidate pair priorities",
1227
+ self.get_name(),
1228
+ self.candidate_pairs.len()
1229
+ );
1230
+
1231
+ self.event_outs.push_back(TaggedEvent {
1232
+ now,
1233
+ event: Event::RoleChange(self.is_controlling),
1234
+ });
1235
+ }
1236
+
1237
+ /// Removes pending binding requests that are over `maxBindingRequestTimeout` old Let HTO be the
1238
+ /// transaction timeout, which SHOULD be 2*RTT if RTT is known or 500 ms otherwise.
1239
+ ///
1240
+ /// reference: (IETF ref-8445)[https://tools.ietf.org/html/rfc8445#appendix-B.1].
1241
+ pub(crate) fn invalidate_pending_binding_requests(&mut self, filter_time: Instant) {
1242
+ let pending_binding_requests = &mut self.pending_binding_requests;
1243
+ let initial_size = pending_binding_requests.len();
1244
+
1245
+ let mut temp = vec![];
1246
+ for binding_request in pending_binding_requests.drain(..) {
1247
+ if filter_time
1248
+ .checked_duration_since(binding_request.timestamp)
1249
+ .map(|duration| duration < MAX_BINDING_REQUEST_TIMEOUT)
1250
+ .unwrap_or(true)
1251
+ {
1252
+ temp.push(binding_request);
1253
+ }
1254
+ }
1255
+
1256
+ *pending_binding_requests = temp;
1257
+ let bind_requests_remaining = pending_binding_requests.len();
1258
+ let bind_requests_removed = initial_size - bind_requests_remaining;
1259
+ if bind_requests_removed > 0 {
1260
+ trace!(
1261
+ "[{}]: Discarded {} binding requests because they expired, still {} remaining",
1262
+ self.get_name(),
1263
+ bind_requests_removed,
1264
+ bind_requests_remaining,
1265
+ );
1266
+ }
1267
+ }
1268
+
1269
+ /// Assert that the passed `TransactionID` is in our `pendingBindingRequests` and returns the
1270
+ /// destination, If the bindingRequest was valid remove it from our pending cache.
1271
+ pub(crate) fn handle_inbound_binding_success(
1272
+ &mut self,
1273
+ now: Instant,
1274
+ id: TransactionId,
1275
+ ) -> Option<BindingRequest> {
1276
+ self.invalidate_pending_binding_requests(now);
1277
+
1278
+ let pending_binding_requests = &mut self.pending_binding_requests;
1279
+ for i in 0..pending_binding_requests.len() {
1280
+ if pending_binding_requests[i].transaction_id == id {
1281
+ let valid_binding_request = pending_binding_requests.remove(i);
1282
+ return Some(valid_binding_request);
1283
+ }
1284
+ }
1285
+ None
1286
+ }
1287
+
1288
+ /// Processes STUN traffic from a remote candidate.
1289
+ pub(crate) fn handle_inbound(
1290
+ &mut self,
1291
+ now: Instant,
1292
+ m: &mut Message,
1293
+ local_index: usize,
1294
+ remote_addr: SocketAddr,
1295
+ ) -> Result<()> {
1296
+ if m.typ.method != METHOD_BINDING
1297
+ || !(m.typ.class == CLASS_SUCCESS_RESPONSE
1298
+ || m.typ.class == CLASS_REQUEST
1299
+ || m.typ.class == CLASS_INDICATION)
1300
+ {
1301
+ trace!(
1302
+ "[{}]: unhandled STUN from {} to {} class({}) method({})",
1303
+ self.get_name(),
1304
+ remote_addr,
1305
+ self.local_candidates[local_index],
1306
+ m.typ.class,
1307
+ m.typ.method
1308
+ );
1309
+ return Err(Error::ErrUnhandledStunpacket);
1310
+ }
1311
+
1312
+ // RFC 8445 Section 7.3.1.1 - Detecting and Repairing Role Conflicts
1313
+ if self.is_controlling {
1314
+ if m.contains(ATTR_ICE_CONTROLLING) {
1315
+ // Both agents are controlling - role conflict detected
1316
+ let mut remote_controlling = crate::attributes::control::AttrControlling::default();
1317
+ if let Err(err) = remote_controlling.get_from(m) {
1318
+ warn!(
1319
+ "[{}]: Failed to get remote ICE-CONTROLLING attribute: {}",
1320
+ self.get_name(),
1321
+ err
1322
+ );
1323
+ return Err(err);
1324
+ }
1325
+
1326
+ debug!(
1327
+ "[{}]: Role conflict detected (both controlling), local tiebreaker: {}, remote tiebreaker: {}",
1328
+ self.get_name(),
1329
+ self.tie_breaker,
1330
+ remote_controlling.0
1331
+ );
1332
+
1333
+ // Only process if this is a request (not a response)
1334
+ if m.typ.class == CLASS_REQUEST {
1335
+ // Send 487 Role Conflict error
1336
+ if let Some(remote_index) = self.find_remote_candidate(remote_addr) {
1337
+ self.send_role_conflict_error(now, m, local_index, remote_index);
1338
+ }
1339
+
1340
+ // Compare tiebreakers - if ours is smaller, we switch to controlled
1341
+ if self.tie_breaker < remote_controlling.0 {
1342
+ info!(
1343
+ "[{}]: Switching from controlling to controlled due to role conflict (smaller tiebreaker)",
1344
+ self.get_name()
1345
+ );
1346
+ self.switch_role(now);
1347
+ }
1348
+ }
1349
+ // Continue processing the message after handling role conflict
1350
+ } else if m.contains(ATTR_USE_CANDIDATE) {
1351
+ debug!(
1352
+ "[{}]: useCandidate && a.isControlling == true",
1353
+ self.get_name(),
1354
+ );
1355
+ return Err(Error::ErrUnexpectedStunrequestMessage);
1356
+ }
1357
+ } else if m.contains(ATTR_ICE_CONTROLLED) {
1358
+ // Both agents are controlled - role conflict detected
1359
+ let mut remote_controlled = crate::attributes::control::AttrControlled::default();
1360
+ if let Err(err) = remote_controlled.get_from(m) {
1361
+ warn!(
1362
+ "[{}]: Failed to get remote ICE-CONTROLLED attribute: {}",
1363
+ self.get_name(),
1364
+ err
1365
+ );
1366
+ return Err(err);
1367
+ }
1368
+
1369
+ debug!(
1370
+ "[{}]: Role conflict detected (both controlled), local tiebreaker: {}, remote tiebreaker: {}",
1371
+ self.get_name(),
1372
+ self.tie_breaker,
1373
+ remote_controlled.0
1374
+ );
1375
+
1376
+ // Only process if this is a request (not a response)
1377
+ if m.typ.class == CLASS_REQUEST {
1378
+ // Send 487 Role Conflict error
1379
+ if let Some(remote_index) = self.find_remote_candidate(remote_addr) {
1380
+ self.send_role_conflict_error(now, m, local_index, remote_index);
1381
+ }
1382
+
1383
+ // Compare tiebreakers - if ours is larger, we switch to controlling
1384
+ if self.tie_breaker > remote_controlled.0 {
1385
+ info!(
1386
+ "[{}]: Switching from controlled to controlling due to role conflict (larger tiebreaker)",
1387
+ self.get_name()
1388
+ );
1389
+ self.switch_role(now);
1390
+ }
1391
+ }
1392
+ // Continue processing the message after handling role conflict
1393
+ }
1394
+
1395
+ let Some(remote_credentials) = &self.ufrag_pwd.remote_credentials else {
1396
+ debug!(
1397
+ "[{}]: ufrag_pwd.remote_credentials.is_none",
1398
+ self.get_name(),
1399
+ );
1400
+ return Err(Error::ErrPasswordEmpty);
1401
+ };
1402
+
1403
+ let mut remote_candidate_index = self.find_remote_candidate(remote_addr);
1404
+ if m.typ.class == CLASS_SUCCESS_RESPONSE {
1405
+ if let Err(err) = assert_inbound_message_integrity(
1406
+ m,
1407
+ remote_credentials.pwd.as_bytes(),
1408
+ self.crypto_provider.crypto(),
1409
+ ) {
1410
+ warn!(
1411
+ "[{}]: discard message from ({}), {}",
1412
+ self.get_name(),
1413
+ remote_addr,
1414
+ err
1415
+ );
1416
+ return Err(err);
1417
+ }
1418
+
1419
+ if let Some(remote_index) = &remote_candidate_index {
1420
+ self.handle_success_response(now, m, local_index, *remote_index, remote_addr);
1421
+ } else {
1422
+ warn!(
1423
+ "[{}]: discard success message from ({}), no such remote",
1424
+ self.get_name(),
1425
+ remote_addr
1426
+ );
1427
+ return Err(Error::ErrUnhandledStunpacket);
1428
+ }
1429
+ } else if m.typ.class == CLASS_REQUEST {
1430
+ {
1431
+ let username = self.ufrag_pwd.local_credentials.ufrag.clone()
1432
+ + ":"
1433
+ + remote_credentials.ufrag.as_str();
1434
+ if let Err(err) = assert_inbound_username(m, &username) {
1435
+ warn!(
1436
+ "[{}]: discard message from ({}), {}",
1437
+ self.get_name(),
1438
+ remote_addr,
1439
+ err
1440
+ );
1441
+ return Err(err);
1442
+ } else if let Err(err) = assert_inbound_message_integrity(
1443
+ m,
1444
+ self.ufrag_pwd.local_credentials.pwd.as_bytes(),
1445
+ self.crypto_provider.crypto(),
1446
+ ) {
1447
+ warn!(
1448
+ "[{}]: discard message from ({}), {}",
1449
+ self.get_name(),
1450
+ remote_addr,
1451
+ err
1452
+ );
1453
+ return Err(err);
1454
+ }
1455
+ }
1456
+
1457
+ if remote_candidate_index.is_none() {
1458
+ // Use the local candidate's network type for the peer-reflexive candidate
1459
+ let network_type = self.local_candidates[local_index].network_type();
1460
+ let (ip, port) = (remote_addr.ip(), remote_addr.port());
1461
+
1462
+ let prflx_candidate_config = CandidatePeerReflexiveConfig {
1463
+ base_config: CandidateConfig {
1464
+ network: network_type.to_string(),
1465
+ address: ip.to_string(),
1466
+ port,
1467
+ component: self.local_candidates[local_index].component(),
1468
+ ..CandidateConfig::default()
1469
+ },
1470
+ rel_addr: "".to_owned(),
1471
+ rel_port: 0,
1472
+ };
1473
+
1474
+ match prflx_candidate_config.new_candidate_peer_reflexive() {
1475
+ Ok(prflx_candidate) => {
1476
+ if let Ok(added) = self.add_remote_candidate(prflx_candidate)
1477
+ && added
1478
+ {
1479
+ // Look the candidate up by address rather than assuming it
1480
+ // is the last element: `add_remote_candidate` may not have
1481
+ // appended it (e.g. a duplicate), and this stays correct if
1482
+ // the vector is ever mutated underneath us. See issue #88.
1483
+ remote_candidate_index = self.find_remote_candidate(remote_addr);
1484
+ }
1485
+ }
1486
+ Err(err) => {
1487
+ error!(
1488
+ "[{}]: Failed to create new remote prflx candidate ({})",
1489
+ self.get_name(),
1490
+ err
1491
+ );
1492
+ return Err(err);
1493
+ }
1494
+ };
1495
+
1496
+ debug!(
1497
+ "[{}]: adding a new peer-reflexive candidate: {} ",
1498
+ self.get_name(),
1499
+ remote_addr
1500
+ );
1501
+ }
1502
+
1503
+ trace!(
1504
+ "[{}]: inbound STUN (Request) from {} to {}",
1505
+ self.get_name(),
1506
+ remote_addr,
1507
+ self.local_candidates[local_index]
1508
+ );
1509
+
1510
+ if let Some(remote_index) = &remote_candidate_index {
1511
+ self.handle_binding_request(now, m, local_index, *remote_index);
1512
+ }
1513
+ }
1514
+
1515
+ if let Some(remote_index) = remote_candidate_index {
1516
+ self.remote_candidates[remote_index].seen(now, false);
1517
+ }
1518
+
1519
+ Ok(())
1520
+ }
1521
+
1522
+ // Processes non STUN traffic from a remote candidate, and returns true if it is an actual
1523
+ // remote candidate.
1524
+ pub(crate) fn validate_non_stun_traffic(
1525
+ &mut self,
1526
+ now: Instant,
1527
+ remote_addr: SocketAddr,
1528
+ ) -> bool {
1529
+ self.find_remote_candidate(remote_addr)
1530
+ .is_some_and(|remote_index| {
1531
+ self.remote_candidates[remote_index].seen(now, false);
1532
+ true
1533
+ })
1534
+ }
1535
+
1536
+ pub(crate) fn send_stun(
1537
+ &mut self,
1538
+ now: Instant,
1539
+ msg: &Message,
1540
+ local_index: usize,
1541
+ remote_index: usize,
1542
+ ) {
1543
+ let peer_addr = self.remote_candidates[remote_index].addr();
1544
+ // RFC 8445 §6.1.2: checks for a (server/peer-)reflexive candidate must
1545
+ // be sent from its base, the bound local socket the candidate was
1546
+ // derived from; the mapped address is not a local socket.
1547
+ let local_addr = self.local_candidates[local_index].base_addr();
1548
+ let transport_protocol = if self.local_candidates[local_index].network_type().is_tcp() {
1549
+ TransportProtocol::TCP
1550
+ } else {
1551
+ TransportProtocol::UDP
1552
+ };
1553
+
1554
+ self.write_outs.push_back(TaggedBytesMut {
1555
+ now,
1556
+ transport: TransportContext {
1557
+ local_addr,
1558
+ peer_addr,
1559
+ ecn: None,
1560
+ transport_protocol,
1561
+ },
1562
+ message: BytesMut::from(&msg.raw[..]),
1563
+ });
1564
+
1565
+ self.local_candidates[local_index].seen(now, true);
1566
+ }
1567
+
1568
+ fn handle_inbound_candidate_msg(
1569
+ &mut self,
1570
+ local_index: usize,
1571
+ msg: TaggedBytesMut,
1572
+ ) -> Result<()> {
1573
+ if is_stun_message(&msg.message) {
1574
+ let mut m = Message {
1575
+ raw: msg.message.to_vec(),
1576
+ ..Message::default()
1577
+ };
1578
+
1579
+ if let Err(err) = m.decode() {
1580
+ warn!(
1581
+ "[{}]: Failed to handle decode ICE from {} to {}: {}",
1582
+ self.get_name(),
1583
+ msg.transport.local_addr,
1584
+ msg.transport.peer_addr,
1585
+ err
1586
+ );
1587
+ Err(err)
1588
+ } else {
1589
+ self.handle_inbound(msg.now, &mut m, local_index, msg.transport.peer_addr)
1590
+ }
1591
+ } else {
1592
+ if !self.validate_non_stun_traffic(msg.now, msg.transport.peer_addr) {
1593
+ warn!(
1594
+ "[{}]: Discarded message, not a valid remote candidate from {}",
1595
+ self.get_name(),
1596
+ msg.transport.peer_addr,
1597
+ );
1598
+ } else {
1599
+ warn!(
1600
+ "[{}]: non-STUN traffic message from a valid remote candidate from {}",
1601
+ self.get_name(),
1602
+ msg.transport.peer_addr
1603
+ );
1604
+ }
1605
+ Err(Error::ErrNonStunmessage)
1606
+ }
1607
+ }
1608
+
1609
+ pub(crate) fn get_name(&self) -> &str {
1610
+ if self.is_controlling {
1611
+ "controlling"
1612
+ } else {
1613
+ "controlled"
1614
+ }
1615
+ }
1616
+
1617
+ pub(crate) fn get_selected_pair(&self) -> Option<usize> {
1618
+ self.selected_pair
1619
+ }
1620
+
1621
+ pub(crate) fn get_best_available_pair(&self) -> Option<usize> {
1622
+ let mut best_pair_index: Option<usize> = None;
1623
+
1624
+ for (index, p) in self.candidate_pairs.iter().enumerate() {
1625
+ if p.state == CandidatePairState::Failed {
1626
+ continue;
1627
+ }
1628
+
1629
+ if let Some(pair_index) = &mut best_pair_index {
1630
+ let b = &self.candidate_pairs[*pair_index];
1631
+ if b.priority() < p.priority() {
1632
+ *pair_index = index;
1633
+ }
1634
+ } else {
1635
+ best_pair_index = Some(index);
1636
+ }
1637
+ }
1638
+
1639
+ best_pair_index
1640
+ }
1641
+
1642
+ pub(crate) fn get_best_valid_candidate_pair(&self) -> Option<usize> {
1643
+ let mut best_pair_index: Option<usize> = None;
1644
+
1645
+ for (index, p) in self.candidate_pairs.iter().enumerate() {
1646
+ if p.state != CandidatePairState::Succeeded {
1647
+ continue;
1648
+ }
1649
+
1650
+ if let Some(pair_index) = &mut best_pair_index {
1651
+ let b = &self.candidate_pairs[*pair_index];
1652
+ if b.priority() < p.priority() {
1653
+ *pair_index = index;
1654
+ }
1655
+ } else {
1656
+ best_pair_index = Some(index);
1657
+ }
1658
+ }
1659
+
1660
+ best_pair_index
1661
+ }
1662
+ }