1use super::coordination::room::{GatewayRequiredRoomBackend, RoomBackend};
2use crate::signaling::{GatewayRequiredSignalingBackend, SignalingBackend};
3use iroh::Endpoint;
4use iroh_tickets::endpoint::EndpointTicket;
5use std::collections::{HashMap, HashSet};
6use std::str::FromStr;
7#[cfg(any(
8 not(target_arch = "wasm32"),
9 feature = "transport-webrtc",
10 feature = "transport-moq"
11))]
12use std::sync::atomic::Ordering;
13use std::sync::atomic::{AtomicBool, AtomicU64};
14use std::sync::{Arc, Mutex, RwLock as StdRwLock};
15use tokio::sync::RwLock;
16
17#[cfg(not(target_arch = "wasm32"))]
18use crate::client::scope_classifier::{default_classifier, ScopeClassifier};
19
20#[cfg(target_arch = "wasm32")]
21fn wasm_init_log(stage: &str) {
22 let now = js_sys::Date::now();
23 web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
24 "[OPENRTC][WASM-INIT] ts_ms={:.0} stage={}",
25 now, stage
26 )));
27}
28
29#[cfg(target_arch = "wasm32")]
30use crate::wasm_node::{AcceptEvent, ConnectEvent, IrohWasmNode};
31
32#[cfg(not(target_arch = "wasm32"))]
33use crate::native_node::{AcceptEvent, ConnectEvent, IncomingStream, IrohNativeNode};
34
35#[cfg(not(target_arch = "wasm32"))]
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct EndpointHandle {
39 pub node_id: String,
40 pub node_addr: String,
41}
42
43#[cfg(not(target_arch = "wasm32"))]
44pub struct BiStream {
45 pub send: crate::application_crypto_streams::PeerSendStream,
46 pub recv: crate::application_crypto_streams::PeerRecvStream,
47 pub id: String,
48}
49
50#[cfg(not(target_arch = "wasm32"))]
54pub enum BiStreamDisposition {
55 Consumed,
56 Forward {
57 send: iroh::endpoint::SendStream,
58 recv: iroh::endpoint::RecvStream,
59 recv_prefix: Vec<u8>,
64 },
65}
66
67#[cfg(not(target_arch = "wasm32"))]
68#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct NativePeerDataEvent {
71 pub connection_id: String,
72 pub remote_node_id: Option<String>,
73 pub transport: String,
74 pub transport_stable_id: u64,
75 pub transport_generation: u64,
76 pub route_generation: u64,
77 pub payload: Vec<u8>,
78}
79
80#[cfg(not(target_arch = "wasm32"))]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub(crate) struct NativePeerDataGeneration {
84 pub transport_stable_id: u64,
85 pub transport_generation: u64,
86 pub route_generation: u64,
87}
88
89#[cfg(target_arch = "wasm32")]
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub(crate) struct WasmPeerDataGeneration {
92 pub transport_stable_id: u64,
93 pub transport_generation: u64,
94 pub route_generation: u64,
95}
96
97#[cfg(not(target_arch = "wasm32"))]
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub(crate) struct NativeBleUpgradeAttemptState {
100 pub generation: NativePeerDataGeneration,
101 pub attempts: u8,
102}
103
104#[cfg(not(target_arch = "wasm32"))]
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub(crate) struct NativeTransportUpgradeGate {
107 pub upgrade_id: String,
108 pub generation: NativePeerDataGeneration,
109 pub policy_epoch: u64,
110 pub peer_policy_epoch: u64,
111}
112
113#[cfg(target_arch = "wasm32")]
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub(crate) struct WasmTransportUpgradeGate {
116 pub upgrade_id: String,
117 pub generation: WasmPeerDataGeneration,
121 pub policy_epoch: u64,
122 pub peer_policy_epoch: u64,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[cfg(any(
131 test,
132 all(
133 target_arch = "wasm32",
134 any(feature = "transport-webrtc", feature = "transport-moq")
135 )
136))]
137pub(crate) enum WasmCarrierUpgradeReservation {
138 Reserved,
139 CurrentReplay,
140 Busy,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145#[cfg(any(
146 test,
147 all(
148 target_arch = "wasm32",
149 any(feature = "transport-webrtc", feature = "transport-moq")
150 )
151))]
152pub(crate) enum WasmCarrierUpgradeFence {
153 Current(u64),
154 Retired,
155 Superseded,
156 PolicyChanged,
157 BaseGenerationChanged,
158}
159
160#[cfg(any(
164 test,
165 all(
166 target_arch = "wasm32",
167 any(feature = "transport-webrtc", feature = "transport-moq")
168 )
169))]
170pub(crate) fn wasm_peer_carrier_failure_code(failure_code: Option<&str>) -> &'static str {
171 match failure_code {
172 Some("carrier-base-generation-stale") => "carrier-base-generation-stale",
173 Some("carrier-base-not-ready") => "carrier-base-not-ready",
174 _ => "peer-rejected-carrier",
175 }
176}
177
178#[cfg(any(
182 test,
183 all(
184 target_arch = "wasm32",
185 any(feature = "transport-webrtc", feature = "transport-moq")
186 )
187))]
188pub(crate) fn should_rearm_wasm_carrier_event_retry(
189 failure_code: &str,
190 initiator: bool,
191 completion_started: bool,
192) -> bool {
193 initiator && !completion_started && failure_code == "carrier-base-not-ready"
194}
195
196#[cfg(any(
200 test,
201 all(
202 target_arch = "wasm32",
203 any(feature = "transport-webrtc", feature = "transport-moq")
204 )
205))]
206pub(crate) fn classify_wasm_carrier_upgrade_reservation(
207 current: Option<(&str, (u64, u64, u64), u64)>,
208 competing_kind_current: bool,
209 upgrade_id: &str,
210 generation: (u64, u64, u64),
211 policy_epoch: u64,
212) -> WasmCarrierUpgradeReservation {
213 if competing_kind_current {
214 return WasmCarrierUpgradeReservation::Busy;
215 }
216 match current {
217 Some((current_id, current_generation, current_policy_epoch))
218 if current_generation == generation
219 && current_id == upgrade_id
220 && current_policy_epoch == policy_epoch =>
221 {
222 WasmCarrierUpgradeReservation::CurrentReplay
223 }
224 Some((_, current_generation, _)) if current_generation == generation => {
225 WasmCarrierUpgradeReservation::Busy
226 }
227 _ => WasmCarrierUpgradeReservation::Reserved,
228 }
229}
230
231#[cfg(any(
232 test,
233 all(
234 target_arch = "wasm32",
235 any(feature = "transport-webrtc", feature = "transport-moq")
236 )
237))]
238pub(crate) fn classify_wasm_carrier_upgrade_fence(
239 current: Option<(&str, (u64, u64, u64), u64)>,
240 upgrade_id: &str,
241 generation: (u64, u64, u64),
242 policy_epoch: u64,
243) -> WasmCarrierUpgradeFence {
244 match current {
245 None => WasmCarrierUpgradeFence::Retired,
246 Some((_, current_generation, _)) if current_generation != generation => {
247 WasmCarrierUpgradeFence::BaseGenerationChanged
248 }
249 Some((current_id, _, _)) if current_id != upgrade_id => WasmCarrierUpgradeFence::Superseded,
250 Some((_, _, current_policy_epoch)) if current_policy_epoch != policy_epoch => {
251 WasmCarrierUpgradeFence::PolicyChanged
252 }
253 Some((_, _, current_policy_epoch)) => {
254 WasmCarrierUpgradeFence::Current(current_policy_epoch)
255 }
256 }
257}
258
259#[cfg(any(test, feature = "iroh-carrier-core"))]
263pub(crate) fn should_bump_remote_carrier_peer_policy_epoch(
264 capabilities_were_known: bool,
265 capabilities_changed: bool,
266 matching_first_attempt: bool,
267) -> bool {
268 capabilities_changed && (capabilities_were_known || !matching_first_attempt)
269}
270
271#[cfg(test)]
272mod wasm_carrier_upgrade_gate_tests {
273 use super::*;
274
275 const BASE: (u64, u64, u64) = (7, 11, 13);
276
277 #[test]
278 fn delayed_proof_duplicate_replay_keeps_the_original_attempt_fenced() {
279 let current = Some(("attempt-a", BASE, 23));
280
281 assert_eq!(
282 classify_wasm_carrier_upgrade_reservation(current, false, "attempt-a", BASE, 23),
283 WasmCarrierUpgradeReservation::CurrentReplay,
284 );
285 assert_eq!(
286 classify_wasm_carrier_upgrade_reservation(current, false, "attempt-b", BASE, 23),
287 WasmCarrierUpgradeReservation::Busy,
288 );
289 assert_eq!(
290 classify_wasm_carrier_upgrade_fence(current, "attempt-a", BASE, 23),
291 WasmCarrierUpgradeFence::Current(23),
292 );
293 }
294
295 #[test]
296 fn another_carrier_cannot_overtake_a_proving_browser_generation() {
297 assert_eq!(
298 classify_wasm_carrier_upgrade_reservation(None, true, "webrtc-attempt", BASE, 23,),
299 WasmCarrierUpgradeReservation::Busy,
300 );
301 }
302
303 #[test]
304 fn remote_capability_epoch_preserves_only_a_matching_first_observation() {
305 assert!(!should_bump_remote_carrier_peer_policy_epoch(
306 false, true, true,
307 ));
308 assert!(should_bump_remote_carrier_peer_policy_epoch(
309 false, true, false,
310 ));
311 assert!(should_bump_remote_carrier_peer_policy_epoch(
312 true, true, true,
313 ));
314 assert!(!should_bump_remote_carrier_peer_policy_epoch(
315 true, false, false,
316 ));
317 }
318
319 #[test]
320 fn peer_base_readiness_failure_remains_recoverable() {
321 assert_eq!(
322 wasm_peer_carrier_failure_code(Some("carrier-base-not-ready")),
323 "carrier-base-not-ready",
324 );
325 assert_eq!(
326 wasm_peer_carrier_failure_code(Some("carrier-base-generation-stale")),
327 "carrier-base-generation-stale",
328 );
329 assert_eq!(
330 wasm_peer_carrier_failure_code(Some("carrier-authorization-rejected")),
331 "peer-rejected-carrier",
332 );
333 assert!(should_rearm_wasm_carrier_event_retry(
334 "carrier-base-not-ready",
335 true,
336 false,
337 ));
338 assert!(!should_rearm_wasm_carrier_event_retry(
339 "peer-rejected-carrier",
340 true,
341 false,
342 ));
343 assert!(!should_rearm_wasm_carrier_event_retry(
344 "carrier-base-not-ready",
345 false,
346 false,
347 ));
348 assert!(!should_rearm_wasm_carrier_event_retry(
349 "carrier-base-not-ready",
350 true,
351 true,
352 ));
353 }
354
355 #[test]
356 fn carrier_fence_distinguishes_lifecycle_retirement_from_policy_change() {
357 assert_eq!(
358 classify_wasm_carrier_upgrade_fence(None, "attempt-a", BASE, 23),
359 WasmCarrierUpgradeFence::Retired,
360 );
361 assert_eq!(
362 classify_wasm_carrier_upgrade_fence(
363 Some(("attempt-a", BASE, 23)),
364 "attempt-a",
365 BASE,
366 24,
367 ),
368 WasmCarrierUpgradeFence::PolicyChanged,
369 );
370 assert_eq!(
371 classify_wasm_carrier_upgrade_fence(
372 Some(("attempt-b", BASE, 23)),
373 "attempt-a",
374 BASE,
375 23,
376 ),
377 WasmCarrierUpgradeFence::Superseded,
378 );
379 assert_eq!(
380 classify_wasm_carrier_upgrade_fence(
381 Some(("attempt-a", (17, 19, 23), 23)),
382 "attempt-a",
383 BASE,
384 23,
385 ),
386 WasmCarrierUpgradeFence::BaseGenerationChanged,
387 );
388 }
389}
390
391#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
392pub(crate) struct NativeWebRtcCarrierAttempt {
393 pub upgrade_id: String,
394 pub bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
395 pub generation: NativePeerDataGeneration,
396 pub remote_endpoint_id: iroh::EndpointId,
397 pub channel: Arc<crate::transport::WebRtcDataChannel>,
398 pub pump: tokio::sync::Mutex<Option<crate::native_webrtc_carrier::NativeWebRtcCarrierSession>>,
399}
400
401#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
402pub(crate) struct NativeMoqCarrierAttempt {
403 pub upgrade_id: String,
404 pub bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
405 pub generation: NativePeerDataGeneration,
406 pub remote_endpoint_id: iroh::EndpointId,
407 pub session: Arc<crate::transport::NativeMoQSession>,
408 pub pump: tokio::sync::Mutex<Option<crate::native_moq_carrier::NativeMoqCarrierSession>>,
409}
410
411#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
412impl std::fmt::Debug for NativeMoqCarrierAttempt {
413 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 formatter
415 .debug_struct("NativeMoqCarrierAttempt")
416 .field("upgrade_id", &self.upgrade_id)
417 .field("generation", &self.generation)
418 .field("remote_endpoint_id", &self.remote_endpoint_id)
419 .finish_non_exhaustive()
420 }
421}
422
423#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
424impl std::fmt::Debug for NativeWebRtcCarrierAttempt {
425 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 formatter
427 .debug_struct("NativeWebRtcCarrierAttempt")
428 .field("upgrade_id", &self.upgrade_id)
429 .field("generation", &self.generation)
430 .field("remote_endpoint_id", &self.remote_endpoint_id)
431 .finish_non_exhaustive()
432 }
433}
434
435#[cfg(not(target_arch = "wasm32"))]
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440pub(crate) struct NativeControlStreamOwner {
441 pub transport_stable_id: u64,
442 pub stream_rank: u64,
443}
444
445#[cfg(not(target_arch = "wasm32"))]
446#[derive(Clone)]
447pub(crate) struct NativeControlStreamEntry {
448 pub owner: NativeControlStreamOwner,
449 pub endpoint_id: iroh::EndpointId,
450 pub send: Arc<tokio::sync::Mutex<iroh::endpoint::SendStream>>,
451}
452
453#[cfg(not(target_arch = "wasm32"))]
462#[derive(Debug, Clone, Copy)]
463pub(crate) struct NativeTransportProtocolActivity {
464 pub transport_stable_id: u64,
465 pub observed_at: std::time::Instant,
466}
467
468#[cfg(not(target_arch = "wasm32"))]
469#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
470pub(crate) struct NativeAdmissionStreamContracts {
471 pub inbound: Option<crate::native_protocol::TokenStreamContract>,
472 pub outbound: Option<crate::native_protocol::TokenStreamContract>,
473}
474
475#[cfg(not(target_arch = "wasm32"))]
476#[derive(Debug, Clone)]
477pub(crate) struct CachedManagedScopeTicket {
478 pub scope: crate::session_token::GrantScope,
479 pub token: String,
480 pub max_connections: u32,
481 pub compound_ticket: String,
482 pub iroh_ticket: String,
483}
484
485#[cfg(not(target_arch = "wasm32"))]
486#[derive(Debug, Clone)]
487pub(crate) struct NativeRouteRepairCredential {
488 pub token: String,
489 pub token_payload: String,
490 pub authoritative_device_id: String,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq)]
500pub(crate) struct ScopedRouteRepairCredential {
501 pub token: String,
502 pub scope: String,
503 pub max_connections: u32,
504 pub expires_at_ms: Option<u64>,
505 pub authoritative_device_id: String,
506}
507
508#[allow(dead_code)]
513#[derive(Clone)]
514pub struct Client {
515 app_tag: String,
516 broadcasts: crate::broadcast::Broadcasts,
517 signaling: Arc<dyn SignalingBackend>,
518 pub room: Arc<dyn RoomBackend>,
519 node_id: Arc<RwLock<Option<String>>>,
520 pub iroh_endpoint: Arc<RwLock<Option<Endpoint>>>,
521 #[cfg(target_arch = "wasm32")]
522 pub iroh_node: Arc<RwLock<Option<IrohWasmNode>>>,
523 #[cfg(not(target_arch = "wasm32"))]
524 pub iroh_node: Arc<RwLock<Option<IrohNativeNode>>>,
525 #[cfg(not(target_arch = "wasm32"))]
526 native_application_streams: async_channel::Sender<IncomingStream>,
527 #[cfg(not(target_arch = "wasm32"))]
528 native_application_streams_receiver: async_channel::Receiver<IncomingStream>,
529 pub connection_manager: Arc<crate::connection_manager::ConnectionManager>,
530 #[cfg(not(target_arch = "wasm32"))]
531 native_device_identity: Arc<RwLock<Option<crate::native_device::NativeDeviceIdentity>>>,
532 #[cfg(not(target_arch = "wasm32"))]
533 native_device_base_dir: Arc<RwLock<Option<std::path::PathBuf>>>,
534 #[cfg(not(target_arch = "wasm32"))]
535 native_device_identity_init_guard: Arc<tokio::sync::Mutex<()>>,
536 #[cfg(not(target_arch = "wasm32"))]
537 native_device_updates:
538 tokio::sync::broadcast::Sender<crate::native_device::NativeDeviceIdentity>,
539 #[cfg(not(target_arch = "wasm32"))]
540 native_connection_state_updates: tokio::sync::broadcast::Sender<StateSnapshot>,
541 #[cfg(not(target_arch = "wasm32"))]
542 native_peer_data_updates: tokio::sync::broadcast::Sender<NativePeerDataEvent>,
543 auto_connect_loop_key: Arc<Mutex<Option<(String, String)>>>,
544 auto_connect_generation: Arc<AtomicU64>,
545 #[cfg(not(target_arch = "wasm32"))]
549 native_auto_connect_wake: Arc<tokio::sync::Notify>,
550 #[cfg(not(target_arch = "wasm32"))]
553 external_desired_peer_actor:
554 Arc<tokio::sync::Mutex<auto_connect_impl::NativeExternalAutoConnectActorState>>,
555 auto_connect_exclusion_owner: Arc<Mutex<()>>,
559 auto_connect_excluded: Arc<Mutex<HashSet<String>>>,
560 auto_connect_peer_requested_excluded: Arc<Mutex<HashSet<String>>>,
567 auto_connect_excluded_node_aliases: Arc<Mutex<HashMap<String, HashSet<String>>>>,
571 pub session_token_registry: Arc<crate::session_token::SessionTokenRegistry>,
575 #[cfg(not(target_arch = "wasm32"))]
579 inbound_session_admission_transport_ids: Arc<StdRwLock<HashMap<String, u64>>>,
580 #[cfg(not(target_arch = "wasm32"))]
583 offline_admission_requirements:
584 Arc<StdRwLock<HashMap<String, crate::offline::OfflineCandidateHandoff>>>,
585 #[cfg(not(target_arch = "wasm32"))]
587 offline_admission_transport_ids: Arc<StdRwLock<HashMap<String, u64>>>,
588 #[cfg(not(target_arch = "wasm32"))]
591 pub(crate) offline_runtime:
592 Arc<tokio::sync::Mutex<Option<crate::offline::InstalledOfflineRuntime>>>,
593 #[cfg(not(target_arch = "wasm32"))]
596 pub(crate) offline_proof_attempts: Arc<tokio::sync::Mutex<HashMap<String, u64>>>,
597 #[cfg(not(target_arch = "wasm32"))]
601 native_admission_stream_contracts:
602 Arc<StdRwLock<HashMap<String, NativeAdmissionStreamContracts>>>,
603 remote_session_admission_proofs: Arc<StdRwLock<HashMap<String, RemoteSessionAdmissionProof>>>,
607 pending_inline_reciprocal_admissions:
612 Arc<StdRwLock<HashMap<String, PendingInlineReciprocalAdmission>>>,
613 outbound_application_security_epoch_fingerprints: Arc<StdRwLock<HashMap<String, String>>>,
618 #[cfg(not(target_arch = "wasm32"))]
622 pending_reciprocal_session_admission_requests: Arc<StdRwLock<HashSet<String>>>,
623 #[cfg(not(target_arch = "wasm32"))]
627 native_route_repair_credentials: Arc<StdRwLock<HashMap<String, NativeRouteRepairCredential>>>,
628 scoped_route_repair_credentials: Arc<StdRwLock<HashMap<String, ScopedRouteRepairCredential>>>,
632 connection_application_crypto_keys:
633 Arc<StdRwLock<HashMap<String, [u8; crate::application_crypto::APPLICATION_KEY_BYTES]>>>,
634 connection_application_crypto_state: Arc<StdRwLock<()>>,
639 connection_application_crypto_required: Arc<StdRwLock<HashSet<String>>>,
640 #[cfg(not(target_arch = "wasm32"))]
644 trusted_user_device_application_crypto_required: Arc<AtomicBool>,
645 connection_application_crypto_confirmed: Arc<
649 StdRwLock<
650 HashMap<
651 String,
652 crate::client::application_crypto_impl::ConnectionApplicationCryptoConfirmation,
653 >,
654 >,
655 >,
656 #[cfg(not(target_arch = "wasm32"))]
663 connection_application_route_updates: Arc<tokio::sync::Notify>,
664 connection_application_crypto_outbound_sequences: Arc<StdRwLock<HashMap<String, u64>>>,
665 connection_application_key_agreements:
666 Arc<StdRwLock<HashMap<String, crate::key_agreement::KeyAgreement>>>,
667 #[cfg(not(target_arch = "wasm32"))]
676 managed_scope_tickets: Arc<StdRwLock<HashMap<String, CachedManagedScopeTicket>>>,
677 known_endpoint_addrs: Arc<RwLock<HashMap<String, iroh::EndpointAddr>>>,
683 known_device_ids_by_node: Arc<StdRwLock<HashMap<String, String>>>,
688 known_device_endpoint_revision: tokio::sync::watch::Sender<u64>,
692 sparse_fanout: Arc<tokio::sync::Mutex<crate::sparse_fanout::SparseFanoutState>>,
696 app_backgrounded: Arc<AtomicBool>,
700 background_execution_allowed: Arc<AtomicBool>,
701 #[cfg(target_arch = "wasm32")]
702 wasm_accept_bridge_started: Arc<std::sync::atomic::AtomicBool>,
703 #[cfg(target_arch = "wasm32")]
704 last_emitted_connection_states:
705 Arc<Mutex<std::collections::HashMap<String, WasmConnectionStateFingerprint>>>,
706 #[cfg(target_arch = "wasm32")]
707 last_empty_peer_sessions_warning_ms: Arc<AtomicU64>,
708 iroh_init_guard: Arc<tokio::sync::Mutex<()>>,
713 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
717 iroh_packet_carriers: Arc<
718 RwLock<
719 HashMap<
720 crate::iroh_carrier_kind::IrohCarrierKind,
721 Arc<crate::packet_carrier_transport::PacketCarrierAddressProvider>,
722 >,
723 >,
724 >,
725 managed_connect_gates: Arc<tokio::sync::Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>>,
732 #[cfg(not(target_arch = "wasm32"))]
736 iroh_path_watcher_stable_ids: Arc<Mutex<HashSet<u64>>>,
737 #[cfg(not(target_arch = "wasm32"))]
743 native_custom_transport_kinds: Arc<RwLock<HashMap<u64, IrohPathKind>>>,
744 #[cfg(not(target_arch = "wasm32"))]
748 native_transport_upgrade_providers:
749 Arc<RwLock<HashMap<IrohPathKind, Arc<dyn UpgradeProvider>>>>,
750 #[cfg(target_arch = "wasm32")]
754 wasm_transport_upgrade_gates:
755 Arc<tokio::sync::Mutex<HashMap<(String, IrohPathKind), WasmTransportUpgradeGate>>>,
756 #[cfg(not(target_arch = "wasm32"))]
759 native_transport_upgrade_gates:
760 Arc<tokio::sync::Mutex<HashMap<(String, IrohPathKind), NativeTransportUpgradeGate>>>,
761 #[cfg(not(target_arch = "wasm32"))]
765 native_ble_upgrade_attempts:
766 Arc<tokio::sync::Mutex<HashMap<String, NativeBleUpgradeAttemptState>>>,
767 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
768 native_webrtc_carrier_attempts: Arc<RwLock<HashMap<String, Arc<NativeWebRtcCarrierAttempt>>>>,
769 #[cfg(all(
774 not(target_arch = "wasm32"),
775 feature = "iroh-carrier-core",
776 any(feature = "test-harness", feature = "testing-endpoints")
777 ))]
778 native_iroh_carrier_debug_events: Arc<StdRwLock<std::collections::VecDeque<serde_json::Value>>>,
779 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
780 native_moq_carrier_attempts: Arc<RwLock<HashMap<String, Arc<NativeMoqCarrierAttempt>>>>,
781 #[cfg(not(target_arch = "wasm32"))]
785 native_peer_transport_capabilities:
786 Arc<RwLock<HashMap<String, HashSet<NativePeerTransportCapability>>>>,
787 pub(crate) presence_loop_tx:
788 Arc<Mutex<Option<tokio::sync::mpsc::Sender<crate::presence::PresenceCommand>>>>,
789 #[cfg(not(target_arch = "wasm32"))]
793 pub(crate) project_id: String,
794 #[cfg(not(target_arch = "wasm32"))]
795 pub(crate) token_provider: Arc<dyn Fn() -> Option<String> + Send + Sync>,
796 transport_config: Arc<RwLock<TransportConfig>>,
797 iroh_carrier_policy_epoch: Arc<AtomicU64>,
800 iroh_carrier_peer_policy_epochs: Arc<StdRwLock<HashMap<String, u64>>>,
804 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
805 pub(crate) local_discovery_registry: crate::local_discovery::LocalDiscoveryRegistry,
806 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
807 mdns_address_lookup: Arc<RwLock<Option<iroh_mdns_address_lookup::MdnsAddressLookup>>>,
808 #[cfg(not(target_arch = "wasm32"))]
814 pub(crate) native_control_streams:
815 Arc<tokio::sync::Mutex<HashMap<String, NativeControlStreamEntry>>>,
816 #[cfg(not(target_arch = "wasm32"))]
820 native_transport_protocol_activity:
821 Arc<StdRwLock<HashMap<String, NativeTransportProtocolActivity>>>,
822 #[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
826 scoped_connection_actor_registry:
827 Arc<RwLock<Option<Arc<crate::client::scoped_connection_actor::ConnectionActors>>>>,
828 #[cfg(not(target_arch = "wasm32"))]
834 auth_readiness: Arc<crate::client::auth_readiness::AuthReadinessStore>,
835 #[cfg(not(target_arch = "wasm32"))]
838 scope_classifier: Arc<RwLock<Arc<dyn ScopeClassifier>>>,
839}
840
841#[derive(Debug, Clone, PartialEq, Eq)]
842struct RemoteSessionAdmissionProof {
843 transport_stable_id: u64,
844 token_fingerprint: String,
845 approval_scope: String,
846}
847
848#[derive(Debug, Clone)]
849struct PendingInlineReciprocalAdmission {
850 connection_id: String,
851 remote_node_id: String,
852 local_device_id: String,
853 remote_device_id: String,
854 transport_stable_id: u64,
855 stream_instance_id: String,
856 presentation_id: String,
857 token: String,
858 token_fingerprint: String,
859 expected_scope: String,
860 inbound_admission_fingerprint: String,
861 inbound_admission_epoch: u64,
862}
863
864fn now_millis_i64() -> i64 {
865 crate::coordination::now_millis_u64().min(i64::MAX as u64) as i64
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
878pub enum IrohPathKind {
879 DirectQuic,
881 DirectLan,
883 Relay,
885 Ble,
887 WebRtc,
889 Moq,
891 Unknown,
893}
894
895#[cfg(not(target_arch = "wasm32"))]
896#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
897pub(crate) enum NativePeerTransportCapability {
898 WebRtc,
899 Moq,
900 Ble,
901}
902
903#[cfg(not(target_arch = "wasm32"))]
910#[async_trait::async_trait]
911pub trait UpgradeProvider: std::fmt::Debug + Send + Sync {
912 fn kind(&self) -> IrohPathKind;
913 fn transport_id(&self) -> u64;
914 async fn prepare_endpoint_addr(
915 &self,
916 endpoint_id: iroh::EndpointId,
917 ) -> anyhow::Result<iroh::EndpointAddr>;
918}
919
920impl IrohPathKind {
921 pub fn is_relay_path(self) -> bool {
922 matches!(self, Self::Relay)
923 }
924
925 pub fn transport_label(self) -> &'static str {
926 match self {
927 Self::DirectQuic => crate::transport_label::IROH_QUIC,
928 Self::DirectLan => crate::transport_label::IROH_LAN,
929 Self::Relay => crate::transport_label::IROH_RELAY,
930 Self::Ble => crate::transport_label::BLE,
931 Self::WebRtc => crate::transport_label::WEBRTC,
932 Self::Moq => crate::transport_label::MOQ,
933 Self::Unknown => crate::transport_label::IROH,
934 }
935 }
936}
937
938#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
940#[serde(rename_all = "camelCase")]
941pub struct IrohLanConfig {
942 #[serde(default = "default_lan_enabled")]
943 pub enabled: bool,
944 #[serde(default = "default_lan_advertise")]
946 pub advertise: bool,
947}
948
949fn default_lan_enabled() -> bool {
950 true
951}
952
953fn default_lan_advertise() -> bool {
954 true
955}
956
957impl Default for IrohLanConfig {
958 fn default() -> Self {
959 Self {
960 enabled: true,
961 advertise: true,
962 }
963 }
964}
965
966#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)]
971#[serde(rename_all = "camelCase")]
972pub struct BleConfig {
973 #[serde(default)]
974 pub enabled: bool,
975 #[serde(default, skip_serializing_if = "Option::is_none")]
976 pub connect_timeout_ms: Option<u64>,
977}
978
979pub(crate) const MANAGED_SETTLE_DEADLINE_MS: i64 =
984 crate::runtime_policy::MANAGED_SETTLE_DEADLINE_MS;
985
986#[cfg(not(target_arch = "wasm32"))]
994pub(crate) const SESSION_ADMISSION_TIMEOUT_MS: u64 =
995 crate::runtime_policy::SESSION_ADMISSION_TIMEOUT_MS;
996
997#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
998#[serde(rename_all = "camelCase")]
999pub enum ConnectionStatus {
1000 Disconnected,
1001 Connecting,
1002 Connected,
1003 Failed,
1004 Closed,
1005 Online,
1006}
1007
1008#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1009#[serde(rename_all = "camelCase")]
1010pub enum DevicePresenceStatus {
1011 Online,
1012 Idle,
1013 Offline,
1014}
1015
1016#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1017#[serde(rename_all = "camelCase")]
1018pub enum ReadinessState {
1019 Connecting,
1020 TransportOnly,
1021 Settling,
1022 Routable,
1023 AwaitingReplacement,
1024 Closed,
1025 Failed,
1026}
1027
1028#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1029#[serde(rename_all = "camelCase")]
1030pub struct LatencySnapshot {
1031 pub webrtc: Option<u64>,
1032 pub iroh: Option<u64>,
1033 pub iroh_lan: Option<u64>,
1034 pub iroh_relay: Option<u64>,
1035 pub ble: Option<u64>,
1036 pub moq: Option<u64>,
1037}
1038
1039impl LatencySnapshot {
1040 fn set(&mut self, transport: &str, latency_ms: u64) {
1041 match transport.trim().to_ascii_lowercase().as_str() {
1042 "webrtc" => self.webrtc = Some(latency_ms),
1043 "iroh-lan" => self.iroh_lan = Some(latency_ms),
1044 "iroh-relay" => self.iroh_relay = Some(latency_ms),
1045 "iroh" | "iroh-quic" => self.iroh = Some(latency_ms),
1046 "ble" => self.ble = Some(latency_ms),
1047 "moq" => self.moq = Some(latency_ms),
1048 _ => {}
1049 }
1050 }
1051
1052 fn get(&self, transport: &str) -> Option<u64> {
1053 match transport.trim().to_ascii_lowercase().as_str() {
1054 "webrtc" => self.webrtc,
1055 "iroh-lan" => self.iroh_lan,
1056 "iroh-relay" => self.iroh_relay,
1057 "iroh" | "iroh-quic" => self.iroh,
1058 "ble" => self.ble,
1059 "moq" => self.moq,
1060 _ => None,
1061 }
1062 }
1063}
1064
1065#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1066#[serde(rename_all = "camelCase")]
1067pub struct DeviceStatusSnapshot {
1068 #[serde(flatten)]
1069 pub device: crate::signaling::Device,
1070 pub presence_status: DevicePresenceStatus,
1071 pub presence_updated_at: Option<i64>,
1072 pub presence_expires_at: Option<i64>,
1073 pub connectable: bool,
1074 pub connection_status: ConnectionStatus,
1075 pub settled_ready: bool,
1076 pub readiness_state: ReadinessState,
1077 pub readiness_reason: String,
1078 pub peer_health: crate::connection_manager::ConnectionHealth,
1079 pub peer_id: Option<String>,
1080 pub scopes: Vec<String>,
1081 pub connection_id: Option<String>,
1082 pub device_id_hint: Option<String>,
1083 pub active_transport_stable_id: Option<u64>,
1084 pub transport_generation: u64,
1085 pub route_generation: u64,
1086 pub active_transport: String,
1087 pub parallel_transport: Option<String>,
1088 #[serde(default)]
1089 pub latency_ms: Option<u64>,
1090 #[serde(default)]
1091 pub latency_by_transport: LatencySnapshot,
1092}
1093
1094#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1095#[serde(rename_all = "camelCase")]
1096pub struct PeerSessionSnapshot {
1097 pub peer_id: String,
1098 pub device_id: Option<String>,
1099 pub device_id_hint: Option<String>,
1100 pub node_id: Option<String>,
1101 pub active_connection_id: Option<String>,
1102 pub candidate_connection_ids: Vec<String>,
1103 pub status: crate::connection_manager::ConnectionState,
1104 pub health: crate::connection_manager::ConnectionHealth,
1105 pub settled_ready: bool,
1106 #[serde(default)]
1107 pub logical_session_terminal: bool,
1108 pub readiness_state: ReadinessState,
1109 pub active_transport_stable_id: Option<u64>,
1110 pub transport_generation: u64,
1111 pub route_generation: u64,
1112 pub active_transport: String,
1113 pub parallel_transport: Option<String>,
1114 pub replacement_pending: bool,
1115 pub last_lifecycle_transition_at_ms: i64,
1116 pub readiness_reason: String,
1117 pub transition_count: u64,
1118 pub connecting_transition_count: u64,
1119 pub replacement_count: u64,
1120 pub retire_count: u64,
1121 pub last_disconnect_reason: Option<String>,
1122 pub last_reconnect_reason: Option<String>,
1123 pub scopes: Vec<String>,
1124 pub last_seen_at_ms: i64,
1125 pub error: Option<String>,
1126}
1127
1128#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1129#[serde(rename_all = "camelCase")]
1130pub struct ManagedConnectResult {
1131 pub connection_id: String,
1132 pub device_id: Option<String>,
1133 pub device_id_hint: Option<String>,
1134 pub remote_node_id: String,
1135 pub state: String,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub approved_scope: Option<String>,
1138}
1139
1140#[cfg(not(target_arch = "wasm32"))]
1141#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1142#[serde(rename_all = "camelCase")]
1143pub struct ConnectionAdoption {
1144 pub connection_id: String,
1145 pub node_id: String,
1146 pub device_id: Option<String>,
1147 pub transport_generation: u64,
1148 pub status_reason: Option<String>,
1149 pub main_stream_ready: bool,
1150}
1151
1152#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1153#[serde(rename_all = "camelCase")]
1154pub enum ManagedHealth {
1155 Healthy,
1156 AwaitingReplacement,
1157 Dead,
1158}
1159
1160#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1161#[serde(rename_all = "camelCase")]
1162pub struct ConnectionHealthView {
1163 pub connection_id: String,
1164 pub device_id: Option<String>,
1165 pub device_id_hint: Option<String>,
1166 pub node_id: Option<String>,
1167 pub active_transport_stable_id: Option<u64>,
1168 pub transport_generation: u64,
1169 pub route_generation: u64,
1170 pub status: ManagedHealth,
1171 pub settled_ready: bool,
1172 pub readiness_state: ReadinessState,
1173 pub replacement_pending: bool,
1174 pub last_lifecycle_transition_at_ms: i64,
1175 pub readiness_reason: String,
1176 pub transition_count: u64,
1177 pub connecting_transition_count: u64,
1178 pub replacement_count: u64,
1179 pub retire_count: u64,
1180 pub last_disconnect_reason: Option<String>,
1181 pub last_reconnect_reason: Option<String>,
1182}
1183
1184#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1185#[serde(rename_all = "camelCase")]
1186pub struct StateSnapshot {
1187 pub connection_id: String,
1188 pub device_id: Option<String>,
1189 pub device_id_hint: Option<String>,
1190 pub remote_node_id: Option<String>,
1191 pub state: String,
1192 pub transport_state: String,
1193 pub protocol_state: String,
1194 pub routable: bool,
1195 #[serde(default)]
1196 pub logical_session_terminal: bool,
1197 pub readiness_state: ReadinessState,
1198 pub readiness_reason: String,
1199 pub transport_generation: u64,
1200 pub route_generation: u64,
1201 pub active_transport_stable_id: Option<u64>,
1202 pub active_transport: String,
1203 pub parallel_transport: Option<String>,
1204 pub replacement_in_progress: bool,
1205 pub last_lifecycle_transition_at_ms: i64,
1206 pub transition_count: u64,
1207 pub connecting_transition_count: u64,
1208 pub replacement_count: u64,
1209 pub retire_count: u64,
1210 pub last_disconnect_reason: Option<String>,
1211 pub last_reconnect_reason: Option<String>,
1212 #[serde(default)]
1218 pub scopes: Vec<String>,
1219 pub error: Option<String>,
1220 pub created_at: i64,
1221 pub updated_at: i64,
1222}
1223
1224#[cfg(target_arch = "wasm32")]
1225#[derive(Debug, Clone, PartialEq, Eq)]
1226struct WasmConnectionStateFingerprint {
1227 connection_id: String,
1228 device_id: Option<String>,
1229 device_id_hint: Option<String>,
1230 remote_node_id: Option<String>,
1231 state: String,
1232 transport_state: String,
1233 protocol_state: String,
1234 routable: bool,
1235 logical_session_terminal: bool,
1236 transport_generation: u64,
1237 route_generation: u64,
1238 active_transport_stable_id: Option<u64>,
1239 active_transport: String,
1240 parallel_transport: Option<String>,
1241 replacement_in_progress: bool,
1242 transition_count: u64,
1243 connecting_transition_count: u64,
1244 replacement_count: u64,
1245 retire_count: u64,
1246 last_disconnect_reason: Option<String>,
1247 last_reconnect_reason: Option<String>,
1248 scopes: Vec<String>,
1249 error: Option<String>,
1250}
1251
1252#[cfg(target_arch = "wasm32")]
1253impl From<&StateSnapshot> for WasmConnectionStateFingerprint {
1254 fn from(snapshot: &StateSnapshot) -> Self {
1255 Self {
1256 connection_id: snapshot.connection_id.clone(),
1257 device_id: snapshot.device_id.clone(),
1258 device_id_hint: snapshot.device_id_hint.clone(),
1259 remote_node_id: snapshot.remote_node_id.clone(),
1260 state: snapshot.state.clone(),
1261 transport_state: snapshot.transport_state.clone(),
1262 protocol_state: snapshot.protocol_state.clone(),
1263 routable: snapshot.routable,
1264 logical_session_terminal: snapshot.logical_session_terminal,
1265 transport_generation: snapshot.transport_generation,
1266 route_generation: snapshot.route_generation,
1267 active_transport_stable_id: snapshot.active_transport_stable_id,
1268 active_transport: snapshot.active_transport.clone(),
1269 parallel_transport: snapshot.parallel_transport.clone(),
1270 replacement_in_progress: snapshot.replacement_in_progress,
1271 transition_count: snapshot.transition_count,
1272 connecting_transition_count: snapshot.connecting_transition_count,
1273 replacement_count: snapshot.replacement_count,
1274 retire_count: snapshot.retire_count,
1275 last_disconnect_reason: snapshot.last_disconnect_reason.clone(),
1276 last_reconnect_reason: snapshot.last_reconnect_reason.clone(),
1277 scopes: snapshot.scopes.clone(),
1278 error: snapshot.error.clone(),
1279 }
1280 }
1281}
1282
1283#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1284#[serde(rename_all = "camelCase", tag = "kind")]
1285pub enum BridgeAction {
1286 Healthy,
1287 AwaitReplacement {
1288 reason: String,
1289 },
1290 Rebind {
1291 remote_node_id: Option<String>,
1292 transport_generation: u64,
1293 },
1294 Retire {
1295 reason: String,
1296 },
1297}
1298
1299#[cfg(not(target_arch = "wasm32"))]
1300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1301enum DuplicateClosedHandling {
1302 RebindKeptTransport,
1303 RetireClosedTransport,
1304}
1305
1306#[cfg(not(target_arch = "wasm32"))]
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308pub(crate) enum IncomingTransportCloseResolution {
1309 PreservedKeptTransport,
1310 RetiredClosedTransport,
1311}
1312
1313#[cfg(not(target_arch = "wasm32"))]
1314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1315enum AutoConnectTieBreakDecision {
1316 ObserveConnectedTransport,
1317 WaitForInitiator,
1318 ActAsInitiator,
1319}
1320
1321#[cfg(not(target_arch = "wasm32"))]
1322fn duplicate_closed_handling(
1323 close_reason_debug: &str,
1324 kept_transport_alive: bool,
1325 kept_transport_healthy: bool,
1326 kept_stable_id: u64,
1327 closed_stable_id: u64,
1328) -> DuplicateClosedHandling {
1329 let _ = close_reason_debug;
1330 let _ = kept_transport_healthy;
1331 if kept_stable_id != closed_stable_id && kept_transport_alive {
1332 DuplicateClosedHandling::RebindKeptTransport
1333 } else {
1334 DuplicateClosedHandling::RetireClosedTransport
1335 }
1336}
1337
1338#[cfg(not(target_arch = "wasm32"))]
1339fn auto_connect_tie_break_decision(
1340 local_node_id: &str,
1341 remote_node_id: &str,
1342 transport_connected: bool,
1343 waited_ms: i64,
1344 initiator_grace_ms: i64,
1345) -> AutoConnectTieBreakDecision {
1346 if transport_connected {
1347 return AutoConnectTieBreakDecision::ObserveConnectedTransport;
1348 }
1349 if local_node_id <= remote_node_id {
1350 if waited_ms >= initiator_grace_ms {
1353 AutoConnectTieBreakDecision::ActAsInitiator
1354 } else {
1355 AutoConnectTieBreakDecision::WaitForInitiator
1356 }
1357 } else {
1358 AutoConnectTieBreakDecision::ActAsInitiator
1359 }
1360}
1361
1362fn normalize_lookup_id(value: Option<&str>) -> Option<String> {
1363 let trimmed = value?.trim();
1364 if trimmed.is_empty() {
1365 None
1366 } else {
1367 Some(trimmed.to_ascii_lowercase())
1368 }
1369}
1370
1371fn peer_snapshot_lookup_aliases(peer: &crate::connection_manager::PeerSnapshot) -> Vec<String> {
1372 [
1373 normalize_lookup_id(peer.device_id.as_deref()),
1374 normalize_lookup_id(peer.device_id_hint.as_deref()),
1375 normalize_lookup_id(peer.node_id.as_deref()),
1376 normalize_lookup_id(Some(peer.peer_id.as_str())),
1377 ]
1378 .into_iter()
1379 .flatten()
1380 .collect()
1381}
1382
1383fn peer_snapshot_matches_any_alias(
1384 peer: &crate::connection_manager::PeerSnapshot,
1385 aliases: &std::collections::HashSet<String>,
1386) -> bool {
1387 peer_snapshot_lookup_aliases(peer)
1388 .into_iter()
1389 .any(|alias| aliases.contains(&alias))
1390}
1391
1392fn preferred_peer_snapshot(
1393 current: Option<crate::connection_manager::PeerSnapshot>,
1394 candidate: crate::connection_manager::PeerSnapshot,
1395) -> crate::connection_manager::PeerSnapshot {
1396 match current {
1397 Some(existing) if !prefer_device_status_peer_snapshot(&candidate, &existing) => existing,
1398 _ => candidate,
1399 }
1400}
1401
1402fn snapshot_connection_status(
1403 device: &crate::signaling::Device,
1404 peer: Option<&crate::connection_manager::PeerSnapshot>,
1405) -> ConnectionStatus {
1406 match peer.map(|value| &value.status) {
1407 Some(crate::connection_manager::ConnectionState::Pending)
1408 | Some(crate::connection_manager::ConnectionState::Connecting) => {
1409 ConnectionStatus::Connecting
1410 }
1411 Some(crate::connection_manager::ConnectionState::Connected)
1412 if peer.map(peer_snapshot_settled_ready).unwrap_or(false) =>
1413 {
1414 ConnectionStatus::Connected
1415 }
1416 Some(crate::connection_manager::ConnectionState::Connected) => ConnectionStatus::Connecting,
1417 Some(crate::connection_manager::ConnectionState::Failed) => ConnectionStatus::Failed,
1418 Some(crate::connection_manager::ConnectionState::Closed)
1419 | Some(crate::connection_manager::ConnectionState::Closing) => ConnectionStatus::Closed,
1420 None if device.online => ConnectionStatus::Online,
1421 None => ConnectionStatus::Disconnected,
1422 }
1423}
1424
1425fn snapshot_settled_ready(
1426 peer: Option<&crate::connection_manager::PeerSnapshot>,
1427 connection_status: &ConnectionStatus,
1428) -> bool {
1429 matches!(connection_status, ConnectionStatus::Connected)
1430 && peer.map(peer_snapshot_settled_ready).unwrap_or(false)
1431}
1432
1433fn device_presence_updated_at(device: &crate::signaling::Device) -> Option<i64> {
1434 [
1435 device.last_seen_at.as_ref(),
1436 device.updated_at.as_ref(),
1437 device.created_at.as_ref(),
1438 ]
1439 .into_iter()
1440 .flatten()
1441 .filter_map(|value| crate::presence_policy::parse_millis(Some(value)))
1442 .max()
1443}
1444
1445fn device_presence_expires_at(device: &crate::signaling::Device) -> Option<i64> {
1446 crate::presence_policy::parse_millis(device.expires_at.as_ref())
1447}
1448
1449fn device_presence_status(
1450 device: &crate::signaling::Device,
1451 presence_updated_at: Option<i64>,
1452 now_ms: i64,
1453) -> DevicePresenceStatus {
1454 if !device.online {
1455 return DevicePresenceStatus::Offline;
1456 }
1457
1458 if presence_updated_at
1459 .map(|updated_at| {
1460 updated_at.saturating_add(crate::presence_policy::DEVICE_STALE_HEARTBEAT_MS) <= now_ms
1461 })
1462 .unwrap_or(false)
1463 {
1464 DevicePresenceStatus::Idle
1465 } else {
1466 DevicePresenceStatus::Online
1467 }
1468}
1469
1470fn device_connectable(
1471 device: &crate::signaling::Device,
1472 presence_status: &DevicePresenceStatus,
1473) -> bool {
1474 matches!(presence_status, DevicePresenceStatus::Online)
1475 && device
1476 .ticket
1477 .as_deref()
1478 .map(str::trim)
1479 .is_some_and(|value| !value.is_empty())
1480}
1481
1482fn peer_snapshot_settled_ready(peer: &crate::connection_manager::PeerSnapshot) -> bool {
1483 matches!(peer_readiness_state(peer), ReadinessState::Routable)
1484}
1485
1486fn peer_has_live_transport(peer: &crate::connection_manager::PeerSnapshot) -> bool {
1487 !peer.connection_ids.is_empty() && peer.active_transport_stable_id.is_some()
1488}
1489
1490fn peer_is_replacement_handoff(peer: &crate::connection_manager::PeerSnapshot) -> bool {
1491 peer.active_transport_stable_id.is_none()
1492 && peer.error.as_deref() == Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
1493}
1494
1495fn peer_readiness_state(peer: &crate::connection_manager::PeerSnapshot) -> ReadinessState {
1496 match peer.status {
1497 crate::connection_manager::ConnectionState::Pending
1498 | crate::connection_manager::ConnectionState::Connecting => ReadinessState::Connecting,
1499 crate::connection_manager::ConnectionState::Connected => {
1500 let connection_ids_present = !peer.connection_ids.is_empty();
1504 let healthy_with_connections = connection_ids_present
1505 && matches!(
1506 peer.health,
1507 crate::connection_manager::ConnectionHealth::Healthy
1508 );
1509 if healthy_with_connections {
1510 ReadinessState::Routable
1511 } else if peer_is_replacement_handoff(peer) {
1512 ReadinessState::AwaitingReplacement
1513 } else if !peer_has_live_transport(peer) {
1514 ReadinessState::TransportOnly
1515 } else if now_millis_i64().saturating_sub(peer.last_lifecycle_transition_at_ms)
1516 >= MANAGED_SETTLE_DEADLINE_MS
1517 {
1518 ReadinessState::AwaitingReplacement
1519 } else {
1520 ReadinessState::Settling
1521 }
1522 }
1523 crate::connection_manager::ConnectionState::Closing
1524 | crate::connection_manager::ConnectionState::Closed => ReadinessState::Closed,
1525 crate::connection_manager::ConnectionState::Failed => ReadinessState::Failed,
1526 }
1527}
1528
1529fn peer_readiness_reason(peer: &crate::connection_manager::PeerSnapshot) -> String {
1530 match peer_readiness_state(peer) {
1531 ReadinessState::Connecting => "waiting-for-transport".to_string(),
1532 ReadinessState::TransportOnly => {
1533 "transport-attached-awaiting-readiness-confirmation".to_string()
1534 }
1535 ReadinessState::Settling => "transport-connected-health-check-pending".to_string(),
1536 ReadinessState::Routable => "transport-healthy".to_string(),
1537 ReadinessState::AwaitingReplacement => {
1538 if peer.active_transport_stable_id.is_none() {
1539 "missing-active-transport".to_string()
1540 } else {
1541 "readiness-confirmation-timed-out".to_string()
1542 }
1543 }
1544 ReadinessState::Closed => "transport-closed".to_string(),
1545 ReadinessState::Failed => "connection-failed".to_string(),
1546 }
1547}
1548
1549fn connection_readiness_state(
1550 record: &crate::connection_manager::ConnectionRecord,
1551 peer_snapshot: Option<&crate::connection_manager::PeerSnapshot>,
1552) -> ReadinessState {
1553 match record.state {
1554 crate::connection_manager::ConnectionState::Pending
1555 | crate::connection_manager::ConnectionState::Connecting => ReadinessState::Connecting,
1556 crate::connection_manager::ConnectionState::Connected => {
1557 if peer_snapshot
1558 .map(peer_snapshot_settled_ready)
1559 .unwrap_or(false)
1560 {
1561 ReadinessState::Routable
1562 } else if record.transport_stable_id.is_none() {
1563 ReadinessState::AwaitingReplacement
1564 } else if now_millis_i64().saturating_sub(record.last_transport_change_at_ms)
1565 >= MANAGED_SETTLE_DEADLINE_MS
1566 {
1567 ReadinessState::AwaitingReplacement
1568 } else {
1569 ReadinessState::Settling
1570 }
1571 }
1572 crate::connection_manager::ConnectionState::Closing
1573 | crate::connection_manager::ConnectionState::Closed => ReadinessState::Closed,
1574 crate::connection_manager::ConnectionState::Failed => ReadinessState::Failed,
1575 }
1576}
1577
1578fn connection_readiness_reason(
1579 record: &crate::connection_manager::ConnectionRecord,
1580 peer_snapshot: Option<&crate::connection_manager::PeerSnapshot>,
1581) -> String {
1582 match connection_readiness_state(record, peer_snapshot) {
1583 ReadinessState::Connecting => "waiting-for-transport".to_string(),
1584 ReadinessState::TransportOnly => {
1585 "transport-attached-awaiting-readiness-confirmation".to_string()
1586 }
1587 ReadinessState::Settling => "bounded-health-confirmation-pending".to_string(),
1588 ReadinessState::Routable => "transport-healthy".to_string(),
1589 ReadinessState::AwaitingReplacement => {
1590 if record.transport_stable_id.is_none() {
1591 "missing-active-transport".to_string()
1592 } else {
1593 "readiness-confirmation-timed-out".to_string()
1594 }
1595 }
1596 ReadinessState::Closed => "transport-closed".to_string(),
1597 ReadinessState::Failed => "connection-failed".to_string(),
1598 }
1599}
1600
1601fn public_peer_state(
1602 peer: &crate::connection_manager::PeerSnapshot,
1603) -> crate::connection_manager::ConnectionState {
1604 if matches!(
1605 peer.status,
1606 crate::connection_manager::ConnectionState::Connected
1607 ) && !peer_snapshot_settled_ready(peer)
1608 {
1609 crate::connection_manager::ConnectionState::Connecting
1610 } else {
1611 peer.status.clone()
1612 }
1613}
1614
1615fn peer_session_snapshot_from_peer(
1616 peer: crate::connection_manager::PeerSnapshot,
1617) -> PeerSessionSnapshot {
1618 let status = public_peer_state(&peer);
1619 let active_connection_id = peer.connection_ids.first().cloned();
1620 let candidate_connection_ids = if peer.connection_ids.len() > 1 {
1621 peer.connection_ids[1..].to_vec()
1622 } else {
1623 Vec::new()
1624 };
1625 let settled_ready = peer_snapshot_settled_ready(&peer);
1626 let readiness_state = peer_readiness_state(&peer);
1627 let readiness_reason = peer_readiness_reason(&peer);
1628 let crate::connection_manager::PeerSnapshot {
1629 peer_id,
1630 device_id,
1631 device_id_hint,
1632 node_id,
1633 active_transport_stable_id,
1634 active_transport_generation,
1635 active_route_generation,
1636 active_transport,
1637 parallel_transport,
1638 last_lifecycle_transition_at_ms,
1639 health,
1640 transition_count,
1641 connecting_transition_count,
1642 replacement_count,
1643 retire_count,
1644 last_disconnect_reason,
1645 last_reconnect_reason,
1646 logical_session_terminal,
1647 scopes,
1648 last_seen_at_ms,
1649 error,
1650 ..
1651 } = peer;
1652
1653 PeerSessionSnapshot {
1654 peer_id,
1655 device_id,
1656 device_id_hint,
1657 node_id,
1658 active_connection_id,
1659 candidate_connection_ids,
1660 status,
1661 health,
1662 settled_ready,
1663 logical_session_terminal,
1664 readiness_state: readiness_state.clone(),
1665 active_transport_stable_id,
1666 transport_generation: active_transport_generation,
1667 route_generation: active_route_generation,
1668 active_transport,
1669 parallel_transport,
1670 replacement_pending: matches!(readiness_state, ReadinessState::AwaitingReplacement),
1671 last_lifecycle_transition_at_ms,
1672 readiness_reason,
1673 transition_count,
1674 connecting_transition_count,
1675 replacement_count,
1676 retire_count,
1677 last_disconnect_reason,
1678 last_reconnect_reason,
1679 scopes,
1680 last_seen_at_ms,
1681 error,
1682 }
1683}
1684
1685fn connection_state_snapshot_from_parts(
1686 record: &crate::connection_manager::ConnectionRecord,
1687 peer_snapshot: Option<&crate::connection_manager::PeerSnapshot>,
1688) -> StateSnapshot {
1689 let settled_ready = peer_snapshot
1690 .map(peer_snapshot_settled_ready)
1691 .unwrap_or(false);
1692 let readiness_state = connection_readiness_state(record, peer_snapshot);
1693 let in_place_reconnect_close = matches!(
1710 record.state,
1711 crate::connection_manager::ConnectionState::Closing
1712 | crate::connection_manager::ConnectionState::Closed
1713 ) && crate::lifecycle_reason::LifecycleReasonCode::from_text(
1714 record.status_reason.as_deref(),
1715 )
1716 .map(|code| {
1717 matches!(
1718 code,
1719 crate::lifecycle_reason::LifecycleReasonCode::ReplacementInProgress
1720 ) || code.is_transient()
1721 })
1722 .unwrap_or(false);
1723 let state = match record.state {
1724 crate::connection_manager::ConnectionState::Pending => "connecting",
1725 crate::connection_manager::ConnectionState::Connecting => "connecting",
1726 crate::connection_manager::ConnectionState::Connected if settled_ready => "connected",
1727 crate::connection_manager::ConnectionState::Connected => "connecting",
1728 crate::connection_manager::ConnectionState::Failed => "failed",
1729 crate::connection_manager::ConnectionState::Closing
1730 | crate::connection_manager::ConnectionState::Closed
1731 if in_place_reconnect_close =>
1732 {
1733 "connecting"
1734 }
1735 crate::connection_manager::ConnectionState::Closing
1736 | crate::connection_manager::ConnectionState::Closed => "closed",
1737 };
1738 let transport_state = match record.state {
1739 crate::connection_manager::ConnectionState::Pending
1740 | crate::connection_manager::ConnectionState::Connecting => "connecting",
1741 crate::connection_manager::ConnectionState::Connected => "connected",
1742 crate::connection_manager::ConnectionState::Closing
1743 | crate::connection_manager::ConnectionState::Closed
1744 if in_place_reconnect_close =>
1745 {
1746 "connecting"
1747 }
1748 crate::connection_manager::ConnectionState::Failed
1749 | crate::connection_manager::ConnectionState::Closing
1750 | crate::connection_manager::ConnectionState::Closed => "closed",
1751 };
1752 let protocol_state = match record.state {
1753 crate::connection_manager::ConnectionState::Connected if settled_ready => "routable",
1754 crate::connection_manager::ConnectionState::Connected => "transport-only",
1755 crate::connection_manager::ConnectionState::Pending
1756 | crate::connection_manager::ConnectionState::Connecting => "connecting",
1757 crate::connection_manager::ConnectionState::Closing
1758 | crate::connection_manager::ConnectionState::Closed
1759 if in_place_reconnect_close =>
1760 {
1761 "connecting"
1762 }
1763 crate::connection_manager::ConnectionState::Failed
1764 | crate::connection_manager::ConnectionState::Closing
1765 | crate::connection_manager::ConnectionState::Closed => "closed",
1766 };
1767
1768 StateSnapshot {
1769 connection_id: record.connection_id.clone(),
1770 device_id: record
1771 .device_id
1772 .clone()
1773 .or_else(|| peer_snapshot.and_then(|snapshot| snapshot.device_id.clone())),
1774 device_id_hint: record
1775 .device_id_hint
1776 .clone()
1777 .or_else(|| peer_snapshot.and_then(|snapshot| snapshot.device_id_hint.clone())),
1778 remote_node_id: record.node_id.clone(),
1779 state: state.to_string(),
1780 transport_state: transport_state.to_string(),
1781 protocol_state: protocol_state.to_string(),
1782 routable: settled_ready,
1783 logical_session_terminal: record.logical_session_terminal()
1786 && peer_snapshot
1787 .map(|peer| {
1788 peer.logical_session_terminal
1789 && peer.connection_ids.first() == Some(&record.connection_id)
1790 && peer.active_transport_generation == record.transport_generation
1791 && peer.active_route_generation == record.route_generation
1792 && peer.active_transport_stable_id == record.transport_stable_id
1793 })
1794 .unwrap_or(false),
1795 readiness_state: readiness_state.clone(),
1796 readiness_reason: connection_readiness_reason(record, peer_snapshot),
1797 transport_generation: record.transport_generation,
1798 route_generation: record.route_generation,
1799 active_transport_stable_id: record.transport_stable_id,
1800 active_transport: record.active_transport.clone(),
1801 parallel_transport: record.parallel_transport.clone(),
1802 replacement_in_progress: matches!(readiness_state, ReadinessState::AwaitingReplacement),
1803 last_lifecycle_transition_at_ms: record
1804 .last_state_change_at_ms
1805 .max(record.last_transport_change_at_ms)
1806 .max(record.last_route_change_at_ms),
1807 transition_count: record.transition_count,
1808 connecting_transition_count: record.connecting_transition_count,
1809 replacement_count: record.replacement_count,
1810 retire_count: record.retire_count,
1811 last_disconnect_reason: record.last_disconnect_reason.clone(),
1812 last_reconnect_reason: record.last_reconnect_reason.clone(),
1813 scopes: peer_snapshot
1814 .map(|snapshot| snapshot.scopes.clone())
1815 .unwrap_or_default(),
1816 error: record.status_reason.clone(),
1817 created_at: record.created_at_ms,
1818 updated_at: record.updated_at_ms,
1819 }
1820}
1821
1822fn merge_device_status_snapshots(
1823 devices: Vec<crate::signaling::Device>,
1824 peers: Vec<crate::connection_manager::PeerSnapshot>,
1825) -> Vec<DeviceStatusSnapshot> {
1826 let mut peer_by_device: std::collections::HashMap<
1827 String,
1828 crate::connection_manager::PeerSnapshot,
1829 > = std::collections::HashMap::new();
1830
1831 for peer in peers {
1832 for alias in peer_snapshot_lookup_aliases(&peer) {
1833 let replace = match peer_by_device.get(&alias) {
1834 Some(existing) => prefer_device_status_peer_snapshot(&peer, existing),
1835 None => true,
1836 };
1837 if replace {
1838 peer_by_device.insert(alias, peer.clone());
1839 }
1840 }
1841 }
1842
1843 devices
1844 .into_iter()
1845 .map(|device| {
1846 let authoritative_node_id = normalize_lookup_id(device.node_id.as_deref());
1847 let lookup_aliases: Vec<String> = [
1848 normalize_lookup_id(Some(device.device_id.as_str())),
1849 authoritative_node_id.clone(),
1850 ]
1851 .into_iter()
1852 .flatten()
1853 .collect();
1854 let peer = lookup_aliases
1855 .iter()
1856 .filter_map(|key| peer_by_device.get(key).cloned())
1857 .filter(|candidate| {
1858 let candidate_node_id = normalize_lookup_id(candidate.node_id.as_deref());
1859 match (&authoritative_node_id, candidate_node_id) {
1860 (Some(expected), Some(actual)) => expected == &actual,
1861 _ => true,
1862 }
1863 })
1864 .fold(None, |current, candidate| {
1865 Some(preferred_peer_snapshot(current, candidate))
1866 });
1867 let connection_status = snapshot_connection_status(&device, peer.as_ref());
1868 let presence_updated_at = device_presence_updated_at(&device);
1869 let presence_expires_at = device_presence_expires_at(&device);
1870 let presence_status =
1871 device_presence_status(&device, presence_updated_at, now_millis_i64());
1872 let connectable = device_connectable(&device, &presence_status);
1873 let readiness_state = peer.as_ref().map(peer_readiness_state).unwrap_or_else(|| {
1874 if device.online {
1875 ReadinessState::Connecting
1876 } else {
1877 ReadinessState::Closed
1878 }
1879 });
1880 DeviceStatusSnapshot {
1881 presence_status,
1882 presence_updated_at,
1883 presence_expires_at,
1884 connectable,
1885 settled_ready: snapshot_settled_ready(peer.as_ref(), &connection_status),
1886 readiness_state: readiness_state.clone(),
1887 readiness_reason: peer.as_ref().map(peer_readiness_reason).unwrap_or_else(|| {
1888 if device.online {
1889 "device-online-awaiting-runtime-session".to_string()
1890 } else {
1891 "device-offline".to_string()
1892 }
1893 }),
1894 connection_status,
1895 peer_health: peer
1896 .as_ref()
1897 .map(|value| value.health.clone())
1898 .unwrap_or_else(|| {
1899 if device.online {
1900 crate::connection_manager::ConnectionHealth::Healthy
1901 } else {
1902 crate::connection_manager::ConnectionHealth::Unknown
1903 }
1904 }),
1905 peer_id: peer.as_ref().map(|value| value.peer_id.clone()),
1906 scopes: peer
1907 .as_ref()
1908 .map(|value| value.scopes.clone())
1909 .unwrap_or_default(),
1910 connection_id: peer
1911 .as_ref()
1912 .and_then(|value| value.connection_ids.first().cloned()),
1913 device_id_hint: peer.as_ref().and_then(|value| value.device_id_hint.clone()),
1914 active_transport_stable_id: peer
1915 .as_ref()
1916 .and_then(|value| value.active_transport_stable_id),
1917 transport_generation: peer
1918 .as_ref()
1919 .map(|value| value.active_transport_generation)
1920 .unwrap_or(0),
1921 route_generation: peer
1922 .as_ref()
1923 .map(|value| value.active_route_generation)
1924 .unwrap_or(0),
1925 active_transport: peer
1926 .as_ref()
1927 .map(|value| value.active_transport.clone())
1928 .unwrap_or_else(|| "iroh".to_string()),
1929 parallel_transport: peer
1930 .as_ref()
1931 .and_then(|value| value.parallel_transport.clone()),
1932 latency_ms: None,
1933 latency_by_transport: LatencySnapshot::default(),
1934 device,
1935 }
1936 })
1937 .collect()
1938}
1939
1940fn peer_snapshot_recency_ms(peer: &crate::connection_manager::PeerSnapshot) -> i64 {
1941 peer.last_seen_at_ms
1942 .max(peer.last_lifecycle_transition_at_ms)
1943}
1944
1945fn prefer_device_status_peer_snapshot(
1951 candidate: &crate::connection_manager::PeerSnapshot,
1952 existing: &crate::connection_manager::PeerSnapshot,
1953) -> bool {
1954 use crate::connection_manager::ConnectionState;
1955 let c = &candidate.status;
1956 let e = &existing.status;
1957 let c_fail = matches!(c, ConnectionState::Failed);
1958 let c_conn = matches!(c, ConnectionState::Connected);
1959 let e_fail = matches!(e, ConnectionState::Failed);
1960 let e_conn = matches!(e, ConnectionState::Connected);
1961
1962 let c_live = c_conn && peer_has_live_transport(candidate);
1968 let e_live = e_conn && peer_has_live_transport(existing);
1969 if c_live && !e_live {
1970 return true;
1971 }
1972 if !c_live && e_live {
1973 return false;
1974 }
1975
1976 if c_fail && e_conn {
1977 return peer_snapshot_recency_ms(candidate) >= peer_snapshot_recency_ms(existing);
1978 }
1979 if c_conn && e_fail {
1980 return peer_snapshot_recency_ms(candidate) > peer_snapshot_recency_ms(existing);
1981 }
1982 device_status_peer_priority(candidate) > device_status_peer_priority(existing)
1983}
1984
1985fn device_status_peer_priority(
1986 peer: &crate::connection_manager::PeerSnapshot,
1987) -> (u8, u8, u8, u8, i64, u64, u64) {
1988 let readiness_rank = match peer_readiness_state(peer) {
1989 ReadinessState::Routable => 5,
1990 ReadinessState::Settling => 4,
1991 ReadinessState::TransportOnly => 3,
1992 ReadinessState::Connecting => 2,
1993 ReadinessState::AwaitingReplacement => 1,
1994 ReadinessState::Closed | ReadinessState::Failed => 0,
1995 };
1996
1997 let status_rank = match peer.status {
1998 crate::connection_manager::ConnectionState::Connected => 3,
1999 crate::connection_manager::ConnectionState::Connecting
2000 | crate::connection_manager::ConnectionState::Pending => 2,
2001 crate::connection_manager::ConnectionState::Closing
2002 | crate::connection_manager::ConnectionState::Closed => 1,
2003 crate::connection_manager::ConnectionState::Failed => 0,
2004 };
2005
2006 let health_rank = match peer.health {
2007 crate::connection_manager::ConnectionHealth::Healthy => 3,
2008 crate::connection_manager::ConnectionHealth::Suspect => 2,
2009 crate::connection_manager::ConnectionHealth::Unknown => 1,
2010 crate::connection_manager::ConnectionHealth::Stale => 0,
2011 };
2012
2013 (
2014 readiness_rank,
2015 status_rank,
2016 health_rank,
2017 u8::from(peer_has_live_transport(peer)),
2018 peer.last_seen_at_ms,
2019 peer.transition_count,
2020 peer.replacement_count,
2021 )
2022}
2023
2024#[cfg(not(target_arch = "wasm32"))]
2025fn auto_connect_verbose() -> bool {
2026 std::env::var("OPENRTC_AUTOCONNECT_VERBOSE")
2027 .map(|value| {
2028 let normalized = value.trim().to_ascii_lowercase();
2029 normalized == "1" || normalized == "true" || normalized == "yes"
2030 })
2031 .unwrap_or(false)
2032}
2033
2034#[cfg(not(target_arch = "wasm32"))]
2035fn parse_env_bool(value: &str) -> Option<bool> {
2036 match value.trim().to_ascii_lowercase().as_str() {
2037 "1" | "true" | "yes" | "on" => Some(true),
2038 "0" | "false" | "no" | "off" => Some(false),
2039 _ => None,
2040 }
2041}
2042
2043#[cfg(not(target_arch = "wasm32"))]
2044fn env_bool(name: &str) -> Option<bool> {
2045 std::env::var(name)
2046 .ok()
2047 .and_then(|value| parse_env_bool(&value))
2048}
2049
2050#[cfg(not(target_arch = "wasm32"))]
2051fn should_use_relay_only_mode() -> bool {
2052 env_bool("PLUTO_IROH_RELAY_ONLY").unwrap_or(false)
2053}
2054
2055#[cfg(not(target_arch = "wasm32"))]
2056fn should_disable_ipv6() -> bool {
2057 resolve_disable_ipv6(
2058 env_bool("PLUTO_IROH_DISABLE_IPV6"),
2059 env_bool("PLUTO_IROH_ENABLE_IPV6"),
2060 )
2061}
2062
2063#[cfg(not(target_arch = "wasm32"))]
2064fn resolve_disable_ipv6(disable: Option<bool>, enable: Option<bool>) -> bool {
2065 disable
2066 .or_else(|| enable.map(|value| !value))
2067 .unwrap_or(false)
2068}
2069
2070#[cfg(not(target_arch = "wasm32"))]
2071fn apply_native_network_preferences(
2072 mut builder: iroh::endpoint::Builder,
2073 relay_only: bool,
2074 relay_transport_policy: RelayPolicy,
2075) -> anyhow::Result<iroh::endpoint::Builder> {
2076 #[cfg(openrtc_iroh_relay_transport_policy_api)]
2077 {
2078 let policy = match relay_transport_policy {
2079 RelayPolicy::Auto => iroh::RelayTransportPolicy::Auto,
2080 RelayPolicy::QuicRequired => iroh::RelayTransportPolicy::QuicRequired,
2081 RelayPolicy::WebsocketRequired => iroh::RelayTransportPolicy::WebsocketRequired,
2082 };
2083 builder = builder.relay_transport_policy(policy);
2084 }
2085 #[cfg(not(openrtc_iroh_relay_transport_policy_api))]
2086 {
2087 match relay_transport_policy {
2088 RelayPolicy::QuicRequired => {
2089 anyhow::bail!(
2090 "requested relay carriage quicRequired is unsupported by this upstream-Iroh build; refusing to initialize"
2091 );
2092 }
2093 RelayPolicy::Auto => {
2094 eprintln!(
2095 "[pluto-rtc][native] requested relay carriage auto; effective carriage is websocket fallback because this upstream-Iroh build has no QUIC relay policy API"
2096 );
2097 }
2098 RelayPolicy::WebsocketRequired => {}
2099 }
2100 }
2101
2102 builder = builder.transport_config(crate::native_node::iroh_config());
2119
2120 if relay_only {
2121 return Ok(builder.clear_ip_transports());
2123 }
2124
2125 if should_disable_ipv6() {
2126 builder = builder.clear_ip_transports();
2129 builder = builder
2130 .bind_addr("0.0.0.0:0")
2131 .map_err(|e| anyhow::anyhow!("failed to bind IPv4 transport: {}", e))?;
2132 }
2133
2134 Ok(builder)
2135}
2136
2137#[cfg(not(target_arch = "wasm32"))]
2138fn auto_connect_failure_backoff_ms(failure_count: u8) -> i64 {
2139 match failure_count {
2140 0 => 0,
2141 _ => {
2142 let exp = 500_i64.saturating_mul(1_i64 << (failure_count as u32).min(4));
2146 exp.min(8_000)
2147 }
2148 }
2149}
2150
2151#[cfg(not(target_arch = "wasm32"))]
2152fn auto_connect_admission_rejection_backoff_ms(failure_count: u8) -> i64 {
2153 match failure_count {
2154 0 => 0,
2155 _ => {
2156 let exp = 1_000_i64.saturating_mul(1_i64 << (failure_count as u32).min(5));
2160 exp.min(30_000)
2161 }
2162 }
2163}
2164
2165#[cfg(not(target_arch = "wasm32"))]
2166fn auto_connect_network_change_failure_threshold() -> u8 {
2167 std::env::var("OPENRTC_NETWORK_CHANGE_FAILURE_THRESHOLD")
2168 .ok()
2169 .and_then(|value| value.parse::<u8>().ok())
2170 .map(|value| value.clamp(1, 6))
2171 .unwrap_or(2)
2172}
2173
2174#[cfg(not(target_arch = "wasm32"))]
2178fn non_initiator_escalation_grace_ms(escalation_count: u8) -> i64 {
2179 let base_ms = 2_000_i64;
2180 let exp_ms = base_ms.saturating_mul(1_i64 << (escalation_count as u32).min(3));
2181 let capped_ms = exp_ms.min(16_000);
2182 let jitter_pct = ((escalation_count as i64 * 37 + 7) % 41) - 20; let jitter_ms = capped_ms * jitter_pct / 100;
2186 (capped_ms + jitter_ms).max(1_000)
2187}
2188
2189#[cfg(not(target_arch = "wasm32"))]
2190fn next_non_initiator_wakeup_delay_ms(
2191 wait_started_at: &std::collections::HashMap<String, i64>,
2192 escalation_count: &std::collections::HashMap<String, u8>,
2193 now_ms: i64,
2194) -> Option<u64> {
2195 wait_started_at
2196 .iter()
2197 .map(|(device_id, started_at)| {
2198 let grace_ms = non_initiator_escalation_grace_ms(
2199 escalation_count.get(device_id).copied().unwrap_or(0),
2200 );
2201 started_at
2202 .saturating_add(grace_ms)
2203 .saturating_sub(now_ms)
2204 .max(1) as u64
2205 })
2206 .min()
2207}
2208
2209#[cfg(not(target_arch = "wasm32"))]
2210fn register_auto_connect_failure(
2211 remote_device_id: &str,
2212 now_ms: i64,
2213 failure_count: &mut std::collections::HashMap<String, u8>,
2214 failure_backoff_until: &mut std::collections::HashMap<String, i64>,
2215) {
2216 let count = failure_count
2217 .entry(remote_device_id.to_string())
2218 .or_insert(0);
2219 *count = count.saturating_add(1).min(10);
2220 let backoff_ms = auto_connect_failure_backoff_ms(*count);
2221 failure_backoff_until.insert(
2222 remote_device_id.to_string(),
2223 now_ms.saturating_add(backoff_ms),
2224 );
2225}
2226
2227#[cfg(not(target_arch = "wasm32"))]
2228fn register_auto_connect_admission_rejection(
2229 remote_device_id: &str,
2230 now_ms: i64,
2231 failure_count: &mut std::collections::HashMap<String, u8>,
2232 failure_backoff_until: &mut std::collections::HashMap<String, i64>,
2233) {
2234 let count = failure_count
2235 .entry(remote_device_id.to_string())
2236 .or_insert(0);
2237 *count = count.saturating_add(1).min(10);
2238 let backoff_ms = auto_connect_admission_rejection_backoff_ms(*count);
2239 failure_backoff_until.insert(
2240 remote_device_id.to_string(),
2241 now_ms.saturating_add(backoff_ms),
2242 );
2243}
2244
2245#[cfg(not(target_arch = "wasm32"))]
2246fn clear_auto_connect_failure_state(
2247 remote_device_id: &str,
2248 failure_count: &mut std::collections::HashMap<String, u8>,
2249 failure_backoff_until: &mut std::collections::HashMap<String, i64>,
2250) {
2251 failure_count.remove(remote_device_id);
2252 failure_backoff_until.remove(remote_device_id);
2253}
2254
2255#[cfg(target_arch = "wasm32")]
2256fn emit_wasm_connection_state_event(snapshot: &StateSnapshot) {
2257 use wasm_bindgen::JsValue;
2258
2259 let Some(window) = web_sys::window() else {
2260 return;
2261 };
2262
2263 let detail = js_sys::Object::new();
2264 let _ = js_sys::Reflect::set(
2265 &detail,
2266 &JsValue::from_str("connectionId"),
2267 &JsValue::from_str(snapshot.connection_id.as_str()),
2268 );
2269 let _ = js_sys::Reflect::set(
2270 &detail,
2271 &JsValue::from_str("state"),
2272 &JsValue::from_str(snapshot.state.as_str()),
2273 );
2274 let _ = js_sys::Reflect::set(
2275 &detail,
2276 &JsValue::from_str("createdAt"),
2277 &JsValue::from_f64(snapshot.created_at as f64),
2278 );
2279 let _ = js_sys::Reflect::set(
2280 &detail,
2281 &JsValue::from_str("updatedAt"),
2282 &JsValue::from_f64(snapshot.updated_at as f64),
2283 );
2284 let _ = js_sys::Reflect::set(
2285 &detail,
2286 &JsValue::from_str("transportState"),
2287 &JsValue::from_str(snapshot.transport_state.as_str()),
2288 );
2289 let _ = js_sys::Reflect::set(
2290 &detail,
2291 &JsValue::from_str("protocolState"),
2292 &JsValue::from_str(snapshot.protocol_state.as_str()),
2293 );
2294 let _ = js_sys::Reflect::set(
2295 &detail,
2296 &JsValue::from_str("routable"),
2297 &JsValue::from_bool(snapshot.routable),
2298 );
2299 let _ = js_sys::Reflect::set(
2300 &detail,
2301 &JsValue::from_str("logicalSessionTerminal"),
2302 &JsValue::from_bool(snapshot.logical_session_terminal),
2303 );
2304 let _ = js_sys::Reflect::set(
2305 &detail,
2306 &JsValue::from_str("transportGeneration"),
2307 &JsValue::from_f64(snapshot.transport_generation as f64),
2308 );
2309 let _ = js_sys::Reflect::set(
2310 &detail,
2311 &JsValue::from_str("routeGeneration"),
2312 &JsValue::from_f64(snapshot.route_generation as f64),
2313 );
2314 let _ = js_sys::Reflect::set(
2315 &detail,
2316 &JsValue::from_str("activeTransportStableId"),
2317 &snapshot
2318 .active_transport_stable_id
2319 .map(|value| JsValue::from_f64(value as f64))
2320 .unwrap_or(JsValue::NULL),
2321 );
2322 let _ = js_sys::Reflect::set(
2323 &detail,
2324 &JsValue::from_str("activeTransport"),
2325 &JsValue::from_str(snapshot.active_transport.as_str()),
2326 );
2327 let _ = js_sys::Reflect::set(
2328 &detail,
2329 &JsValue::from_str("parallelTransport"),
2330 &snapshot
2331 .parallel_transport
2332 .as_deref()
2333 .map(JsValue::from_str)
2334 .unwrap_or(JsValue::NULL),
2335 );
2336 let _ = js_sys::Reflect::set(
2337 &detail,
2338 &JsValue::from_str("replacementInProgress"),
2339 &JsValue::from_bool(snapshot.replacement_in_progress),
2340 );
2341 let _ = js_sys::Reflect::set(
2342 &detail,
2343 &JsValue::from_str("transitionCount"),
2344 &JsValue::from_f64(snapshot.transition_count as f64),
2345 );
2346 let _ = js_sys::Reflect::set(
2347 &detail,
2348 &JsValue::from_str("connectingTransitionCount"),
2349 &JsValue::from_f64(snapshot.connecting_transition_count as f64),
2350 );
2351 let _ = js_sys::Reflect::set(
2352 &detail,
2353 &JsValue::from_str("replacementCount"),
2354 &JsValue::from_f64(snapshot.replacement_count as f64),
2355 );
2356 let _ = js_sys::Reflect::set(
2357 &detail,
2358 &JsValue::from_str("retireCount"),
2359 &JsValue::from_f64(snapshot.retire_count as f64),
2360 );
2361
2362 if let Some(device_id) = snapshot.device_id.as_deref() {
2363 let _ = js_sys::Reflect::set(
2364 &detail,
2365 &JsValue::from_str("deviceId"),
2366 &JsValue::from_str(device_id),
2367 );
2368 }
2369 if let Some(device_id_hint) = snapshot.device_id_hint.as_deref() {
2370 let _ = js_sys::Reflect::set(
2371 &detail,
2372 &JsValue::from_str("deviceIdHint"),
2373 &JsValue::from_str(device_id_hint),
2374 );
2375 }
2376 if let Some(remote_node_id) = snapshot.remote_node_id.as_deref() {
2377 let _ = js_sys::Reflect::set(
2378 &detail,
2379 &JsValue::from_str("remoteNodeId"),
2380 &JsValue::from_str(remote_node_id),
2381 );
2382 }
2383 if let Some(error) = snapshot.error.as_deref() {
2384 let _ = js_sys::Reflect::set(
2385 &detail,
2386 &JsValue::from_str("error"),
2387 &JsValue::from_str(error),
2388 );
2389 }
2390 if let Some(reason) = snapshot.last_disconnect_reason.as_deref() {
2391 let _ = js_sys::Reflect::set(
2392 &detail,
2393 &JsValue::from_str("lastDisconnectReason"),
2394 &JsValue::from_str(reason),
2395 );
2396 }
2397 if let Some(reason) = snapshot.last_reconnect_reason.as_deref() {
2398 let _ = js_sys::Reflect::set(
2399 &detail,
2400 &JsValue::from_str("lastReconnectReason"),
2401 &JsValue::from_str(reason),
2402 );
2403 }
2404
2405 let init = web_sys::CustomEventInit::new();
2406 init.set_detail(&detail.into());
2407 if let Ok(event) =
2408 web_sys::CustomEvent::new_with_event_init_dict("connection-state-changed", &init)
2409 {
2410 let _ = window.dispatch_event(&event);
2411 }
2412}
2413
2414fn parse_endpoint_ticket(ticket: &str) -> anyhow::Result<iroh::EndpointAddr> {
2415 let trimmed = ticket.trim();
2416 if trimmed.is_empty() {
2417 return Err(anyhow::anyhow!("endpoint ticket is required"));
2418 }
2419
2420 let parsed = EndpointTicket::from_str(trimmed)
2421 .map_err(|e| anyhow::anyhow!("invalid endpoint ticket: {}", e))?;
2422 Ok(parsed.endpoint_addr().clone())
2423}
2424
2425#[derive(Debug, Clone, serde::Serialize)]
2426#[serde(rename_all = "camelCase")]
2427pub struct RuntimeStatus {
2428 pub ready: bool,
2429 pub node_id: Option<String>,
2430 pub transport: TransportStatus,
2431}
2432
2433#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
2434#[serde(rename_all = "kebab-case")]
2435pub enum CapabilityMaturity {
2436 Stable,
2437 Preview,
2438 SupportOnly,
2439 Unavailable,
2440}
2441
2442#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
2443#[serde(rename_all = "camelCase")]
2444pub struct ProductCapabilityMaturity {
2445 pub bounded_rooms: CapabilityMaturity,
2446 pub service_nodes: CapabilityMaturity,
2447 pub offline_edge: CapabilityMaturity,
2448 pub broadcast: CapabilityMaturity,
2449}
2450
2451impl ProductCapabilityMaturity {
2452 pub(crate) fn for_current_target() -> Self {
2453 Self {
2454 bounded_rooms: CapabilityMaturity::Stable,
2455 service_nodes: CapabilityMaturity::Preview,
2456 offline_edge: if cfg!(all(not(target_arch = "wasm32"), feature = "transport-lan")) {
2457 CapabilityMaturity::Preview
2458 } else {
2459 CapabilityMaturity::SupportOnly
2460 },
2461 broadcast: CapabilityMaturity::Preview,
2462 }
2463 }
2464}
2465
2466#[derive(Debug, Clone, serde::Serialize)]
2467#[serde(rename_all = "camelCase")]
2468pub struct FeatureStatus {
2469 pub compiled: bool,
2470 pub enabled: bool,
2471}
2472
2473impl FeatureStatus {
2474 pub(crate) fn new(compiled: bool, enabled: bool) -> Self {
2475 Self {
2476 compiled,
2477 enabled: compiled && enabled,
2478 }
2479 }
2480}
2481
2482#[derive(Debug, Clone, serde::Serialize)]
2483#[serde(rename_all = "camelCase")]
2484pub struct TransportStatus {
2485 pub iroh_quic: FeatureStatus,
2486 pub iroh_lan: FeatureStatus,
2487 pub web_rtc: FeatureStatus,
2488 pub moq: FeatureStatus,
2489 pub ble: FeatureStatus,
2490 pub iroh_relay_only: bool,
2491 #[serde(skip_serializing_if = "Option::is_none")]
2492 pub iroh_relay_transport_policy: Option<RelayPolicy>,
2493 pub iroh_relay: RelayStatus,
2494 pub route_priority: Vec<crate::route_policy::KnownRoute>,
2495 pub optimize_for: crate::route_policy::RouteOptimization,
2496 pub network_policy: NetworkPolicy,
2497}
2498
2499#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
2500#[serde(rename_all = "camelCase")]
2501pub enum IrohRelayProvider {
2502 VendoredIroh,
2504 UpstreamIroh,
2506}
2507
2508#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
2509#[serde(rename_all = "camelCase")]
2510pub enum RelayCarriage {
2511 Quic,
2512 Websocket,
2513 QuicWithWebsocketFallback,
2514 WebsocketFallback,
2515 Unsupported,
2516}
2517
2518#[derive(Debug, Clone, serde::Serialize)]
2519#[serde(rename_all = "camelCase")]
2520pub struct RelayStatus {
2521 pub provider: IrohRelayProvider,
2522 pub requested_carriage: RelayPolicy,
2523 pub effective_carriage: RelayCarriage,
2524}
2525
2526impl RelayStatus {
2527 pub(crate) fn from_policy(
2528 requested_carriage: RelayPolicy,
2529 provider: IrohRelayProvider,
2530 ) -> Self {
2531 let effective_carriage = match requested_carriage {
2532 RelayPolicy::WebsocketRequired => RelayCarriage::Websocket,
2533 RelayPolicy::Auto => {
2534 #[cfg(target_arch = "wasm32")]
2535 {
2536 RelayCarriage::Websocket
2537 }
2538 #[cfg(all(not(target_arch = "wasm32"), openrtc_iroh_relay_transport_policy_api))]
2539 {
2540 RelayCarriage::QuicWithWebsocketFallback
2541 }
2542 #[cfg(all(
2543 not(target_arch = "wasm32"),
2544 not(openrtc_iroh_relay_transport_policy_api)
2545 ))]
2546 {
2547 RelayCarriage::WebsocketFallback
2548 }
2549 }
2550 RelayPolicy::QuicRequired => {
2551 #[cfg(target_arch = "wasm32")]
2552 {
2553 RelayCarriage::Unsupported
2554 }
2555 #[cfg(all(not(target_arch = "wasm32"), openrtc_iroh_relay_transport_policy_api))]
2556 {
2557 RelayCarriage::Quic
2558 }
2559 #[cfg(all(
2560 not(target_arch = "wasm32"),
2561 not(openrtc_iroh_relay_transport_policy_api)
2562 ))]
2563 {
2564 RelayCarriage::Unsupported
2565 }
2566 }
2567 };
2568
2569 Self {
2570 provider,
2571 requested_carriage,
2572 effective_carriage,
2573 }
2574 }
2575}
2576
2577#[cfg(openrtc_iroh_relay_transport_policy_api)]
2578pub(crate) const fn current_iroh_relay_provider() -> IrohRelayProvider {
2579 IrohRelayProvider::VendoredIroh
2580}
2581
2582#[cfg(not(openrtc_iroh_relay_transport_policy_api))]
2583pub(crate) const fn current_iroh_relay_provider() -> IrohRelayProvider {
2584 IrohRelayProvider::UpstreamIroh
2585}
2586
2587impl TransportStatus {
2588 pub(crate) fn from_config_with_ble_available(
2589 config: &TransportConfig,
2590 ble_available: bool,
2591 relay_provider: IrohRelayProvider,
2592 ) -> Self {
2593 let lan_compiled = cfg!(feature = "transport-lan");
2594 let lan_enabled = config
2595 .iroh_lan
2596 .as_ref()
2597 .map(|lan| lan.enabled)
2598 .unwrap_or(false);
2599 let webrtc_compiled = cfg!(feature = "transport-webrtc");
2600 let webrtc_enabled = config.webrtc.is_some();
2601 let moq_compiled = cfg!(feature = "transport-moq");
2602 let moq_enabled = config.moq.is_some();
2603 let ble_compiled = ble_available;
2604 let ble_enabled = config.ble.as_ref().map(|ble| ble.enabled).unwrap_or(false);
2605
2606 Self {
2607 iroh_quic: FeatureStatus::new(true, true),
2608 iroh_lan: FeatureStatus::new(lan_compiled, lan_enabled),
2609 web_rtc: FeatureStatus::new(webrtc_compiled, webrtc_enabled),
2610 moq: FeatureStatus::new(moq_compiled, moq_enabled),
2611 ble: FeatureStatus::new(ble_compiled, ble_enabled),
2612 iroh_relay_only: config.iroh_relay_only,
2613 iroh_relay_transport_policy: config.iroh_relay_transport_policy.clone(),
2614 iroh_relay: RelayStatus::from_policy(
2615 config
2616 .iroh_relay_transport_policy
2617 .clone()
2618 .unwrap_or(RelayPolicy::WebsocketRequired),
2619 relay_provider,
2620 ),
2621 route_priority: config.route_priority.clone(),
2622 optimize_for: config.optimize_for,
2623 network_policy: config.network_policy,
2624 }
2625 }
2626}
2627
2628#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2629#[serde(rename_all = "camelCase")]
2630pub struct IceServerConfig {
2631 #[serde(default)]
2632 pub urls: Vec<String>,
2633 #[serde(default, skip_serializing_if = "Option::is_none")]
2634 pub username: Option<String>,
2635 #[serde(default, skip_serializing_if = "Option::is_none")]
2636 pub credential: Option<String>,
2637}
2638
2639#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2640#[serde(rename_all = "camelCase")]
2641pub struct WebRTCConfig {
2642 #[serde(default)]
2643 pub ice_servers: Vec<IceServerConfig>,
2644 #[serde(default)]
2645 pub privacy_mode: bool,
2646 #[serde(default)]
2647 pub lan_mode: bool,
2648}
2649
2650impl Default for WebRTCConfig {
2651 fn default() -> Self {
2652 Self {
2653 ice_servers: Vec::new(),
2654 privacy_mode: false,
2655 lan_mode: false,
2656 }
2657 }
2658}
2659
2660#[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2661#[serde(rename_all = "camelCase")]
2662pub struct MoQConfig {
2663 #[serde(default)]
2664 pub relay_url: String,
2665 #[serde(default, skip_serializing)]
2666 pub access_token: Option<String>,
2667}
2668
2669impl std::fmt::Debug for MoQConfig {
2670 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2671 let relay_label = self
2672 .relay_url
2673 .split(['?', '#'])
2674 .next()
2675 .unwrap_or("invalid-moq-relay-url");
2676 formatter
2677 .debug_struct("MoQConfig")
2678 .field("relay_url", &relay_label)
2679 .field(
2680 "access_token",
2681 &self.access_token.as_ref().map(|_| "[redacted]"),
2682 )
2683 .finish()
2684 }
2685}
2686
2687impl Default for MoQConfig {
2688 fn default() -> Self {
2689 Self {
2690 relay_url: String::new(),
2691 access_token: None,
2692 }
2693 }
2694}
2695
2696#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2697#[serde(rename_all = "camelCase")]
2698pub enum RelayPolicy {
2699 Auto,
2700 QuicRequired,
2701 WebsocketRequired,
2702}
2703
2704#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2711#[serde(rename_all = "kebab-case")]
2712pub enum NetworkPolicy {
2713 #[default]
2714 Managed,
2715 LocalOnly,
2716}
2717
2718#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2719#[serde(rename_all = "camelCase")]
2720pub struct TransportConfig {
2721 #[serde(default)]
2722 pub network_policy: NetworkPolicy,
2723 #[serde(default = "default_true")]
2726 pub relay: bool,
2727 #[serde(default)]
2729 pub privacy_mode: bool,
2730 #[serde(default)]
2733 pub iroh_relay_only: bool,
2734 #[serde(default, skip_serializing_if = "Option::is_none")]
2735 pub iroh_relay_transport_policy: Option<RelayPolicy>,
2736 #[serde(default, skip_serializing_if = "Option::is_none")]
2737 pub iroh_lan: Option<IrohLanConfig>,
2738 #[serde(default, skip_serializing_if = "Option::is_none")]
2739 pub webrtc: Option<WebRTCConfig>,
2740 #[serde(default, skip_serializing_if = "Option::is_none")]
2741 pub moq: Option<MoQConfig>,
2742 #[serde(default, skip_serializing_if = "Option::is_none")]
2743 pub ble: Option<BleConfig>,
2744 #[serde(default = "default_route_priority")]
2747 pub route_priority: Vec<crate::route_policy::KnownRoute>,
2748 #[serde(default)]
2749 pub optimize_for: crate::route_policy::RouteOptimization,
2750}
2751
2752fn default_route_priority() -> Vec<crate::route_policy::KnownRoute> {
2753 crate::route_policy::DEFAULT_ROUTE_PRIORITY.to_vec()
2754}
2755
2756fn default_true() -> bool {
2757 true
2758}
2759
2760impl Default for TransportConfig {
2761 fn default() -> Self {
2762 Self {
2763 network_policy: NetworkPolicy::Managed,
2764 relay: true,
2765 privacy_mode: false,
2766 iroh_relay_only: false,
2767 iroh_relay_transport_policy: None,
2768 iroh_lan: Some(IrohLanConfig::default()),
2769 webrtc: None,
2770 moq: None,
2771 ble: None,
2772 route_priority: default_route_priority(),
2773 optimize_for: crate::route_policy::RouteOptimization::Balanced,
2774 }
2775 }
2776}
2777
2778impl TransportConfig {
2779 #[cfg(any(test, feature = "transport-webrtc", feature = "transport-moq"))]
2783 pub(crate) fn ranked_iroh_carriers(
2784 &self,
2785 supports_webrtc: bool,
2786 supports_moq: bool,
2787 ) -> Vec<crate::route_policy::KnownRoute> {
2788 crate::route_policy::rank_iroh_carriers(
2789 &self.route_priority,
2790 supports_webrtc,
2791 supports_moq,
2792 self.privacy_mode,
2793 self.webrtc
2794 .as_ref()
2795 .is_some_and(|webrtc| webrtc.privacy_mode),
2796 )
2797 }
2798
2799 #[cfg(not(target_arch = "wasm32"))]
2803 pub(crate) fn endpoint_rebind_required(&self, next: &Self) -> bool {
2804 self.network_policy != next.network_policy
2805 || self.relay != next.relay
2806 || self.iroh_relay_only != next.iroh_relay_only
2807 || self.iroh_relay_transport_policy != next.iroh_relay_transport_policy
2808 || self.iroh_lan != next.iroh_lan
2809 }
2810
2811 pub fn sanitize_for_runtime(self) -> Self {
2812 self.sanitize_for_runtime_with_ble_available(false)
2813 }
2814
2815 fn sanitize_for_client_construction(self) -> Self {
2821 #[cfg(not(target_arch = "wasm32"))]
2822 let deferred_ble_requested = self.ble.as_ref().is_some_and(|config| config.enabled);
2823 #[cfg(target_arch = "wasm32")]
2824 let deferred_ble_requested = false;
2825 self.sanitize_for_runtime_with_ble_available(deferred_ble_requested)
2826 }
2827
2828 pub(crate) fn sanitize_for_runtime_with_ble_available(mut self, ble_available: bool) -> Self {
2829 let mut seen_routes = std::collections::HashSet::new();
2830 self.route_priority
2831 .retain(|route| seen_routes.insert(*route));
2832 if self.route_priority.is_empty() {
2833 self.route_priority = default_route_priority();
2834 }
2835
2836 if self.network_policy == NetworkPolicy::LocalOnly {
2837 self.relay = false;
2838 self.privacy_mode = false;
2839 self.iroh_relay_only = false;
2840 self.iroh_relay_transport_policy = None;
2841 self.iroh_lan = Some(IrohLanConfig {
2842 enabled: true,
2843 advertise: self
2844 .iroh_lan
2845 .as_ref()
2846 .map(|lan| lan.advertise)
2847 .unwrap_or(true),
2848 });
2849 self.webrtc = None;
2850 self.moq = None;
2851 self.route_priority.retain(|route| {
2852 matches!(
2853 route,
2854 crate::route_policy::KnownRoute::IrohLan
2855 | crate::route_policy::KnownRoute::IrohQuic
2856 | crate::route_policy::KnownRoute::Ble
2857 )
2858 });
2859 if self.route_priority.is_empty() {
2860 self.route_priority = vec![
2861 crate::route_policy::KnownRoute::IrohLan,
2862 crate::route_policy::KnownRoute::IrohQuic,
2863 crate::route_policy::KnownRoute::Ble,
2864 ];
2865 }
2866 }
2867
2868 if self.privacy_mode && self.relay {
2869 self.iroh_relay_only = true;
2870 self.iroh_lan = None;
2871 self.ble = None;
2872 self.route_priority.retain(|route| {
2873 !matches!(
2874 route,
2875 crate::route_policy::KnownRoute::IrohLan
2876 | crate::route_policy::KnownRoute::IrohQuic
2877 | crate::route_policy::KnownRoute::Ble
2878 )
2879 });
2880 if let Some(webrtc) = self.webrtc.as_mut() {
2881 webrtc.privacy_mode = true;
2882 webrtc.lan_mode = false;
2883 for server in &mut webrtc.ice_servers {
2884 server.urls.retain(|url| {
2885 let url = url.trim().to_ascii_lowercase();
2886 url.starts_with("turn:") || url.starts_with("turns:")
2887 });
2888 }
2889 webrtc.ice_servers.retain(|server| !server.urls.is_empty());
2890 }
2891 }
2892
2893 if !self.relay {
2894 self.privacy_mode = false;
2895 self.iroh_relay_only = false;
2896 self.iroh_relay_transport_policy = None;
2897 self.moq = None;
2898 self.route_priority.retain(|route| {
2899 !matches!(
2900 route,
2901 crate::route_policy::KnownRoute::IrohRelay
2902 | crate::route_policy::KnownRoute::Moq
2903 )
2904 });
2905 if self.route_priority.is_empty() {
2906 self.route_priority = vec![
2907 crate::route_policy::KnownRoute::IrohLan,
2908 crate::route_policy::KnownRoute::IrohQuic,
2909 crate::route_policy::KnownRoute::WebRtc,
2910 crate::route_policy::KnownRoute::Ble,
2911 ];
2912 }
2913 if let Some(webrtc) = self.webrtc.as_mut() {
2914 webrtc.privacy_mode = false;
2915 for server in &mut webrtc.ice_servers {
2916 server.urls.retain(|url| {
2917 let url = url.trim().to_ascii_lowercase();
2918 !url.starts_with("turn:") && !url.starts_with("turns:")
2919 });
2920 }
2921 webrtc.ice_servers.retain(|server| !server.urls.is_empty());
2922 }
2923 }
2924
2925 if self.ble.as_ref().map(|ble| ble.enabled).unwrap_or(false) && !ble_available {
2926 self.ble = None;
2927 }
2928
2929 self
2930 }
2931}
2932
2933pub struct ClientBuilder {
2934 #[cfg(not(target_arch = "wasm32"))]
2935 project_id: String,
2936 app_tag: String,
2937 transport_config: TransportConfig,
2938 #[cfg(not(target_arch = "wasm32"))]
2939 token_provider: Arc<dyn Fn() -> Option<String> + Send + Sync>,
2940 signaling: Option<Arc<dyn SignalingBackend>>,
2941 room: Option<Arc<dyn RoomBackend>>,
2942}
2943
2944impl ClientBuilder {
2945 pub fn new_provider_neutral(
2946 app_tag: String,
2947 identity_credential_provider: Box<dyn Fn() -> Option<String> + Send + Sync>,
2948 ) -> Self {
2949 #[cfg(target_arch = "wasm32")]
2950 let _ = identity_credential_provider;
2951 Self {
2952 #[cfg(not(target_arch = "wasm32"))]
2956 project_id: String::new(),
2957 app_tag,
2958 transport_config: TransportConfig::default(),
2959 #[cfg(not(target_arch = "wasm32"))]
2960 token_provider: Arc::from(identity_credential_provider),
2961 signaling: None,
2962 room: None,
2963 }
2964 }
2965
2966 #[cfg(test)]
2969 pub(crate) fn new_for_test(
2970 project_id: String,
2971 api_key: String,
2972 token_provider: Box<dyn Fn() -> Option<String> + Send + Sync>,
2973 ) -> Self {
2974 Self::new_with_app_tag(
2975 project_id,
2976 crate::app_tag_from_api_key(&api_key),
2977 token_provider,
2978 )
2979 }
2980
2981 #[cfg(test)]
2982 pub fn new_with_app_tag(
2983 project_id: String,
2984 app_tag: String,
2985 token_provider: Box<dyn Fn() -> Option<String> + Send + Sync>,
2986 ) -> Self {
2987 Self {
2988 #[cfg(not(target_arch = "wasm32"))]
2989 project_id,
2990 app_tag,
2991 transport_config: TransportConfig::default(),
2992 #[cfg(not(target_arch = "wasm32"))]
2993 token_provider: Arc::from(token_provider),
2994 signaling: None,
2995 room: None,
2996 }
2997 }
2998
2999 pub fn transport_config(mut self, transport_config: TransportConfig) -> Self {
3000 self.transport_config = transport_config.sanitize_for_client_construction();
3001 self
3002 }
3003
3004 pub fn signaling_backend(mut self, signaling: Arc<dyn SignalingBackend>) -> Self {
3005 self.signaling = Some(signaling);
3006 self
3007 }
3008
3009 pub fn room_backend(mut self, room: Arc<dyn RoomBackend>) -> Self {
3010 self.room = Some(room);
3011 self
3012 }
3013
3014 pub fn build(self) -> Client {
3015 let app_backgrounded = Arc::new(AtomicBool::new(false));
3016
3017 let room = self
3018 .room
3019 .unwrap_or_else(|| Arc::new(GatewayRequiredRoomBackend) as Arc<dyn RoomBackend>);
3020
3021 let signaling = self.signaling.unwrap_or_else(|| {
3022 Arc::new(GatewayRequiredSignalingBackend) as Arc<dyn SignalingBackend>
3023 });
3024
3025 #[cfg(not(target_arch = "wasm32"))]
3026 let (native_device_updates, _) = tokio::sync::broadcast::channel(32);
3027 #[cfg(not(target_arch = "wasm32"))]
3028 let (native_connection_state_updates, _) = tokio::sync::broadcast::channel(64);
3029 #[cfg(not(target_arch = "wasm32"))]
3030 let (native_peer_data_updates, _) = tokio::sync::broadcast::channel(256);
3031 #[cfg(not(target_arch = "wasm32"))]
3032 let (native_application_streams, native_application_streams_receiver) =
3033 async_channel::bounded(256);
3034
3035 Client {
3036 app_tag: self.app_tag,
3037 broadcasts: crate::broadcast::Broadcasts::default(),
3038 signaling,
3039 room,
3040 node_id: Arc::new(RwLock::new(None)),
3041 iroh_endpoint: Arc::new(RwLock::new(None)),
3042 #[cfg(target_arch = "wasm32")]
3043 iroh_node: Arc::new(RwLock::new(None)),
3044 #[cfg(not(target_arch = "wasm32"))]
3045 iroh_node: Arc::new(RwLock::new(None)),
3046 #[cfg(not(target_arch = "wasm32"))]
3047 native_application_streams,
3048 #[cfg(not(target_arch = "wasm32"))]
3049 native_application_streams_receiver,
3050 connection_manager: Arc::new(crate::connection_manager::ConnectionManager::new()),
3051 session_token_registry: Arc::new(crate::session_token::SessionTokenRegistry::new()),
3052 #[cfg(not(target_arch = "wasm32"))]
3053 inbound_session_admission_transport_ids: Arc::new(StdRwLock::new(HashMap::new())),
3054 #[cfg(not(target_arch = "wasm32"))]
3055 offline_admission_requirements: Arc::new(StdRwLock::new(HashMap::new())),
3056 #[cfg(not(target_arch = "wasm32"))]
3057 offline_admission_transport_ids: Arc::new(StdRwLock::new(HashMap::new())),
3058 #[cfg(not(target_arch = "wasm32"))]
3059 offline_runtime: Arc::new(tokio::sync::Mutex::new(None)),
3060 #[cfg(not(target_arch = "wasm32"))]
3061 offline_proof_attempts: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
3062 #[cfg(not(target_arch = "wasm32"))]
3063 native_admission_stream_contracts: Arc::new(StdRwLock::new(HashMap::new())),
3064 remote_session_admission_proofs: Arc::new(StdRwLock::new(HashMap::new())),
3065 pending_inline_reciprocal_admissions: Arc::new(StdRwLock::new(HashMap::new())),
3066 outbound_application_security_epoch_fingerprints: Arc::new(StdRwLock::new(
3067 HashMap::new(),
3068 )),
3069 #[cfg(not(target_arch = "wasm32"))]
3070 pending_reciprocal_session_admission_requests: Arc::new(StdRwLock::new(HashSet::new())),
3071 #[cfg(not(target_arch = "wasm32"))]
3072 native_route_repair_credentials: Arc::new(StdRwLock::new(HashMap::new())),
3073 scoped_route_repair_credentials: Arc::new(StdRwLock::new(HashMap::new())),
3074 connection_application_crypto_keys:
3075 crate::client::application_crypto_impl::new_connection_application_crypto_key_map(),
3076 connection_application_crypto_state: Arc::new(StdRwLock::new(())),
3077 connection_application_crypto_required:
3078 crate::client::application_crypto_impl::new_connection_application_crypto_required_set(),
3079 #[cfg(not(target_arch = "wasm32"))]
3080 trusted_user_device_application_crypto_required: Arc::new(AtomicBool::new(false)),
3081 connection_application_crypto_confirmed:
3082 crate::client::application_crypto_impl::new_connection_application_crypto_confirmed_map(),
3083 #[cfg(not(target_arch = "wasm32"))]
3084 connection_application_route_updates: Arc::new(tokio::sync::Notify::new()),
3085 connection_application_crypto_outbound_sequences:
3086 crate::client::application_crypto_impl::new_connection_application_crypto_outbound_sequences(),
3087 connection_application_key_agreements:
3088 crate::client::application_crypto_impl::new_connection_application_key_agreement_map(),
3089 #[cfg(not(target_arch = "wasm32"))]
3090 managed_scope_tickets: Arc::new(StdRwLock::new(HashMap::new())),
3091 known_endpoint_addrs: Arc::new(RwLock::new(HashMap::new())),
3092 known_device_ids_by_node: Arc::new(StdRwLock::new(HashMap::new())),
3093 known_device_endpoint_revision: tokio::sync::watch::channel(0).0,
3094 sparse_fanout: Arc::new(tokio::sync::Mutex::new(
3095 crate::sparse_fanout::SparseFanoutState::default(),
3096 )),
3097 #[cfg(not(target_arch = "wasm32"))]
3098 native_device_identity: Arc::new(RwLock::new(None)),
3099 #[cfg(not(target_arch = "wasm32"))]
3100 native_device_base_dir: Arc::new(RwLock::new(None)),
3101 #[cfg(not(target_arch = "wasm32"))]
3102 native_device_identity_init_guard: Arc::new(tokio::sync::Mutex::new(())),
3103 #[cfg(not(target_arch = "wasm32"))]
3104 native_device_updates,
3105 #[cfg(not(target_arch = "wasm32"))]
3106 native_connection_state_updates,
3107 #[cfg(not(target_arch = "wasm32"))]
3108 native_peer_data_updates,
3109 auto_connect_loop_key: Arc::new(Mutex::new(None)),
3110 auto_connect_generation: Arc::new(AtomicU64::new(0)),
3111 #[cfg(not(target_arch = "wasm32"))]
3112 native_auto_connect_wake: Arc::new(tokio::sync::Notify::new()),
3113 #[cfg(not(target_arch = "wasm32"))]
3114 external_desired_peer_actor: Arc::new(tokio::sync::Mutex::new(
3115 auto_connect_impl::NativeExternalAutoConnectActorState::default(),
3116 )),
3117 auto_connect_exclusion_owner: Arc::new(Mutex::new(())),
3118 auto_connect_excluded: Arc::new(Mutex::new(HashSet::new())),
3119 auto_connect_peer_requested_excluded: Arc::new(Mutex::new(HashSet::new())),
3120 auto_connect_excluded_node_aliases: Arc::new(Mutex::new(HashMap::new())),
3121 app_backgrounded,
3122 background_execution_allowed: Arc::new(AtomicBool::new(false)),
3123 #[cfg(target_arch = "wasm32")]
3124 wasm_accept_bridge_started: Arc::new(std::sync::atomic::AtomicBool::new(false)),
3125 #[cfg(target_arch = "wasm32")]
3126 last_emitted_connection_states: Arc::new(Mutex::new(std::collections::HashMap::new())),
3127 #[cfg(target_arch = "wasm32")]
3128 last_empty_peer_sessions_warning_ms: Arc::new(AtomicU64::new(0)),
3129 iroh_init_guard: Arc::new(tokio::sync::Mutex::new(())),
3130 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
3131 iroh_packet_carriers: Arc::new(RwLock::new(HashMap::new())),
3132 managed_connect_gates: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
3133 #[cfg(not(target_arch = "wasm32"))]
3134 iroh_path_watcher_stable_ids: Arc::new(Mutex::new(HashSet::new())),
3135 #[cfg(not(target_arch = "wasm32"))]
3136 native_custom_transport_kinds: Arc::new(RwLock::new(HashMap::new())),
3137 #[cfg(not(target_arch = "wasm32"))]
3138 native_transport_upgrade_providers: Arc::new(RwLock::new(HashMap::new())),
3139 #[cfg(target_arch = "wasm32")]
3140 wasm_transport_upgrade_gates: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
3141 #[cfg(not(target_arch = "wasm32"))]
3142 native_transport_upgrade_gates: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
3143 #[cfg(not(target_arch = "wasm32"))]
3144 native_ble_upgrade_attempts: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
3145 #[cfg(not(target_arch = "wasm32"))]
3146 native_peer_transport_capabilities: Arc::new(RwLock::new(HashMap::new())),
3147 presence_loop_tx: Arc::new(Mutex::new(None)),
3148 #[cfg(not(target_arch = "wasm32"))]
3149 project_id: self.project_id,
3150 #[cfg(not(target_arch = "wasm32"))]
3151 token_provider: self.token_provider,
3152 transport_config: Arc::new(RwLock::new(
3153 self.transport_config.sanitize_for_client_construction(),
3154 )),
3155 iroh_carrier_policy_epoch: Arc::new(AtomicU64::new(0)),
3156 iroh_carrier_peer_policy_epochs: Arc::new(StdRwLock::new(HashMap::new())),
3157 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
3158 local_discovery_registry: crate::local_discovery::LocalDiscoveryRegistry::new(),
3159 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
3160 mdns_address_lookup: Arc::new(RwLock::new(None)),
3161 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
3162 native_webrtc_carrier_attempts: Arc::new(RwLock::new(HashMap::new())),
3163 #[cfg(all(
3164 not(target_arch = "wasm32"),
3165 feature = "iroh-carrier-core",
3166 any(feature = "test-harness", feature = "testing-endpoints")
3167 ))]
3168 native_iroh_carrier_debug_events: Arc::new(StdRwLock::new(
3169 std::collections::VecDeque::new(),
3170 )),
3171 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3172 native_moq_carrier_attempts: Arc::new(RwLock::new(HashMap::new())),
3173 #[cfg(not(target_arch = "wasm32"))]
3174 native_control_streams: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
3175 #[cfg(not(target_arch = "wasm32"))]
3176 native_transport_protocol_activity: Arc::new(StdRwLock::new(HashMap::new())),
3177 #[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
3178 scoped_connection_actor_registry: Arc::new(RwLock::new(None)),
3179 #[cfg(not(target_arch = "wasm32"))]
3180 auth_readiness: Arc::new(crate::client::auth_readiness::AuthReadinessStore::new()),
3181 #[cfg(not(target_arch = "wasm32"))]
3182 scope_classifier: Arc::new(RwLock::new(default_classifier())),
3183 }
3184 }
3185}
3186
3187mod admission_impl;
3195mod application_crypto_impl;
3196#[cfg(not(target_arch = "wasm32"))]
3197pub mod auth_readiness;
3198mod auto_connect_impl;
3199mod core_impl;
3200#[cfg(not(target_arch = "wasm32"))]
3201pub mod correlation;
3202#[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
3203mod drive_grant_actor;
3204#[cfg(all(
3205 test,
3206 not(target_arch = "wasm32"),
3207 feature = "experimental-scoped-actor"
3208))]
3209mod drive_grant_actor_tests;
3210#[cfg(not(target_arch = "wasm32"))]
3211pub mod scope_classifier;
3212#[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
3213pub mod scoped_connection_actor;
3214mod sparse_fanout_impl;
3215mod state_signaling_impl;
3216#[cfg(not(target_arch = "wasm32"))]
3217mod transport_upgrade_impl;
3218#[cfg(all(
3219 target_arch = "wasm32",
3220 any(feature = "transport-webrtc", feature = "transport-moq")
3221))]
3222mod wasm_carrier_impl;
3223#[cfg(all(not(target_arch = "wasm32"), test))]
3224pub(crate) use transport_upgrade_impl::{
3225 retire_losing_native_control_candidate, retire_replaced_native_control_send,
3226};
3227
3228impl Client {
3229 pub fn broadcasts(&self) -> crate::broadcast::Broadcasts {
3232 self.broadcasts.clone()
3233 }
3234}
3235
3236#[cfg(any(
3237 not(target_arch = "wasm32"),
3238 feature = "transport-webrtc",
3239 feature = "transport-moq"
3240))]
3241impl Client {
3242 pub(crate) fn current_iroh_carrier_policy_epoch(&self) -> u64 {
3243 self.iroh_carrier_policy_epoch.load(Ordering::Acquire)
3244 }
3245
3246 #[cfg(feature = "iroh-carrier-core")]
3247 pub(crate) fn bump_iroh_carrier_policy_epoch(&self) -> u64 {
3248 self.iroh_carrier_policy_epoch
3249 .fetch_add(1, Ordering::AcqRel)
3250 .saturating_add(1)
3251 }
3252
3253 #[cfg(feature = "iroh-carrier-core")]
3254 pub(crate) fn iroh_carrier_policy_epoch_is_current(&self, policy_epoch: u64) -> bool {
3255 self.current_iroh_carrier_policy_epoch() == policy_epoch
3256 }
3257
3258 pub(crate) fn current_iroh_carrier_peer_policy_epoch(&self, connection_id: &str) -> u64 {
3259 self.iroh_carrier_peer_policy_epochs
3260 .read()
3261 .ok()
3262 .and_then(|epochs| epochs.get(connection_id).copied())
3263 .unwrap_or_default()
3264 }
3265
3266 #[cfg(feature = "iroh-carrier-core")]
3267 pub(crate) fn bump_iroh_carrier_peer_policy_epoch(&self, connection_id: &str) -> u64 {
3268 let Ok(mut epochs) = self.iroh_carrier_peer_policy_epochs.write() else {
3269 return self.current_iroh_carrier_peer_policy_epoch(connection_id);
3270 };
3271 let next = epochs
3272 .get(connection_id)
3273 .copied()
3274 .unwrap_or_default()
3275 .saturating_add(1);
3276 epochs.insert(connection_id.to_string(), next);
3277 next
3278 }
3279
3280 #[cfg(feature = "iroh-carrier-core")]
3281 pub(crate) fn forget_iroh_carrier_peer_policy_epoch(&self, connection_id: &str) {
3282 if let Ok(mut epochs) = self.iroh_carrier_peer_policy_epochs.write() {
3283 epochs.remove(connection_id);
3284 }
3285 }
3286}
3287
3288#[cfg(target_arch = "wasm32")]
3293impl Client {
3294 pub(crate) async fn set_relay(&self, enabled: bool) -> anyhow::Result<()> {
3295 anyhow::ensure!(
3296 self.iroh_endpoint.read().await.is_none(),
3297 "relay must be configured before Iroh endpoint initialization",
3298 );
3299 let mut config = self.transport_config.read().await.clone();
3300 config.relay = enabled;
3301 *self.transport_config.write().await = config.sanitize_for_runtime();
3302 Ok(())
3303 }
3304
3305 pub async fn send_peer(&self, _id: &str, _data: &[u8]) -> anyhow::Result<()> {
3306 Err(anyhow::anyhow!(
3307 "send_peer: use TypeScript Connection.sendTyped() for transport-aware sending in browser environments"
3308 ))
3309 }
3310}
3311
3312#[cfg(not(target_arch = "wasm32"))]
3313impl Client {
3314 pub fn subscribe_native_peer_data(
3315 &self,
3316 ) -> tokio::sync::broadcast::Receiver<NativePeerDataEvent> {
3317 self.native_peer_data_updates.subscribe()
3318 }
3319
3320 #[cfg_attr(
3321 not(any(feature = "transport-webrtc", feature = "transport-moq")),
3322 allow(dead_code)
3323 )]
3324 pub(crate) fn emit_native_peer_data(&self, event: NativePeerDataEvent) {
3325 let _ = self.native_peer_data_updates.send(event);
3326 }
3327}
3328
3329#[cfg(test)]
3330mod tests;
3331
3332#[cfg(all(test, not(target_arch = "wasm32")))]
3333mod sparse_iroh_lane_tests;
3334
3335#[cfg(all(test, not(target_arch = "wasm32")))]
3336mod relay_diagnostic_truth_tests {
3337 use super::*;
3338
3339 #[test]
3340 fn auto_reports_requested_and_effective_relay_carriage() {
3341 let diagnostic = RelayStatus::from_policy(RelayPolicy::Auto, current_iroh_relay_provider());
3342
3343 assert_eq!(diagnostic.requested_carriage, RelayPolicy::Auto);
3344 let serialized = serde_json::to_value(&diagnostic).expect("relay diagnostic serializes");
3345 assert_eq!(serialized["requestedCarriage"], "auto");
3346 #[cfg(openrtc_iroh_relay_transport_policy_api)]
3347 assert_eq!(
3348 diagnostic.effective_carriage,
3349 RelayCarriage::QuicWithWebsocketFallback
3350 );
3351 #[cfg(not(openrtc_iroh_relay_transport_policy_api))]
3352 assert_eq!(
3353 diagnostic.effective_carriage,
3354 RelayCarriage::WebsocketFallback
3355 );
3356 }
3357
3358 #[cfg(not(openrtc_iroh_relay_transport_policy_api))]
3359 #[test]
3360 fn upstream_quic_required_fails_closed_before_endpoint_bind() {
3361 let result = apply_native_network_preferences(
3362 iroh::Endpoint::builder(iroh::endpoint::presets::N0),
3363 false,
3364 RelayPolicy::QuicRequired,
3365 );
3366
3367 assert!(result.is_err());
3368 assert!(result
3369 .err()
3370 .expect("unsupported QUIC policy should return an error")
3371 .to_string()
3372 .contains("unsupported by this upstream-Iroh build"));
3373 }
3374
3375 #[cfg(openrtc_iroh_relay_transport_policy_api)]
3376 #[test]
3377 fn vendored_quic_required_reports_quic_and_keeps_endpoint_policy_available() {
3378 let diagnostic =
3379 RelayStatus::from_policy(RelayPolicy::QuicRequired, current_iroh_relay_provider());
3380
3381 assert_eq!(diagnostic.effective_carriage, RelayCarriage::Quic);
3382 assert_eq!(diagnostic.provider, IrohRelayProvider::VendoredIroh);
3383 }
3384}