1use super::*;
7
8#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
9fn native_iroh_carrier_failure_code(error: &anyhow::Error) -> &'static str {
10 let message = format!("{error:#}");
11 if message.contains("base generation changed before candidate acknowledgement")
12 || message.contains("incumbent generation changed")
13 || message.contains("replacement incumbent is stale")
14 {
15 "carrier-base-generation-stale"
16 } else if message.contains("authorization epoch changed") {
17 "carrier-authorization-stale"
18 } else if message.contains("stale before atomic commit")
19 || message.contains("became stale during atomic commit")
20 {
21 "carrier-logical-generation-stale"
22 } else {
23 "carrier-proof-failed"
24 }
25}
26
27#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
36#[derive(Debug, Clone)]
37pub(super) struct NativeIrohCarrierAuthorizationFence {
38 inbound_admission_was_current: bool,
39 remote_admission_proof: Option<RemoteSessionAdmissionProof>,
40 admission_fingerprint: Option<String>,
41 application_security_epoch_fingerprint: Option<String>,
42 policy_epoch: u64,
43}
44
45#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
46fn moq_retry_attempt(failure_code: &str, attempt: u8) -> Option<u8> {
47 (failure_code == "carrier-base-generation-stale" && attempt == 1).then_some(2)
48}
49
50#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
51fn native_moq_peer_failure_code(failure_code: Option<&str>) -> (&str, &'static str) {
52 let diagnostic = failure_code.unwrap_or("peer-rejected-carrier");
53 let lifecycle = match diagnostic {
54 "carrier-base-generation-stale" => "carrier-base-generation-stale",
55 "carrier-proof-failed" => "carrier-proof-failed",
56 _ => "peer-rejected-carrier",
57 };
58 (diagnostic, lifecycle)
59}
60
61#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
62use crate::iroh_carrier_bootstrap::{
63 webrtc_peer_failure_code, webrtc_retry_attempt, webrtc_retry_delay,
64};
65
66#[cfg(not(target_arch = "wasm32"))]
67type NativeMainSendFuture<'a> =
68 std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), String>> + Send + 'a>>;
69
70#[cfg(not(target_arch = "wasm32"))]
71fn native_control_stream_rank(stream_id: iroh::endpoint::StreamId) -> u64 {
72 let initiator_rank = match stream_id.initiator() {
73 iroh::endpoint::Side::Client => 0,
74 iroh::endpoint::Side::Server => 1,
75 };
76 stream_id.index().saturating_mul(2) + initiator_rank
77}
78
79#[cfg(not(target_arch = "wasm32"))]
80fn should_replace_native_control_stream(
81 existing: NativeControlStreamOwner,
82 incoming: NativeControlStreamOwner,
83) -> bool {
84 existing.transport_stable_id != incoming.transport_stable_id
85 || incoming.stream_rank < existing.stream_rank
86}
87
88#[cfg(not(target_arch = "wasm32"))]
89const MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES: u8 = 16;
90
91#[cfg(not(target_arch = "wasm32"))]
92fn should_drain_replaced_native_control_frame(drained_frames: u8) -> bool {
93 drained_frames < MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES
94}
95
96#[cfg(not(target_arch = "wasm32"))]
97pub(crate) async fn retire_losing_native_control_candidate(
98 send: iroh::endpoint::SendStream,
99 recv: iroh::endpoint::RecvStream,
100) -> std::io::Result<()> {
101 let delivery = crate::application_crypto_streams::PeerSendStream::plain(send)
102 .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
103 .await;
104 drop(recv);
105 delivery
106}
107
108#[cfg(not(target_arch = "wasm32"))]
109pub(crate) async fn retire_replaced_native_control_send(
110 send: Arc<tokio::sync::Mutex<iroh::endpoint::SendStream>>,
111) -> std::io::Result<()> {
112 let mut send = send.lock().await;
113 send.finish()
114 .map_err(|error| std::io::Error::other(format!("finish replaced stream: {error}")))?;
115 match tokio::time::timeout(std::time::Duration::from_secs(2), send.stopped()).await {
116 Ok(Ok(_)) => Ok(()),
117 Ok(Err(error)) => Err(std::io::Error::other(format!(
118 "wait for peer acknowledgement: {error}"
119 ))),
120 Err(_) => Err(std::io::Error::new(
121 std::io::ErrorKind::TimedOut,
122 "timed out waiting for peer acknowledgement",
123 )),
124 }
125}
126
127#[cfg(not(target_arch = "wasm32"))]
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129enum NativeBleSwitchAdmission {
130 Ready,
131 Wait,
132 Reject,
133}
134
135#[cfg(not(target_arch = "wasm32"))]
136const NATIVE_BLE_MAX_UPGRADE_ATTEMPTS: u8 = 3;
137
138#[cfg(not(target_arch = "wasm32"))]
139const NATIVE_BLE_RETRY_BASE_DELAY_MS: u64 = 1_000;
140
141#[cfg(not(target_arch = "wasm32"))]
142const NATIVE_BLE_SWITCH_RESPONSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
143
144#[cfg(not(target_arch = "wasm32"))]
145const NATIVE_BLE_COMPLETION_GATE_PREFIX: &str = "completion:";
146
147#[cfg(not(target_arch = "wasm32"))]
148fn native_ble_retry_delay(attempt: u8, jitter_seed: u64) -> Option<std::time::Duration> {
149 if attempt >= NATIVE_BLE_MAX_UPGRADE_ATTEMPTS {
150 return None;
151 }
152 let exponent = u32::from(attempt.saturating_sub(1));
153 let base_ms = NATIVE_BLE_RETRY_BASE_DELAY_MS.saturating_mul(2_u64.saturating_pow(exponent));
154 let jitter_percent = i64::try_from(jitter_seed % 41).unwrap_or(0) - 20;
155 let jitter_ms = i128::from(base_ms) * i128::from(jitter_percent) / 100;
156 let delay_ms = (i128::from(base_ms) + jitter_ms).max(1) as u64;
157 Some(std::time::Duration::from_millis(delay_ms))
158}
159
160#[cfg(not(target_arch = "wasm32"))]
161fn native_ble_attempt_state_after_recovery_wake(
162 current: crate::client::NativeBleUpgradeAttemptState,
163 gate_active: bool,
164) -> Option<crate::client::NativeBleUpgradeAttemptState> {
165 gate_active.then_some(current)
166}
167
168#[cfg(not(target_arch = "wasm32"))]
169fn reserve_native_ble_attempt_state(
170 current: Option<crate::client::NativeBleUpgradeAttemptState>,
171 generation: crate::client::NativePeerDataGeneration,
172) -> (crate::client::NativeBleUpgradeAttemptState, Option<u8>) {
173 let mut state = current
174 .filter(|state| state.generation == generation)
175 .unwrap_or(crate::client::NativeBleUpgradeAttemptState {
176 generation,
177 attempts: 0,
178 });
179 if state.attempts >= NATIVE_BLE_MAX_UPGRADE_ATTEMPTS {
180 return (state, None);
181 }
182 state.attempts = state.attempts.saturating_add(1);
183 let attempt = state.attempts;
184 (state, Some(attempt))
185}
186
187#[cfg(not(target_arch = "wasm32"))]
188fn classify_native_ble_switch_admission(
189 block: Option<&(bool, String)>,
190) -> NativeBleSwitchAdmission {
191 match block {
192 None => NativeBleSwitchAdmission::Ready,
193 Some((true, _)) => NativeBleSwitchAdmission::Reject,
194 Some((false, _)) => NativeBleSwitchAdmission::Wait,
195 }
196}
197
198fn dedicated_carrier_control_is_ready(
203 dedicated_carrier_control: bool,
204 application_crypto_confirmed: bool,
205 native_admission_route_ready: bool,
206) -> bool {
207 dedicated_carrier_control && application_crypto_confirmed && native_admission_route_ready
208}
209
210impl Client {
211 #[cfg(all(
212 not(target_arch = "wasm32"),
213 feature = "iroh-carrier-core",
214 any(feature = "test-harness", feature = "testing-endpoints")
215 ))]
216 pub fn native_iroh_carrier_debug_events(&self) -> Vec<serde_json::Value> {
217 self.native_iroh_carrier_debug_events
218 .read()
219 .map(|events| events.iter().cloned().collect())
220 .unwrap_or_default()
221 }
222
223 #[cfg(all(
224 not(target_arch = "wasm32"),
225 feature = "iroh-carrier-core",
226 any(feature = "test-harness", feature = "testing-endpoints")
227 ))]
228 pub(super) fn record_native_iroh_carrier_debug_event(
229 &self,
230 connection_id: &str,
231 upgrade_id: &str,
232 action: &'static str,
233 reason: Option<&str>,
234 ) {
235 const DEBUG_EVENT_LIMIT: usize = 256;
239 let Ok(mut events) = self.native_iroh_carrier_debug_events.write() else {
240 return;
241 };
242 if events.len() == DEBUG_EVENT_LIMIT {
243 events.pop_front();
244 }
245 events.push_back(serde_json::json!({
246 "connectionId": connection_id,
247 "upgradeId": upgrade_id,
248 "action": action,
249 "reason": reason,
250 "observedAtMs": std::time::SystemTime::now()
251 .duration_since(std::time::UNIX_EPOCH)
252 .map(|duration| duration.as_millis() as u64)
253 .unwrap_or_default(),
254 }));
255 if Self::admission_trace_enabled() {
256 eprintln!(
257 "[OpenRTC][Iroh carrier][candidate] connection_id={} upgrade_id={} action={} reason={}",
258 connection_id,
259 upgrade_id,
260 action,
261 reason.unwrap_or("none"),
262 );
263 }
264 }
265
266 #[cfg(all(
267 not(target_arch = "wasm32"),
268 feature = "iroh-carrier-core",
269 not(any(feature = "test-harness", feature = "testing-endpoints"))
270 ))]
271 pub(super) fn record_native_iroh_carrier_debug_event(
272 &self,
273 _connection_id: &str,
274 _upgrade_id: &str,
275 _action: &'static str,
276 _reason: Option<&str>,
277 ) {
278 }
279
280 #[cfg(not(target_arch = "wasm32"))]
281 pub(crate) fn send_native_main_frame<'a>(
282 &'a self,
283 connection_id: &'a str,
284 endpoint_id: iroh::EndpointId,
285 signal_type: &'a str,
286 frame_buf: &'a [u8],
287 ) -> NativeMainSendFuture<'a> {
288 Box::pin(self.send_native_main_frame_inner(
289 connection_id,
290 endpoint_id,
291 signal_type,
292 frame_buf,
293 ))
294 }
295
296 #[cfg(not(target_arch = "wasm32"))]
297 async fn send_native_main_frame_inner(
298 &self,
299 connection_id: &str,
300 endpoint_id: iroh::EndpointId,
301 signal_type: &str,
302 frame_buf: &[u8],
303 ) -> Result<(), String> {
304 use tokio::io::AsyncWriteExt;
305
306 let current_transport_stable_id = self
307 .get_connection(endpoint_id)
308 .await
309 .map(|connection| crate::transport_generation::for_connection(&connection))
310 .ok_or_else(|| format!("missing Iroh connection for endpoint {}", endpoint_id))?;
311
312 let cached = {
314 let map = self.native_control_streams.lock().await;
315 map.get(connection_id).cloned()
316 };
317
318 if let Some(entry) = cached {
319 if entry.endpoint_id != endpoint_id
320 || entry.owner.transport_stable_id != current_transport_stable_id
321 {
322 self.evict_native_signal_stream_if_owner(
323 connection_id,
324 entry.owner,
325 "cached-owner-stale",
326 )
327 .await;
328 } else {
329 let mut send_guard = entry.send.lock().await;
330 match send_guard.write_all(frame_buf).await {
331 Ok(_) => {
332 if let Err(flush_err) = send_guard.flush().await {
333 eprintln!(
334 "[SIGNAL-STREAM] flush-failed-on-cached connection_id={} signal_type={} error={}",
335 connection_id, signal_type, flush_err,
336 );
337 drop(send_guard);
338 self.evict_native_signal_stream_if_owner(
339 connection_id,
340 entry.owner,
341 "cached-flush-failed",
342 )
343 .await;
344 } else {
345 eprintln!(
346 "[SIGNAL-STREAM] wrote-on-cached connection_id={} signal_type={} bytes={}",
347 connection_id,
348 signal_type,
349 frame_buf.len(),
350 );
351 return Ok(());
352 }
353 }
354 Err(err) => {
355 eprintln!(
356 "[SIGNAL-STREAM] write-failed-on-cached connection_id={} signal_type={} error={}",
357 connection_id, signal_type, err,
358 );
359 drop(send_guard);
360 self.evict_native_signal_stream_if_owner(
361 connection_id,
362 entry.owner,
363 "cached-write-failed",
364 )
365 .await;
366 }
367 }
368 }
369 }
370
371 let (transport_stable_id, mut send, recv) = self
373 .open_current_bi_internal_with_transport_stable_id(endpoint_id)
374 .await
375 .map_err(|e| format!("open_bi via iroh failed: {}", e))?;
376 if transport_stable_id != current_transport_stable_id {
377 return Err(format!(
378 "fresh native-main stream opened on a replacement transport generation expected={} actual={}",
379 current_transport_stable_id, transport_stable_id,
380 ));
381 }
382
383 eprintln!(
384 "[SIGNAL-STREAM] opened-persistent connection_id={} endpoint_id={} signal_type={} stream_id={}",
385 connection_id,
386 endpoint_id,
387 signal_type,
388 send.id().index(),
389 );
390
391 let label = b"main";
392 let label_len = (label.len() as u32).to_be_bytes();
393 let mut opening_buf = Vec::with_capacity(1 + 4 + label.len() + frame_buf.len());
394 opening_buf.push(0x00);
395 opening_buf.extend_from_slice(&label_len);
396 opening_buf.extend_from_slice(label);
397 opening_buf.extend_from_slice(frame_buf);
398
399 send.write_all(&opening_buf)
400 .await
401 .map_err(|e| format!("initial write failed: {}", e))?;
402 send.flush()
403 .await
404 .map_err(|e| format!("initial flush failed: {}", e))?;
405
406 eprintln!(
407 "[SIGNAL-STREAM] wrote-on-fresh connection_id={} signal_type={} bytes={}",
408 connection_id,
409 signal_type,
410 opening_buf.len(),
411 );
412
413 if !self
414 .install_native_control_stream(
415 connection_id,
416 endpoint_id,
417 transport_stable_id,
418 send,
419 recv,
420 "opened",
421 )
422 .await
423 {
424 return Err(format!(
425 "fresh native-main stream lost ownership race connection_id={} transport_stable_id={}",
426 connection_id, transport_stable_id
427 ));
428 }
429
430 Ok(())
431 }
432
433 #[cfg(not(target_arch = "wasm32"))]
434 pub(crate) async fn install_native_control_stream(
435 &self,
436 connection_id: &str,
437 endpoint_id: iroh::EndpointId,
438 transport_stable_id: u64,
439 send: iroh::endpoint::SendStream,
440 recv: iroh::endpoint::RecvStream,
441 source: &str,
442 ) -> bool {
443 let stream_id = send.id();
444 let owner = NativeControlStreamOwner {
445 transport_stable_id,
446 stream_rank: native_control_stream_rank(stream_id),
447 };
448 let current_transport_stable_id = self
449 .get_connection(endpoint_id)
450 .await
451 .map(|connection| crate::transport_generation::for_connection(&connection));
452 if current_transport_stable_id != Some(transport_stable_id) {
453 eprintln!(
454 "[OpenRTC][control] stale stream ignored connection_id={} endpoint_id={} transport_stable_id={} current_transport_stable_id={:?} stream_id={} source={}",
455 connection_id,
456 endpoint_id,
457 transport_stable_id,
458 current_transport_stable_id,
459 stream_id,
460 source,
461 );
462 return false;
463 }
464
465 let mut candidate_send = Some(send);
470 let (installed, replaced_entry) = {
471 let mut streams = self.native_control_streams.lock().await;
472 let replace = streams
473 .get(connection_id)
474 .is_none_or(|existing| should_replace_native_control_stream(existing.owner, owner));
475 if replace {
476 let replaced = streams.insert(
477 connection_id.to_string(),
478 NativeControlStreamEntry {
479 owner,
480 endpoint_id,
481 send: Arc::new(tokio::sync::Mutex::new(
482 candidate_send
483 .take()
484 .expect("native control candidate must be available"),
485 )),
486 },
487 );
488 (true, replaced)
489 } else {
490 (false, None)
491 }
492 };
493 if !installed {
494 let delivery_result = retire_losing_native_control_candidate(
495 candidate_send
496 .take()
497 .expect("losing native control candidate must be available"),
498 recv,
499 )
500 .await;
501 eprintln!(
502 "[OpenRTC][control] duplicate stream retired connection_id={} endpoint_id={} transport_stable_id={} stream_id={} stream_rank={} source={} verdict_delivery={}",
503 connection_id,
504 endpoint_id,
505 transport_stable_id,
506 stream_id,
507 owner.stream_rank,
508 source,
509 if delivery_result.is_ok() { "acknowledged" } else { "unconfirmed" },
510 );
511 if let Err(error) = delivery_result {
512 eprintln!(
513 "[OpenRTC][control] duplicate stream delivery wait ended connection_id={} endpoint_id={} transport_stable_id={} stream_id={} source={} error={}",
514 connection_id,
515 endpoint_id,
516 transport_stable_id,
517 stream_id,
518 source,
519 error,
520 );
521 }
522 return false;
523 }
524 let replaced_owner = replaced_entry.as_ref().map(|entry| entry.owner);
525 eprintln!(
526 "[OpenRTC][control] stream installed connection_id={} endpoint_id={} transport_stable_id={} stream_id={} stream_rank={} source={} replaced_owner={:?}",
527 connection_id,
528 endpoint_id,
529 transport_stable_id,
530 stream_id,
531 owner.stream_rank,
532 source,
533 replaced_owner,
534 );
535 if let Some(replaced) = replaced_entry {
536 let connection_id = connection_id.to_string();
537 tokio::spawn(async move {
538 let delivery_result = retire_replaced_native_control_send(replaced.send).await;
539 eprintln!(
540 "[OpenRTC][control] replaced stream retired connection_id={} endpoint_id={} transport_stable_id={} stream_rank={} verdict_delivery={}",
541 connection_id,
542 replaced.endpoint_id,
543 replaced.owner.transport_stable_id,
544 replaced.owner.stream_rank,
545 if delivery_result.is_ok() { "acknowledged" } else { "unconfirmed" },
546 );
547 if let Err(error) = delivery_result {
548 eprintln!(
549 "[OpenRTC][control] replaced stream delivery wait ended connection_id={} endpoint_id={} transport_stable_id={} stream_rank={} error={}",
550 connection_id,
551 replaced.endpoint_id,
552 replaced.owner.transport_stable_id,
553 replaced.owner.stream_rank,
554 error,
555 );
556 }
557 });
558 }
559 self.spawn_native_signal_stream_tasks(connection_id, endpoint_id, owner, recv);
560 true
561 }
562
563 #[cfg(not(target_arch = "wasm32"))]
564 fn spawn_native_signal_stream_tasks(
565 &self,
566 connection_id: &str,
567 endpoint_id: iroh::EndpointId,
568 owner: NativeControlStreamOwner,
569 recv: iroh::endpoint::RecvStream,
570 ) {
571 let (frame_tx, frame_rx) =
572 tokio::sync::mpsc::channel::<crate::native_protocol::ParsedMainFrame>(32);
573 let connection_id_owned = connection_id.to_string();
574 let endpoint_id_for_log = endpoint_id.to_string();
575
576 let reader_client = self.clone();
577 let reader_connection_id = connection_id_owned.clone();
578 let reader_endpoint_id = endpoint_id_for_log.clone();
579 tokio::spawn(async move {
580 reader_client
581 .read_signal_stream_loop(
582 reader_connection_id,
583 reader_endpoint_id,
584 owner.transport_stable_id,
585 recv,
586 frame_tx,
587 )
588 .await;
589 });
590
591 let client = self.clone();
592 tokio::spawn(async move {
593 client
594 .dispatch_signal_stream_frames(
595 connection_id_owned,
596 endpoint_id_for_log,
597 owner,
598 frame_rx,
599 )
600 .await;
601 });
602 }
603
604 #[cfg(not(target_arch = "wasm32"))]
605 async fn dispatch_signal_stream_frames(
606 &self,
607 connection_id: String,
608 endpoint_id_for_log: String,
609 owner: NativeControlStreamOwner,
610 mut frame_rx: tokio::sync::mpsc::Receiver<crate::native_protocol::ParsedMainFrame>,
611 ) {
612 use crate::native_protocol::ParsedMainFrame;
613 let mut replaced_same_generation_frames = 0u8;
614 while let Some(parsed) = frame_rx.recv().await {
615 let owns_current_stream = self
616 .native_signal_stream_is_current(&connection_id, owner)
617 .await;
618 let same_transport_generation = self
619 .connection_manager
620 .current_transport_matches(&connection_id, Some(owner.transport_stable_id))
621 .await;
622 if !owns_current_stream && !same_transport_generation {
623 eprintln!(
624 "[SIGNAL-STREAM] stale-generation dispatcher stopped connection_id={} transport_stable_id={} stream_rank={}",
625 connection_id, owner.transport_stable_id, owner.stream_rank,
626 );
627 break;
628 }
629 if !owns_current_stream
630 && !should_drain_replaced_native_control_frame(replaced_same_generation_frames)
631 {
632 eprintln!(
633 "[SIGNAL-STREAM] same-generation drain cap reached connection_id={} transport_stable_id={} stream_rank={} drained_frames={}",
634 connection_id,
635 owner.transport_stable_id,
636 owner.stream_rank,
637 replaced_same_generation_frames,
638 );
639 break;
640 }
641 match parsed {
642 ParsedMainFrame::TypeScriptJson(json) => {
643 #[cfg(feature = "iroh-carrier-core")]
644 if json.get("type").and_then(serde_json::Value::as_str)
645 == Some(crate::iroh_carrier_bootstrap::CARRIER_BOOTSTRAP_FRAME_TYPE)
646 {
647 eprintln!(
648 "[OpenRTC][Iroh carrier] dispatching bootstrap connection_id={} endpoint_id={} transport_stable_id={} stream_rank={} owns_current_stream={} same_transport_generation={}",
649 connection_id,
650 endpoint_id_for_log,
651 owner.transport_stable_id,
652 owner.stream_rank,
653 owns_current_stream,
654 same_transport_generation,
655 );
656 }
657 self.maybe_handle_typescript_json_frame(
658 &connection_id,
659 Some(&endpoint_id_for_log),
660 &json,
661 )
662 .await;
663 }
664 ParsedMainFrame::TypeScriptHandshake(handshake) => {
665 if let crate::session_token::SessionAdmission::Rejected { reason } =
677 self.session_admission(&connection_id)
678 {
679 eprintln!(
680 "[SIGNAL-STREAM] handshake-frame ignored connection_id={} endpoint_id={} reason=admission-rejected detail={}",
681 connection_id, endpoint_id_for_log, reason,
682 );
683 continue;
684 }
685 eprintln!(
686 "[SIGNAL-STREAM] handshake-frame dispatched connection_id={} endpoint_id={} action={}",
687 connection_id,
688 endpoint_id_for_log,
689 handshake.action.as_deref().unwrap_or("<none>"),
690 );
691 if let Some(device_id) = handshake.claimed_device_id.as_deref() {
692 let _ = self.bind_admitted_device(&connection_id, device_id).await;
693 }
694 self.maybe_handle_typescript_handshake_capabilities(
695 &connection_id,
696 Some(&endpoint_id_for_log),
697 Some(owner.transport_stable_id),
698 &handshake,
699 )
700 .await;
701 }
702 ParsedMainFrame::NativeMessage(message) => {
703 let serialized = match serde_json::to_vec(&message) {
704 Ok(serialized) => serialized,
705 Err(error) => {
706 eprintln!(
707 "[SIGNAL-STREAM] native-message rejected connection_id={} endpoint_id={} reason=serialize-failed error={}",
708 connection_id, endpoint_id_for_log, error,
709 );
710 continue;
711 }
712 };
713 if let Err(error) = self
714 .inspect_incoming_native_main_frame_for_transport(
715 &connection_id,
716 Some(&endpoint_id_for_log),
717 None,
718 Some(owner.transport_stable_id),
719 &serialized,
720 )
721 .await
722 {
723 eprintln!(
724 "[SIGNAL-STREAM] native-message rejected connection_id={} endpoint_id={} channel={} error={}",
725 connection_id, endpoint_id_for_log, message.channel, error,
726 );
727 }
728 }
729 ParsedMainFrame::Opaque => {
730 eprintln!(
731 "[SIGNAL-STREAM] frame skipped connection_id={} endpoint_id={} reason=unsupported-on-signal-stream kind=opaque",
732 connection_id, endpoint_id_for_log,
733 );
734 }
735 }
736 if !owns_current_stream {
737 replaced_same_generation_frames = replaced_same_generation_frames.saturating_add(1);
747 eprintln!(
748 "[SIGNAL-STREAM] same-generation buffered frame drained connection_id={} transport_stable_id={} stream_rank={} drained_frames={}",
749 connection_id,
750 owner.transport_stable_id,
751 owner.stream_rank,
752 replaced_same_generation_frames,
753 );
754 }
755 }
756
757 eprintln!(
758 "[SIGNAL-STREAM] dispatcher-exited connection_id={}",
759 connection_id,
760 );
761 self.evict_native_signal_stream_if_owner(&connection_id, owner, "control-stream-ended")
762 .await;
763 }
764
765 #[cfg(not(target_arch = "wasm32"))]
766 async fn read_native_main_payload_frame(
767 recv: &mut iroh::endpoint::RecvStream,
768 frame_len_buf: [u8; 4],
769 first_payload_byte: Option<u8>,
770 ) -> Result<Option<crate::native_protocol::ParsedMainFrame>, String> {
771 let frame_len = u32::from_be_bytes(frame_len_buf) as usize;
772 if frame_len == 0 {
773 return Ok(None);
774 }
775 if frame_len > 4 * 1024 * 1024 {
776 return Err(format!("frame-too-large: {}", frame_len));
777 }
778
779 let mut frame = Vec::with_capacity(frame_len);
780 if let Some(first) = first_payload_byte {
781 frame.push(first);
782 }
783 if frame.len() < frame_len {
784 let remaining = frame_len - frame.len();
785 let mut rest = vec![0u8; remaining];
786 recv.read_exact(&mut rest)
787 .await
788 .map_err(|error| format!("frame-read-failed: {}", error))?;
789 frame.extend_from_slice(&rest);
790 }
791
792 Ok(Some(crate::native_protocol::parse_main_frame(&frame)))
797 }
798
799 #[cfg(not(target_arch = "wasm32"))]
800 fn native_main_read_error_proves_transport_loss(error: &str) -> bool {
801 error.to_ascii_lowercase().contains("connection lost")
802 }
803
804 #[cfg(not(target_arch = "wasm32"))]
805 async fn read_native_main_json_frame(
806 recv: &mut iroh::endpoint::RecvStream,
807 ) -> Result<Option<crate::native_protocol::ParsedMainFrame>, String> {
808 let mut protocol_byte = [0u8; 1];
809 match recv.read_exact(&mut protocol_byte).await {
810 Ok(_) => {}
811 Err(error) => {
812 return Err(format!("protocol-byte-read-failed: {}", error));
813 }
814 }
815 if protocol_byte[0] != 0x00 {
816 return Err(format!("unexpected-protocol-byte: {}", protocol_byte[0]));
817 }
818
819 let mut label_len_buf = [0u8; 4];
820 recv.read_exact(&mut label_len_buf)
821 .await
822 .map_err(|error| format!("label-length-read-failed: {}", error))?;
823 let label_len = u32::from_be_bytes(label_len_buf) as usize;
824 if label_len == 0 || label_len > 256 {
825 let frame_len_buf = [
832 protocol_byte[0],
833 label_len_buf[0],
834 label_len_buf[1],
835 label_len_buf[2],
836 ];
837 return Self::read_native_main_payload_frame(
838 recv,
839 frame_len_buf,
840 Some(label_len_buf[3]),
841 )
842 .await;
843 }
844
845 let mut label_buf = vec![0u8; label_len];
846 recv.read_exact(&mut label_buf)
847 .await
848 .map_err(|error| format!("label-read-failed: {}", error))?;
849 if String::from_utf8_lossy(&label_buf) != "main" {
850 return Err(format!(
851 "unexpected-label: {}",
852 String::from_utf8_lossy(&label_buf)
853 ));
854 }
855
856 let mut frame_len_buf = [0u8; 4];
857 recv.read_exact(&mut frame_len_buf)
858 .await
859 .map_err(|error| format!("frame-length-read-failed: {}", error))?;
860 Self::read_native_main_payload_frame(recv, frame_len_buf, None).await
861 }
862
863 #[cfg(not(target_arch = "wasm32"))]
867 async fn read_signal_stream_loop(
868 &self,
869 connection_id: String,
870 endpoint_id_for_log: String,
871 transport_stable_id: u64,
872 mut recv: iroh::endpoint::RecvStream,
873 frame_tx: tokio::sync::mpsc::Sender<crate::native_protocol::ParsedMainFrame>,
874 ) {
875 eprintln!(
876 "[SIGNAL-STREAM] reader-spawned connection_id={} endpoint_id={}",
877 connection_id, endpoint_id_for_log,
878 );
879
880 loop {
881 let parsed = match Self::read_native_main_json_frame(&mut recv).await {
882 Ok(Some(parsed)) => parsed,
883 Ok(None) => {
884 eprintln!(
885 "[SIGNAL-STREAM] reader-empty-frame connection_id={} endpoint_id={}",
886 connection_id, endpoint_id_for_log,
887 );
888 continue;
889 }
890 Err(err) => {
891 eprintln!(
892 "[SIGNAL-STREAM] reader-closed connection_id={} endpoint_id={} reason=native-main-frame-read-failed error={}",
893 connection_id, endpoint_id_for_log, err,
894 );
895 if Self::native_main_read_error_proves_transport_loss(&err) {
896 let demoted = self
897 .observe_native_transport_generation_lost(
898 &connection_id,
899 transport_stable_id,
900 "native-main-frame-read-failed",
901 )
902 .await;
903 eprintln!(
904 "[SIGNAL-STREAM] transport-loss-observed connection_id={} transport_stable_id={} demoted={}",
905 connection_id, transport_stable_id, demoted,
906 );
907 }
908 return;
909 }
910 };
911
912 let frame_type = match &parsed {
913 crate::native_protocol::ParsedMainFrame::TypeScriptJson(json) => json
914 .get("type")
915 .and_then(|v| v.as_str())
916 .unwrap_or("<none>"),
917 crate::native_protocol::ParsedMainFrame::TypeScriptHandshake(_) => "handshake",
918 crate::native_protocol::ParsedMainFrame::NativeMessage(_) => "native-message",
919 crate::native_protocol::ParsedMainFrame::Opaque => "opaque",
920 };
921 eprintln!(
922 "[SIGNAL-STREAM] reader-frame connection_id={} endpoint_id={} frame_type={}",
923 connection_id, endpoint_id_for_log, frame_type,
924 );
925 if !matches!(parsed, crate::native_protocol::ParsedMainFrame::Opaque) {
926 self.record_native_transport_protocol_activity(&connection_id, transport_stable_id);
927 }
928
929 if frame_tx.send(parsed).await.is_err() {
930 eprintln!(
931 "[SIGNAL-STREAM] reader-closed connection_id={} endpoint_id={} reason=dispatcher-dropped",
932 connection_id, endpoint_id_for_log,
933 );
934 return;
935 }
936 }
937 }
938
939 #[cfg(not(target_arch = "wasm32"))]
940 async fn native_signal_stream_is_current(
941 &self,
942 connection_id: &str,
943 expected_owner: NativeControlStreamOwner,
944 ) -> bool {
945 self.native_control_streams
946 .lock()
947 .await
948 .get(connection_id)
949 .is_some_and(|entry| entry.owner == expected_owner)
950 }
951
952 #[cfg(not(target_arch = "wasm32"))]
956 async fn evict_native_signal_stream_if_owner(
957 &self,
958 connection_id: &str,
959 expected_owner: NativeControlStreamOwner,
960 reason: &str,
961 ) -> bool {
962 let removed = {
963 let mut map = self.native_control_streams.lock().await;
964 match map.get(connection_id) {
965 Some(entry) if entry.owner == expected_owner => {
966 let removed = map
967 .remove(connection_id)
968 .expect("current native control owner must still exist");
969 let proofs_invalidated =
970 if self.native_admission_stream_is_persistent(connection_id) {
971 self.invalidate_native_persistent_route_proofs_for_transport(
977 connection_id,
978 expected_owner.transport_stable_id,
979 )
980 } else {
981 false
982 };
983 Some((removed.endpoint_id, proofs_invalidated))
984 }
985 _ => None,
986 }
987 };
988 if let Some((endpoint_id, proofs_invalidated)) = removed {
989 eprintln!(
990 "[SIGNAL-STREAM] evicted-current connection_id={} transport_stable_id={} stream_rank={} reason={}",
991 connection_id,
992 expected_owner.transport_stable_id,
993 expected_owner.stream_rank,
994 reason,
995 );
996 if proofs_invalidated {
997 let remote_node_id = endpoint_id.to_string();
1004 let _ = self
1005 .wake_native_external_auto_connect_for_replaced_route(remote_node_id.as_str())
1006 .await;
1007 }
1008 true
1009 } else {
1010 eprintln!(
1011 "[SIGNAL-STREAM] stale-eviction-ignored connection_id={} transport_stable_id={} stream_rank={} reason={}",
1012 connection_id,
1013 expected_owner.transport_stable_id,
1014 expected_owner.stream_rank,
1015 reason,
1016 );
1017 false
1018 }
1019 }
1020
1021 #[cfg(not(target_arch = "wasm32"))]
1025 pub(crate) async fn retire_stale_native_main_route(
1026 &self,
1027 connection_id: &str,
1028 current_transport_stable_id: u64,
1029 ) -> bool {
1030 let mut retired = false;
1035 let stale_owner = self
1036 .native_control_streams
1037 .lock()
1038 .await
1039 .get(connection_id)
1040 .map(|entry| entry.owner)
1041 .filter(|owner| owner.transport_stable_id != current_transport_stable_id);
1042
1043 if let Some(owner) = stale_owner {
1044 retired |= self
1045 .evict_native_signal_stream_if_owner(
1046 connection_id,
1047 owner,
1048 "transport-generation-replaced",
1049 )
1050 .await;
1051 }
1052 retired |= self.invalidate_native_main_route_proofs_except_transport(
1053 connection_id,
1054 current_transport_stable_id,
1055 );
1056 if retired {
1057 self.clear_connection_application_crypto_confirmation(connection_id);
1065 eprintln!(
1066 "[OpenRTC][KEY-AGREEMENT] replacement requires fresh confirmation connection_id={} transport_stable_id={}",
1067 connection_id, current_transport_stable_id,
1068 );
1069 }
1070 retired
1071 }
1072
1073 pub async fn update_transport_config(
1074 &self,
1075 transport_config: TransportConfig,
1076 ) -> anyhow::Result<()> {
1077 if let Some(moq) = transport_config.moq.as_ref() {
1078 anyhow::ensure!(
1079 !moq.relay_url.trim().is_empty(),
1080 "transports.moq.relayUrl is required because OpenRTC has no anonymous MoQ relay default"
1081 );
1082 }
1083 #[cfg(not(target_arch = "wasm32"))]
1084 let ble_available = self
1085 .native_custom_transport_kinds
1086 .read()
1087 .await
1088 .values()
1089 .any(|kind| matches!(kind, IrohPathKind::Ble));
1090 #[cfg(target_arch = "wasm32")]
1091 let ble_available = false;
1092 #[cfg(not(target_arch = "wasm32"))]
1093 let was_ble_enabled = ble_available
1094 && self
1095 .transport_config
1096 .read()
1097 .await
1098 .ble
1099 .as_ref()
1100 .is_some_and(|config| config.enabled);
1101 let transport_config =
1102 transport_config.sanitize_for_runtime_with_ble_available(ble_available);
1103 #[cfg(not(target_arch = "wasm32"))]
1104 let will_enable_ble = transport_config
1105 .ble
1106 .as_ref()
1107 .is_some_and(|config| config.enabled);
1108
1109 #[cfg(not(target_arch = "wasm32"))]
1110 if self.iroh_endpoint.read().await.is_some() {
1111 let current = self.transport_config.read().await;
1112 anyhow::ensure!(
1113 !current.endpoint_rebind_required(&transport_config),
1114 "irohRelayOnly, irohRelayTransportPolicy, and irohLan must be configured before Iroh endpoint initialization"
1115 );
1116 }
1117
1118 #[cfg(feature = "iroh-carrier-core")]
1119 let carrier_gate_guard = self.native_transport_upgrade_gates.lock().await;
1120 let mut guard = self.transport_config.write().await;
1121 #[cfg(feature = "iroh-carrier-core")]
1122 if *guard != transport_config {
1123 self.bump_iroh_carrier_policy_epoch();
1124 }
1125 *guard = transport_config;
1126 drop(guard);
1127 #[cfg(feature = "iroh-carrier-core")]
1128 drop(carrier_gate_guard);
1129 #[cfg(not(target_arch = "wasm32"))]
1130 if !was_ble_enabled && will_enable_ble {
1131 self.wake_native_ble_recovery("capability-enabled").await;
1132 }
1133 #[cfg(all(
1134 not(target_arch = "wasm32"),
1135 any(feature = "transport-webrtc", feature = "transport-moq")
1136 ))]
1137 self.reconcile_native_iroh_carrier_policy().await?;
1138 Ok(())
1139 }
1140
1141 #[cfg(all(
1148 not(target_arch = "wasm32"),
1149 any(feature = "transport-webrtc", feature = "transport-moq")
1150 ))]
1151 async fn reconcile_native_iroh_carrier_policy(&self) -> anyhow::Result<usize> {
1152 let records = self.connection_manager.list_active().await;
1153 let capabilities = self.native_peer_transport_capabilities.read().await.clone();
1154 let config = self.transport_config.read().await.clone();
1155 #[cfg(feature = "transport-webrtc")]
1156 let local_webrtc = self.is_webrtc_carrier_enabled().await;
1157 #[cfg(not(feature = "transport-webrtc"))]
1158 let local_webrtc = false;
1159 #[cfg(feature = "transport-moq")]
1160 let local_moq = self.is_moq_carrier_enabled().await;
1161 #[cfg(not(feature = "transport-moq"))]
1162 let local_moq = false;
1163 let mut reconciled = 0usize;
1164
1165 for record in records {
1166 let current = match record.active_transport.as_str() {
1167 crate::transport_label::WEBRTC => IrohPathKind::WebRtc,
1168 crate::transport_label::MOQ => IrohPathKind::Moq,
1169 _ => {
1170 if let Some(remote_node_id) =
1173 record.endpoint_id.as_deref().or(record.node_id.as_deref())
1174 {
1175 if self.iroh_path_kind(remote_node_id).await == IrohPathKind::Relay {
1176 self.revisit_iroh_route_after_admission(&record.connection_id)
1177 .await;
1178 }
1179 }
1180 continue;
1181 }
1182 };
1183 let Some(remote) = capabilities.get(&record.connection_id).cloned() else {
1188 continue;
1189 };
1190 let ranked = config.ranked_iroh_carriers(
1191 local_webrtc && remote.contains(&NativePeerTransportCapability::WebRtc),
1192 local_moq && remote.contains(&NativePeerTransportCapability::Moq),
1193 );
1194 let desired = ranked.first().copied();
1195 if desired.is_some_and(|route| route.as_str() == current.transport_label()) {
1196 continue;
1197 }
1198 let Some(remote_node_id) = record.endpoint_id.as_deref().or(record.node_id.as_deref())
1199 else {
1200 continue;
1201 };
1202 match desired {
1203 #[cfg(feature = "transport-webrtc")]
1204 Some(crate::route_policy::KnownRoute::WebRtc) => {
1205 self.maybe_start_native_webrtc_iroh_carrier(
1206 &record.connection_id,
1207 Some(remote_node_id),
1208 )
1209 .await?;
1210 reconciled = reconciled.saturating_add(1);
1211 }
1212 #[cfg(feature = "transport-moq")]
1213 Some(crate::route_policy::KnownRoute::Moq) => {
1214 self.maybe_start_native_moq_iroh_carrier(
1215 &record.connection_id,
1216 Some(remote_node_id),
1217 )
1218 .await?;
1219 reconciled = reconciled.saturating_add(1);
1220 }
1221 _ => {
1222 let Ok(remote_endpoint_id) = remote_node_id.parse::<iroh::EndpointId>() else {
1223 continue;
1224 };
1225 if self
1226 .close_current_iroh_carrier_generation_with_reason(
1227 &record.connection_id,
1228 remote_endpoint_id,
1229 current,
1230 record.transport_generation,
1231 crate::lifecycle_reason::REASON_IROH_CARRIER_POLICY_CHANGED,
1232 )
1233 .await
1234 {
1235 reconciled = reconciled.saturating_add(1);
1236 }
1237 }
1238 }
1239 }
1240 Ok(reconciled)
1241 }
1242
1243 pub async fn transport_config(&self) -> TransportConfig {
1244 self.transport_config.read().await.clone()
1245 }
1246
1247 pub async fn is_webrtc_transport_enabled(&self) -> bool {
1248 self.transport_config.read().await.webrtc.is_some()
1249 }
1250
1251 pub async fn is_webrtc_external_transport_enabled(&self) -> bool {
1252 false
1253 }
1254
1255 #[cfg(not(target_arch = "wasm32"))]
1256 pub async fn is_webrtc_carrier_enabled(&self) -> bool {
1257 self.transport_config.read().await.webrtc.as_ref().is_some()
1258 && self
1259 .native_transport_upgrade_available(IrohPathKind::WebRtc)
1260 .await
1261 }
1262
1263 pub async fn is_moq_transport_enabled(&self) -> bool {
1264 self.transport_config.read().await.moq.is_some()
1265 }
1266
1267 pub async fn is_moq_external_transport_enabled(&self) -> bool {
1268 false
1269 }
1270
1271 async fn local_typescript_webrtc_capability(&self) -> bool {
1278 #[cfg(all(feature = "transport-webrtc", target_arch = "wasm32"))]
1279 let enabled = self.is_webrtc_transport_enabled().await;
1280 #[cfg(all(feature = "transport-webrtc", not(target_arch = "wasm32")))]
1281 let enabled = self.is_webrtc_carrier_enabled().await;
1282 #[cfg(not(feature = "transport-webrtc"))]
1283 let enabled = false;
1284 enabled
1285 }
1286
1287 async fn local_typescript_moq_capability(&self) -> bool {
1288 #[cfg(all(feature = "transport-moq", target_arch = "wasm32"))]
1289 let enabled = self.is_moq_transport_enabled().await;
1290 #[cfg(all(feature = "transport-moq", not(target_arch = "wasm32")))]
1291 let enabled = self.is_moq_carrier_enabled().await;
1292 #[cfg(not(feature = "transport-moq"))]
1293 let enabled = false;
1294 enabled
1295 }
1296
1297 #[cfg(not(target_arch = "wasm32"))]
1298 pub async fn is_moq_carrier_enabled(&self) -> bool {
1299 self.transport_config.read().await.moq.as_ref().is_some()
1300 && self
1301 .native_transport_upgrade_available(IrohPathKind::Moq)
1302 .await
1303 }
1304
1305 #[cfg(not(target_arch = "wasm32"))]
1306 pub async fn is_ble_transport_enabled(&self) -> bool {
1307 self.transport_config
1308 .read()
1309 .await
1310 .ble
1311 .as_ref()
1312 .is_some_and(|config| config.enabled)
1313 && self
1314 .native_transport_upgrade_available(IrohPathKind::Ble)
1315 .await
1316 }
1317
1318 #[cfg(target_arch = "wasm32")]
1319 pub async fn is_ble_transport_enabled(&self) -> bool {
1320 false
1321 }
1322
1323 pub(crate) async fn maybe_handle_typescript_handshake_capabilities(
1324 &self,
1325 connection_id: &str,
1326 remote_node_id: Option<&str>,
1327 transport_stable_id: Option<u64>,
1328 handshake: &crate::native_protocol::TypeScriptHandshake,
1329 ) {
1330 let uses_canonical_carrier_schema = handshake
1331 .capabilities
1332 .as_ref()
1333 .is_some_and(|caps| caps.carrier_schema == Some(2));
1334 let remote_supports_webrtc = uses_canonical_carrier_schema
1335 && handshake
1336 .capabilities
1337 .as_ref()
1338 .and_then(|caps| caps.webrtc)
1339 .unwrap_or(false);
1340 let remote_supports_moq = uses_canonical_carrier_schema
1341 && handshake
1342 .capabilities
1343 .as_ref()
1344 .and_then(|caps| caps.moq)
1345 .unwrap_or(false);
1346 let remote_supports_ble = handshake
1347 .capabilities
1348 .as_ref()
1349 .and_then(|caps| caps.ble)
1350 .unwrap_or(false);
1351 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1352 let mut carrier_capabilities_changed = false;
1353 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1354 let mut carrier_capabilities_were_known = false;
1355 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1356 let carrier_gate_guard = if uses_canonical_carrier_schema {
1357 Some(self.native_transport_upgrade_gates.lock().await)
1362 } else {
1363 None
1364 };
1365 #[cfg(not(target_arch = "wasm32"))]
1366 {
1367 if uses_canonical_carrier_schema {
1371 let mut capabilities = self.native_peer_transport_capabilities.write().await;
1372 #[cfg(feature = "iroh-carrier-core")]
1373 {
1374 carrier_capabilities_were_known = capabilities.contains_key(connection_id);
1375 }
1376 let peer_capabilities = capabilities.entry(connection_id.to_string()).or_default();
1377 #[cfg(feature = "iroh-carrier-core")]
1378 let previous = peer_capabilities.clone();
1379 if remote_supports_webrtc {
1380 peer_capabilities.insert(NativePeerTransportCapability::WebRtc);
1381 } else {
1382 peer_capabilities.remove(&NativePeerTransportCapability::WebRtc);
1383 }
1384 if remote_supports_moq {
1385 peer_capabilities.insert(NativePeerTransportCapability::Moq);
1386 } else {
1387 peer_capabilities.remove(&NativePeerTransportCapability::Moq);
1388 }
1389 if remote_supports_ble {
1390 peer_capabilities.insert(NativePeerTransportCapability::Ble);
1391 } else {
1392 peer_capabilities.remove(&NativePeerTransportCapability::Ble);
1393 }
1394 #[cfg(feature = "iroh-carrier-core")]
1395 {
1396 carrier_capabilities_changed = *peer_capabilities != previous;
1397 }
1398 }
1403 }
1404 #[cfg(not(target_arch = "wasm32"))]
1405 let (remote_supports_webrtc, remote_supports_moq, remote_supports_ble) = {
1406 let known = self
1412 .native_peer_transport_capabilities
1413 .read()
1414 .await
1415 .get(connection_id)
1416 .cloned()
1417 .unwrap_or_default();
1418 (
1419 effective_typescript_transport_capability(
1420 uses_canonical_carrier_schema,
1421 remote_supports_webrtc,
1422 known.contains(&NativePeerTransportCapability::WebRtc),
1423 ),
1424 effective_typescript_transport_capability(
1425 uses_canonical_carrier_schema,
1426 remote_supports_moq,
1427 known.contains(&NativePeerTransportCapability::Moq),
1428 ),
1429 effective_typescript_transport_capability(
1430 uses_canonical_carrier_schema,
1431 remote_supports_ble,
1432 known.contains(&NativePeerTransportCapability::Ble),
1433 ),
1434 )
1435 };
1436 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1437 let matching_first_attempt = carrier_capabilities_changed
1438 && !carrier_capabilities_were_known
1439 && carrier_gate_guard.as_ref().is_some_and(|gates| {
1440 let global_policy_epoch = self.current_iroh_carrier_policy_epoch();
1441 let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
1442 gates.iter().any(|((candidate_connection_id, kind), gate)| {
1443 candidate_connection_id == connection_id
1444 && gate.policy_epoch == global_policy_epoch
1445 && gate.peer_policy_epoch == peer_policy_epoch
1446 && match kind {
1447 IrohPathKind::WebRtc => remote_supports_webrtc,
1448 IrohPathKind::Moq => remote_supports_moq,
1449 IrohPathKind::Ble => remote_supports_ble,
1450 _ => false,
1451 }
1452 })
1453 });
1454 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1455 if crate::client::should_bump_remote_carrier_peer_policy_epoch(
1456 carrier_capabilities_were_known,
1457 carrier_capabilities_changed,
1458 matching_first_attempt,
1459 ) {
1460 self.bump_iroh_carrier_peer_policy_epoch(connection_id);
1461 }
1462 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1463 drop(carrier_gate_guard);
1464 #[cfg(all(
1465 not(target_arch = "wasm32"),
1466 any(feature = "transport-webrtc", feature = "transport-moq")
1467 ))]
1468 if carrier_capabilities_changed {
1469 if let Err(error) = self.reconcile_native_iroh_carrier_policy().await {
1476 eprintln!(
1477 "[OpenRTC][Iroh carrier] capability reconciliation failed connection_id={} error={}",
1478 connection_id, error,
1479 );
1480 }
1481 }
1482 let application_crypto_was_confirmed =
1483 self.connection_application_crypto_is_confirmed(connection_id, transport_stable_id);
1484 let local_application_key_agreement_public_key = self
1485 .maybe_negotiate_typescript_application_crypto(
1486 connection_id,
1487 remote_node_id,
1488 transport_stable_id,
1489 handshake,
1490 )
1491 .await;
1492
1493 let local_webrtc_enabled = self.local_typescript_webrtc_capability().await;
1494 let local_moq_enabled = self.local_typescript_moq_capability().await;
1495 let local_ble_enabled = self.is_ble_transport_enabled().await;
1496 let received_application_key = handshake.application_key_agreement_public_key.is_some();
1497
1498 if let Some(reply_action) = typescript_application_key_reply_action(
1499 handshake.action.as_deref(),
1500 received_application_key,
1501 local_application_key_agreement_public_key.is_some(),
1502 application_crypto_was_confirmed,
1503 ) {
1504 let public_key = local_application_key_agreement_public_key
1505 .or_else(|| self.connection_key_agreement_public_key(connection_id));
1506 if let Err(error) = self
1507 .send_typescript_handshake_update(
1508 connection_id,
1509 "application-key-confirmation",
1510 public_key,
1511 reply_action,
1512 )
1513 .await
1514 {
1515 eprintln!(
1516 "[OpenRTC][capability] key acknowledgement send failed connection_id={} remote_node_id={:?} error={}",
1517 connection_id, remote_node_id, error,
1518 );
1519 }
1520 }
1521 eprintln!(
1522 "[OpenRTC][transport] TS handshake capabilities connection_id={} remote_node_id={:?} remote_webrtc={} local_webrtc={} remote_moq={} local_moq={} remote_ble={} local_ble={}",
1523 connection_id,
1524 remote_node_id,
1525 remote_supports_webrtc,
1526 local_webrtc_enabled,
1527 remote_supports_moq,
1528 local_moq_enabled,
1529 remote_supports_ble,
1530 local_ble_enabled,
1531 );
1532 let should_send_capability_update = should_send_typescript_capability_update(
1533 local_webrtc_enabled,
1534 local_moq_enabled,
1535 local_ble_enabled,
1536 local_application_key_agreement_public_key.is_some() && !received_application_key,
1537 handshake.action.as_deref(),
1538 );
1539
1540 if should_send_capability_update {
1541 if let Err(error) = self
1546 .send_typescript_capability_update(
1547 connection_id,
1548 if local_webrtc_enabled || local_moq_enabled || local_ble_enabled {
1549 "local-capability"
1550 } else {
1551 "application-key-agreement"
1552 },
1553 local_application_key_agreement_public_key,
1554 )
1555 .await
1556 {
1557 eprintln!(
1558 "[OpenRTC][capability] update send failed connection_id={} remote_node_id={:?} error={}",
1559 connection_id,
1560 remote_node_id,
1561 error,
1562 );
1563 }
1564 } else if handshake.action.as_deref() == Some("capability-update") {
1565 eprintln!(
1566 "[OpenRTC][capability] update received without echo connection_id={} remote_node_id={:?}",
1567 connection_id, remote_node_id,
1568 );
1569 }
1570
1571 #[cfg(not(target_arch = "wasm32"))]
1572 if let Err(error) = self
1573 .maybe_start_preferred_native_iroh_carrier(
1574 connection_id,
1575 remote_node_id,
1576 remote_supports_webrtc,
1577 remote_supports_moq,
1578 )
1579 .await
1580 {
1581 eprintln!(
1582 "[OpenRTC][Iroh carrier] preferred start failed connection_id={} remote_node_id={:?} error={}",
1583 connection_id, remote_node_id, error,
1584 );
1585 }
1586
1587 #[cfg(not(target_arch = "wasm32"))]
1588 if remote_supports_ble && local_ble_enabled {
1589 let _ = self
1590 .maybe_start_native_ble_upgrade(connection_id, remote_node_id)
1591 .await;
1592 }
1593
1594 #[cfg(not(target_arch = "wasm32"))]
1595 if self.connection_application_crypto_is_confirmed(connection_id, transport_stable_id) {
1596 let _ = self
1597 .confirm_managed_connection_readiness(connection_id)
1598 .await;
1599 }
1600 }
1601
1602 #[cfg(all(
1606 not(target_arch = "wasm32"),
1607 any(feature = "transport-webrtc", feature = "transport-moq")
1608 ))]
1609 pub(super) async fn maybe_start_preferred_native_iroh_carrier(
1610 &self,
1611 connection_id: &str,
1612 remote_node_id: Option<&str>,
1613 remote_supports_webrtc: bool,
1614 remote_supports_moq: bool,
1615 ) -> anyhow::Result<Option<crate::route_policy::KnownRoute>> {
1616 #[cfg(feature = "transport-webrtc")]
1617 if remote_supports_webrtc {
1618 let attempt = self
1619 .native_webrtc_carrier_attempts
1620 .read()
1621 .await
1622 .get(connection_id)
1623 .cloned();
1624 if let Some(attempt) = attempt {
1625 let refreshing = attempt.ice_refresh.try_lock().is_err()
1626 || attempt
1627 .ice_refresh_baseline
1628 .try_lock()
1629 .map(|loan| loan.is_some())
1630 .unwrap_or(true);
1631 if refreshing
1632 && self
1633 .native_ice_refresh_is_current(connection_id, &attempt)
1634 .await
1635 {
1636 return Ok(None);
1637 }
1638 }
1639 }
1640 let Some(base_generation) = self
1641 .current_native_peer_data_generation(connection_id, None)
1642 .await
1643 else {
1644 self.record_native_iroh_carrier_debug_event(
1645 connection_id,
1646 "",
1647 "selector-deferred",
1648 Some("base-generation-unavailable"),
1649 );
1650 return Ok(None);
1651 };
1652 if let Some((_rejected, reason)) = self.session_admission_block_reason_for_transport(
1653 connection_id,
1654 Some(base_generation.transport_stable_id),
1655 ) {
1656 eprintln!(
1663 "[OpenRTC][Iroh carrier] preferred start deferred connection_id={} transport_stable_id={} reason={}",
1664 connection_id, base_generation.transport_stable_id, reason,
1665 );
1666 self.record_native_iroh_carrier_debug_event(
1667 connection_id,
1668 "",
1669 "selector-deferred",
1670 Some(&reason),
1671 );
1672 return Ok(None);
1673 }
1674 #[cfg(feature = "transport-webrtc")]
1675 let local_webrtc = self.is_webrtc_carrier_enabled().await;
1676 #[cfg(not(feature = "transport-webrtc"))]
1677 let local_webrtc = false;
1678 #[cfg(feature = "transport-moq")]
1679 let local_moq = self.is_moq_carrier_enabled().await;
1680 #[cfg(not(feature = "transport-moq"))]
1681 let local_moq = false;
1682 let config = self.transport_config.read().await.clone();
1683 let ranked = config.ranked_iroh_carriers(
1684 remote_supports_webrtc && local_webrtc,
1685 remote_supports_moq && local_moq,
1686 );
1687 let Some(selected) = ranked.first().copied() else {
1688 let reason = format!(
1689 "no-eligible-carrier:remote-webrtc={remote_supports_webrtc}:local-webrtc={local_webrtc}:remote-moq={remote_supports_moq}:local-moq={local_moq}:privacy-mode={}:webrtc-relay-only={}",
1690 config.privacy_mode,
1691 config.webrtc.as_ref().is_some_and(|webrtc| webrtc.privacy_mode),
1692 );
1693 self.record_native_iroh_carrier_debug_event(
1694 connection_id,
1695 "",
1696 "selector-deferred",
1697 Some(&reason),
1698 );
1699 return Ok(None);
1700 };
1701 self.record_native_iroh_carrier_debug_event(
1702 connection_id,
1703 "",
1704 "selector-selected",
1705 Some(selected.as_str()),
1706 );
1707 match selected {
1708 #[cfg(feature = "transport-webrtc")]
1709 crate::route_policy::KnownRoute::WebRtc => {
1710 self.maybe_start_native_webrtc_iroh_carrier(connection_id, remote_node_id)
1711 .await?;
1712 }
1713 #[cfg(feature = "transport-moq")]
1714 crate::route_policy::KnownRoute::Moq => {
1715 self.maybe_start_native_moq_iroh_carrier(connection_id, remote_node_id)
1716 .await?;
1717 }
1718 _ => return Ok(None),
1719 }
1720 Ok(Some(selected))
1721 }
1722
1723 #[cfg(all(
1727 not(target_arch = "wasm32"),
1728 any(feature = "transport-webrtc", feature = "transport-moq")
1729 ))]
1730 pub(super) async fn revisit_iroh_route_after_admission(&self, connection_id: &str) {
1731 let Some(remote_node_id) = self
1732 .connection_manager
1733 .get_by_connection_id(connection_id)
1734 .await
1735 .and_then(|record| record.endpoint_id.or(record.node_id))
1736 else {
1737 return;
1738 };
1739 let remote_capabilities = self
1740 .native_peer_transport_capabilities
1741 .read()
1742 .await
1743 .get(connection_id)
1744 .cloned()
1745 .unwrap_or_default();
1746 let local_is_responder = self
1747 .current_node_id()
1748 .await
1749 .is_some_and(|local| local.as_str() <= remote_node_id.as_str());
1750 let selected = match self
1751 .maybe_start_preferred_native_iroh_carrier(
1752 connection_id,
1753 Some(&remote_node_id),
1754 remote_capabilities.contains(&NativePeerTransportCapability::WebRtc),
1755 remote_capabilities.contains(&NativePeerTransportCapability::Moq),
1756 )
1757 .await
1758 {
1759 Ok(selected) => selected,
1760 Err(error) => {
1761 eprintln!(
1762 "[OpenRTC][Iroh carrier] admission-event start failed connection_id={} remote_node_id={} error={}",
1763 connection_id, remote_node_id, error,
1764 );
1765 None
1766 }
1767 };
1768 if selected.is_some() && local_is_responder {
1769 if let Err(error) = self
1773 .send_typescript_capability_update(connection_id, "native-admission-ready", None)
1774 .await
1775 {
1776 eprintln!(
1777 "[OpenRTC][Iroh carrier] admission-ready advertisement failed connection_id={} remote_node_id={} error={}",
1778 connection_id, remote_node_id, error,
1779 );
1780 }
1781 }
1782 }
1783
1784 #[cfg(all(
1785 not(target_arch = "wasm32"),
1786 not(any(feature = "transport-webrtc", feature = "transport-moq"))
1787 ))]
1788 pub(super) async fn maybe_start_preferred_native_iroh_carrier(
1789 &self,
1790 _connection_id: &str,
1791 _remote_node_id: Option<&str>,
1792 _remote_supports_webrtc: bool,
1793 _remote_supports_moq: bool,
1794 ) -> anyhow::Result<Option<crate::route_policy::KnownRoute>> {
1795 Ok(None)
1796 }
1797
1798 #[cfg(all(
1799 not(target_arch = "wasm32"),
1800 any(feature = "transport-webrtc", feature = "transport-moq")
1801 ))]
1802 fn maybe_start_next_native_iroh_carrier<'a>(
1803 &'a self,
1804 connection_id: &'a str,
1805 remote_node_id: &'a str,
1806 failed: crate::route_policy::KnownRoute,
1807 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
1808 Box::pin(async move {
1809 let should_initiate = self
1810 .current_node_id()
1811 .await
1812 .is_some_and(|local| local.as_str() > remote_node_id);
1813 if !should_initiate {
1814 return false;
1815 }
1816 let remote_capabilities = self
1817 .native_peer_transport_capabilities
1818 .read()
1819 .await
1820 .get(connection_id)
1821 .cloned()
1822 .unwrap_or_default();
1823 #[cfg(feature = "transport-webrtc")]
1824 let local_webrtc = self.is_webrtc_carrier_enabled().await;
1825 #[cfg(not(feature = "transport-webrtc"))]
1826 let local_webrtc = false;
1827 #[cfg(feature = "transport-moq")]
1828 let local_moq = self.is_moq_carrier_enabled().await;
1829 #[cfg(not(feature = "transport-moq"))]
1830 let local_moq = false;
1831 let config = self.transport_config.read().await.clone();
1832 let ranked = config.ranked_iroh_carriers(
1833 local_webrtc
1834 && remote_capabilities.contains(&NativePeerTransportCapability::WebRtc),
1835 local_moq && remote_capabilities.contains(&NativePeerTransportCapability::Moq),
1836 );
1837 let Some(next) = ranked
1838 .iter()
1839 .position(|route| *route == failed)
1840 .and_then(|index| ranked.get(index + 1))
1841 .copied()
1842 else {
1843 return false;
1844 };
1845 let result = match next {
1846 #[cfg(feature = "transport-webrtc")]
1847 crate::route_policy::KnownRoute::WebRtc => {
1848 self.maybe_start_native_webrtc_iroh_carrier(connection_id, Some(remote_node_id))
1849 .await
1850 }
1851 #[cfg(feature = "transport-moq")]
1852 crate::route_policy::KnownRoute::Moq => {
1853 self.maybe_start_native_moq_iroh_carrier(connection_id, Some(remote_node_id))
1854 .await
1855 }
1856 _ => return false,
1857 };
1858 if let Err(error) = result {
1859 eprintln!(
1860 "[OpenRTC][Iroh carrier] fallback start failed connection_id={} route={} error={}",
1861 connection_id,
1862 next.as_str(),
1863 error,
1864 );
1865 }
1866 true
1867 })
1868 }
1869
1870 #[cfg(not(target_arch = "wasm32"))]
1871 async fn send_native_ble_upgrade_control(
1872 &self,
1873 connection_id: &str,
1874 action: &str,
1875 upgrade_id: &str,
1876 proof_generation: crate::client::NativePeerDataGeneration,
1877 ) -> anyhow::Result<()> {
1878 let frame = serde_json::json!({
1879 "type": "transport-upgrade",
1880 "transport": "ble",
1881 "action": action,
1882 "upgradeId": upgrade_id,
1883 "base": proof_generation,
1887 });
1888 self.send_typescript_handshake_over_native_main(connection_id, &frame)
1889 .await
1890 }
1891
1892 #[cfg(all(
1893 not(target_arch = "wasm32"),
1894 any(feature = "transport-webrtc", feature = "transport-moq")
1895 ))]
1896 async fn send_iroh_carrier_control(
1897 &self,
1898 connection_id: &str,
1899 frame: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1900 ) -> anyhow::Result<()> {
1901 self.send_typescript_handshake_over_native_main(
1902 connection_id,
1903 &serde_json::to_value(frame)?,
1904 )
1905 .await
1906 }
1907
1908 #[cfg(all(
1909 not(target_arch = "wasm32"),
1910 any(feature = "transport-webrtc", feature = "transport-moq")
1911 ))]
1912 async fn reject_native_iroh_carrier_request_if_base_blocked(
1913 &self,
1914 connection_id: &str,
1915 bootstrap: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1916 transport_stable_id: u64,
1917 carrier_name: &str,
1918 ) -> bool {
1919 let Some((terminal, reason)) = self
1920 .session_admission_block_reason_for_transport(connection_id, Some(transport_stable_id))
1921 else {
1922 return false;
1923 };
1924 let failure_code = if terminal {
1925 "carrier-authorization-rejected"
1926 } else {
1927 "carrier-base-not-ready"
1928 };
1929 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
1930 bootstrap,
1931 failure_code,
1932 ) {
1933 let _ = self.send_iroh_carrier_control(connection_id, &failed).await;
1934 }
1935 eprintln!(
1936 "[OpenRTC][{carrier_name} carrier] responder rejected connection_id={connection_id} transport_stable_id={transport_stable_id} failure_code={failure_code} reason={reason}",
1937 );
1938 true
1939 }
1940
1941 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
1942 async fn create_native_webrtc_carrier_attempt(
1943 &self,
1944 connection_id: &str,
1945 remote_node_id: &str,
1946 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1947 generation: crate::client::NativePeerDataGeneration,
1948 role: crate::transport::NativeWebRTCRole,
1949 start: bool,
1950 ) -> anyhow::Result<Arc<crate::client::NativeWebRtcCarrierAttempt>> {
1951 let config = self
1952 .transport_config
1953 .read()
1954 .await
1955 .webrtc
1956 .clone()
1957 .ok_or_else(|| anyhow::anyhow!("WebRTC carrier config is unavailable"))?;
1958 let local_node_id = self
1959 .current_node_id()
1960 .await
1961 .ok_or_else(|| anyhow::anyhow!("local Iroh node id is unavailable"))?;
1962 let remote_endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
1963 if let Some(previous) = self
1968 .native_webrtc_carrier_attempts
1969 .write()
1970 .await
1971 .remove(connection_id)
1972 {
1973 self.record_native_iroh_carrier_debug_event(
1974 connection_id,
1975 &previous.upgrade_id,
1976 "close-requested",
1977 Some("superseded-before-carrier-activation"),
1978 );
1979 previous.channel.close();
1980 let _ = previous.pump.lock().await.take();
1981 }
1982 let packet_session = self
1983 .activate_native_iroh_packet_carrier(IrohPathKind::WebRtc, remote_endpoint_id)
1984 .await?;
1985 let expected = bootstrap.frame_expectation()?;
1986 let application_key = self
1987 .application_crypto_key_for_connection(Some(connection_id))
1988 .ok_or_else(|| {
1989 anyhow::anyhow!("WebRTC carrier requires the admitted application crypto key")
1990 })?;
1991 let client = self.clone();
1992 let signal_connection_id = connection_id.to_string();
1993 let signal_upgrade_id = bootstrap.upgrade_id.clone();
1994 let signal_sender: crate::transport::WebRtcSignalSender =
1995 Arc::new(move |frame: serde_json::Value| {
1996 let client = client.clone();
1997 let connection_id = signal_connection_id.clone();
1998 let upgrade_id = signal_upgrade_id.clone();
1999 Box::pin(async move {
2000 let current = client
2001 .native_webrtc_carrier_attempts
2002 .read()
2003 .await
2004 .get(&connection_id)
2005 .is_some_and(|attempt| attempt.upgrade_id == upgrade_id);
2006 if current {
2007 let _ = client
2008 .send_typescript_handshake_over_native_main(&connection_id, &frame)
2009 .await;
2010 }
2011 })
2012 });
2013 let channel = Arc::new(
2014 crate::transport::WebRtcDataChannel::new_packet_carrier(
2015 &local_node_id,
2016 remote_node_id,
2017 &config,
2018 signal_sender,
2019 &bootstrap.upgrade_id,
2020 role,
2021 )
2022 .await?,
2023 );
2024 let pump = crate::native_webrtc_carrier::NativeWebRtcCarrierSession::attach(
2025 channel.clone(),
2026 packet_session,
2027 expected,
2028 application_key,
2029 )
2030 .await?;
2031 let attempt = Arc::new(crate::client::NativeWebRtcCarrierAttempt {
2032 upgrade_id: bootstrap.upgrade_id.clone(),
2033 bootstrap,
2034 generation,
2035 remote_endpoint_id,
2036 channel: channel.clone(),
2037 pump: tokio::sync::Mutex::new(Some(pump)),
2038 completion_started: std::sync::atomic::AtomicBool::new(false),
2039 ice_refresh: tokio::sync::Mutex::new(Default::default()),
2040 ice_refresh_baseline: tokio::sync::Mutex::new(None),
2041 });
2042 if let Some(previous) = self
2043 .native_webrtc_carrier_attempts
2044 .write()
2045 .await
2046 .insert(connection_id.to_string(), attempt.clone())
2047 {
2048 self.record_native_iroh_carrier_debug_event(
2049 connection_id,
2050 &previous.upgrade_id,
2051 "close-requested",
2052 Some("superseded-during-carrier-install"),
2053 );
2054 previous.channel.close();
2055 }
2056 self.record_native_iroh_carrier_debug_event(
2057 connection_id,
2058 &attempt.upgrade_id,
2059 "attempt-created",
2060 Some(match role {
2061 crate::transport::NativeWebRTCRole::Initiator => "initiator",
2062 crate::transport::NativeWebRTCRole::Responder => "responder",
2063 crate::transport::NativeWebRTCRole::Auto => "auto",
2064 }),
2065 );
2066 if start {
2067 if let Err(error) = channel.start().await {
2068 self.native_webrtc_carrier_attempts
2069 .write()
2070 .await
2071 .remove(connection_id);
2072 return Err(error);
2073 }
2074 }
2075 Ok(attempt)
2076 }
2077
2078 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2079 async fn wait_native_webrtc_carrier_connected(
2080 &self,
2081 connection_id: &str,
2082 upgrade_id: &str,
2083 ) -> anyhow::Result<Arc<crate::client::NativeWebRtcCarrierAttempt>> {
2084 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(25);
2085 loop {
2086 let attempt = self
2087 .native_webrtc_carrier_attempts
2088 .read()
2089 .await
2090 .get(connection_id)
2091 .filter(|attempt| attempt.upgrade_id == upgrade_id)
2092 .cloned()
2093 .ok_or_else(|| anyhow::anyhow!("WebRTC carrier attempt was retired"))?;
2094 match attempt.channel.state() {
2095 crate::transport::NativeWebRTCState::Connected => return Ok(attempt),
2096 crate::transport::NativeWebRTCState::Failed
2097 | crate::transport::NativeWebRTCState::Closed => {
2098 anyhow::bail!("WebRTC carrier negotiation failed")
2099 }
2100 _ if tokio::time::Instant::now() >= deadline => {
2101 anyhow::bail!("WebRTC carrier negotiation timed out")
2102 }
2103 _ => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
2104 }
2105 }
2106 }
2107
2108 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2109 fn spawn_native_webrtc_carrier_terminal_watcher(
2110 &self,
2111 connection_id: String,
2112 remote_endpoint_id: iroh::EndpointId,
2113 attempt: Arc<crate::client::NativeWebRtcCarrierAttempt>,
2114 ) {
2115 let client = self.clone();
2116 tokio::spawn(async move {
2117 loop {
2118 let current = client
2119 .native_webrtc_carrier_attempts
2120 .read()
2121 .await
2122 .get(&connection_id)
2123 .is_some_and(|value| Arc::ptr_eq(value, &attempt));
2124 if !current {
2125 return;
2126 }
2127 {
2128 let mut baseline = attempt.ice_refresh_baseline.lock().await;
2129 if baseline.as_ref().is_some_and(|loan| {
2130 (loan.started
2131 && loan.since.elapsed().as_secs() >= 1
2132 && !attempt.channel.ice_refresh_in_progress())
2133 || loan.since.elapsed().as_secs() >= 30
2134 || loan.policy_epoch != client.current_iroh_carrier_policy_epoch()
2135 || loan.peer_policy_epoch
2136 != client.current_iroh_carrier_peer_policy_epoch(&connection_id)
2137 }) {
2138 baseline.take();
2139 }
2140 }
2141 let terminal_reason = attempt
2142 .pump
2143 .lock()
2144 .await
2145 .as_ref()
2146 .and_then(|pump| pump.take_terminal_reason());
2147 let terminal_reason = terminal_reason.or_else(|| {
2148 matches!(
2149 attempt.channel.state(),
2150 crate::transport::NativeWebRTCState::Failed
2151 | crate::transport::NativeWebRTCState::Closed
2152 )
2153 .then_some("datachannel-terminal")
2154 });
2155 if let Some(reason) = terminal_reason {
2156 client.record_native_iroh_carrier_debug_event(
2157 &connection_id,
2158 &attempt.upgrade_id,
2159 "terminal-observed",
2160 Some(reason),
2161 );
2162 let retired = client
2168 .retire_native_webrtc_carrier_attempt_if_current(
2169 &connection_id,
2170 &attempt,
2171 reason,
2172 )
2173 .await;
2174 if retired {
2175 remove_native_transport_upgrade_gate_for_generation(
2176 &client.native_transport_upgrade_gates,
2177 &(connection_id.clone(), IrohPathKind::WebRtc),
2178 &attempt.upgrade_id,
2179 attempt.generation,
2180 )
2181 .await;
2182 }
2183 let _ = client
2184 .close_current_iroh_carrier_generation_with_reason(
2185 &connection_id,
2186 remote_endpoint_id,
2187 IrohPathKind::WebRtc,
2188 attempt.generation.transport_generation.saturating_add(1),
2189 reason,
2190 )
2191 .await;
2192 return;
2193 }
2194 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
2195 }
2196 });
2197 }
2198
2199 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2200 async fn retire_native_webrtc_carrier_attempt(
2201 &self,
2202 connection_id: &str,
2203 upgrade_id: &str,
2204 reason: &str,
2205 ) -> bool {
2206 let removed = {
2207 let mut attempts = self.native_webrtc_carrier_attempts.write().await;
2208 if attempts
2209 .get(connection_id)
2210 .is_some_and(|attempt| attempt.upgrade_id == upgrade_id)
2211 {
2212 attempts.remove(connection_id)
2213 } else {
2214 None
2215 }
2216 };
2217 if let Some(attempt) = removed {
2218 self.record_native_iroh_carrier_debug_event(
2219 connection_id,
2220 &attempt.upgrade_id,
2221 "close-requested",
2222 Some(reason),
2223 );
2224 attempt.channel.close();
2225 let _ = attempt.pump.lock().await.take();
2226 true
2227 } else {
2228 false
2229 }
2230 }
2231
2232 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2233 async fn retire_native_webrtc_carrier_attempt_if_current(
2234 &self,
2235 connection_id: &str,
2236 expected: &Arc<crate::client::NativeWebRtcCarrierAttempt>,
2237 reason: &str,
2238 ) -> bool {
2239 let removed = {
2240 let mut attempts = self.native_webrtc_carrier_attempts.write().await;
2241 if attempts
2242 .get(connection_id)
2243 .is_some_and(|attempt| Arc::ptr_eq(attempt, expected))
2244 {
2245 attempts.remove(connection_id)
2246 } else {
2247 None
2248 }
2249 };
2250 if let Some(attempt) = removed {
2251 self.record_native_iroh_carrier_debug_event(
2252 connection_id,
2253 &attempt.upgrade_id,
2254 "close-requested",
2255 Some(reason),
2256 );
2257 attempt.channel.close();
2258 let _ = attempt.pump.lock().await.take();
2259 true
2260 } else {
2261 false
2262 }
2263 }
2264
2265 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2266 async fn fail_native_webrtc_carrier_attempt(
2267 &self,
2268 connection_id: &str,
2269 remote_node_id: &str,
2270 request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
2271 failure_code: &'static str,
2272 notify_peer: bool,
2273 ) -> Option<crate::client::NativeTransportUpgradeGate> {
2274 let gate = {
2277 let key = (connection_id.to_string(), IrohPathKind::WebRtc);
2278 let mut gates = self.native_transport_upgrade_gates.lock().await;
2279 let gate = gates
2280 .get(&key)
2281 .filter(|gate| gate.upgrade_id == request.upgrade_id)
2282 .cloned()?;
2283 gates.remove(&key);
2284 gate
2285 };
2286 let retry_pending = webrtc_retry_attempt(failure_code, request.attempt).is_some();
2287 self.retire_native_webrtc_carrier_attempt(connection_id, &request.upgrade_id, failure_code)
2288 .await;
2289 if notify_peer {
2290 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
2291 request,
2292 failure_code,
2293 ) {
2294 let _ = self.send_iroh_carrier_control(connection_id, &failed).await;
2295 }
2296 }
2297 eprintln!(
2298 "[OpenRTC][WebRTC carrier] attempt failed connection_id={} upgrade_id={} failure_code={}",
2299 connection_id, request.upgrade_id, failure_code,
2300 );
2301 if !retry_pending {
2302 let _ = self
2303 .maybe_start_next_native_iroh_carrier(
2304 connection_id,
2305 remote_node_id,
2306 crate::route_policy::KnownRoute::WebRtc,
2307 )
2308 .await;
2309 }
2310 Some(gate)
2311 }
2312
2313 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2314 async fn schedule_failed_webrtc_retry(
2315 &self,
2316 connection_id: &str,
2317 remote_node_id: &str,
2318 request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
2319 failure_code: &str,
2320 failed_gate: crate::client::NativeTransportUpgradeGate,
2321 ) -> bool {
2322 let Some(attempt) = webrtc_retry_attempt(failure_code, request.attempt) else {
2323 return false;
2324 };
2325 let client = self.clone();
2329 let connection_id = connection_id.to_string();
2330 let remote_node_id = remote_node_id.to_string();
2331 let policy_epoch = failed_gate.policy_epoch;
2332 let peer_policy_epoch = failed_gate.peer_policy_epoch;
2333 let delay = webrtc_retry_delay(&request.upgrade_id);
2334 let generation = client
2337 .current_native_peer_data_generation(&connection_id, None)
2338 .await;
2339 tokio::spawn(async move {
2340 tokio::time::sleep(delay).await;
2341 if generation.is_none()
2342 || client
2343 .current_native_peer_data_generation(&connection_id, None)
2344 .await
2345 != generation
2346 || !client.iroh_carrier_policy_epoch_is_current(policy_epoch)
2347 || client.current_iroh_carrier_peer_policy_epoch(&connection_id)
2348 != peer_policy_epoch
2349 || client
2350 .native_webrtc_carrier_attempts
2351 .read()
2352 .await
2353 .contains_key(&connection_id)
2354 {
2355 return;
2356 }
2357 if let Err(error) = Box::pin(client.maybe_start_native_webrtc_iroh_carrier_attempt(
2358 &connection_id,
2359 Some(&remote_node_id),
2360 attempt.saturating_sub(1),
2361 ))
2362 .await
2363 {
2364 eprintln!(
2365 "[OpenRTC][WebRTC carrier] bounded retry failed connection_id={} attempt={} error={error:#}",
2366 connection_id, attempt,
2367 );
2368 }
2369 });
2370 true
2371 }
2372
2373 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2374 pub(super) async fn maybe_start_native_webrtc_iroh_carrier(
2375 &self,
2376 connection_id: &str,
2377 remote_node_id: Option<&str>,
2378 ) -> anyhow::Result<()> {
2379 self.maybe_start_native_webrtc_iroh_carrier_attempt(connection_id, remote_node_id, 0)
2380 .await
2381 }
2382
2383 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2384 fn maybe_start_native_webrtc_iroh_carrier_attempt<'a>(
2385 &'a self,
2386 connection_id: &'a str,
2387 remote_node_id: Option<&'a str>,
2388 retry_count: u8,
2389 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>> + Send + 'a>> {
2390 Box::pin(async move {
2391 if !self.is_webrtc_carrier_enabled().await {
2392 self.record_native_iroh_carrier_debug_event(
2393 connection_id,
2394 "",
2395 "start-deferred",
2396 Some("carrier-disabled"),
2397 );
2398 return Ok(());
2399 }
2400 let remote_node_id = remote_node_id
2401 .map(str::trim)
2402 .filter(|value| !value.is_empty())
2403 .ok_or_else(|| anyhow::anyhow!("WebRTC carrier requires remote node id"))?;
2404 let local_node_id = self
2405 .current_node_id()
2406 .await
2407 .ok_or_else(|| anyhow::anyhow!("WebRTC carrier requires local node id"))?;
2408 if local_node_id.as_str() <= remote_node_id {
2409 self.record_native_iroh_carrier_debug_event(
2410 connection_id,
2411 "",
2412 "start-deferred",
2413 Some("deterministic-responder"),
2414 );
2415 return Ok(());
2416 }
2417 let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
2418 let generation = self
2419 .current_native_peer_data_generation(connection_id, None)
2420 .await
2421 .ok_or_else(|| anyhow::anyhow!("WebRTC carrier generation is unavailable"))?;
2422 if let Some((_terminal, reason)) = self.session_admission_block_reason_for_transport(
2423 connection_id,
2424 Some(generation.transport_stable_id),
2425 ) {
2426 self.record_native_iroh_carrier_debug_event(
2427 connection_id,
2428 "",
2429 "start-deferred",
2430 Some(&reason),
2431 );
2432 return Ok(());
2433 }
2434 let selected_attempt = self
2435 .native_webrtc_carrier_attempts
2436 .read()
2437 .await
2438 .get(connection_id)
2439 .filter(|attempt| {
2440 attempt.remote_endpoint_id == endpoint_id
2441 && attempt.generation.transport_generation.saturating_add(1)
2442 == generation.transport_generation
2443 })
2444 .cloned();
2445 let selected_record = self
2446 .connection_manager
2447 .get_by_connection_id(connection_id)
2448 .await
2449 .is_some_and(|record| {
2450 record.transport_stable_id == Some(generation.transport_stable_id)
2451 && record.active_transport == crate::transport_label::WEBRTC
2452 });
2453 let refreshing = selected_attempt.as_ref().is_some_and(|attempt| {
2454 attempt.ice_refresh.try_lock().is_err()
2455 || attempt
2456 .ice_refresh_baseline
2457 .try_lock()
2458 .map(|baseline| baseline.is_some())
2459 .unwrap_or(true)
2460 });
2461 let healthy = selected_attempt.as_ref().is_some_and(|attempt| {
2462 attempt.channel.state() == crate::transport::NativeWebRTCState::Connected
2463 });
2464 if selected_attempt.is_some() && (refreshing || (selected_record && healthy)) {
2465 self.record_native_iroh_carrier_debug_event(
2470 connection_id,
2471 "",
2472 "start-deferred",
2473 Some("carrier-already-selected"),
2474 );
2475 return Ok(());
2476 }
2477 let base_path = self.iroh_path_kind(remote_node_id).await;
2478 if !crate::iroh_connection_policy::custom_carrier_base_allows(
2479 base_path,
2480 IrohPathKind::WebRtc,
2481 false,
2482 ) {
2483 let reason = format!("base-path-not-relay:{base_path:?}");
2484 self.record_native_iroh_carrier_debug_event(
2485 connection_id,
2486 "",
2487 "start-deferred",
2488 Some(&reason),
2489 );
2490 return Ok(());
2491 }
2492 self.get_connection(endpoint_id)
2493 .await
2494 .ok_or_else(|| anyhow::anyhow!("WebRTC carrier base is unavailable"))?;
2495 let kind = IrohPathKind::WebRtc;
2496 let gate_key = (connection_id.to_string(), kind);
2497 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
2498 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
2499 generation.into(),
2500 retry_count.saturating_add(1),
2501 )?;
2502 {
2503 let mut gates = self.native_transport_upgrade_gates.lock().await;
2504 if !reserve_native_transport_upgrade_gate(
2505 &mut gates,
2506 gate_key.clone(),
2507 &bootstrap.upgrade_id,
2508 generation,
2509 self.current_iroh_carrier_policy_epoch(),
2510 self.current_iroh_carrier_peer_policy_epoch(connection_id),
2511 false,
2512 ) {
2513 self.record_native_iroh_carrier_debug_event(
2514 connection_id,
2515 &bootstrap.upgrade_id,
2516 "start-deferred",
2517 Some("upgrade-gate-active"),
2518 );
2519 return Ok(());
2520 }
2521 }
2522 let result = async {
2523 let attempt = self
2524 .create_native_webrtc_carrier_attempt(
2525 connection_id,
2526 remote_node_id,
2527 bootstrap.clone(),
2528 generation,
2529 crate::transport::NativeWebRTCRole::Initiator,
2530 false,
2531 )
2532 .await?;
2533 self.send_iroh_carrier_control(connection_id, &bootstrap)
2534 .await?;
2535 attempt.channel.start().await?;
2536 Ok::<(), anyhow::Error>(())
2537 }
2538 .await;
2539 if let Err(error) = result {
2540 eprintln!(
2541 "[OpenRTC][WebRTC carrier] start failed connection_id={} upgrade_id={} retry_count={} error={error:#}",
2542 connection_id, bootstrap.upgrade_id, retry_count,
2543 );
2544 let retired = self
2545 .fail_native_webrtc_carrier_attempt(
2546 connection_id,
2547 remote_node_id,
2548 &bootstrap,
2549 "carrier-start-failed",
2550 true,
2551 )
2552 .await;
2553 if let Some(failed_gate) = retired {
2554 if self
2555 .schedule_failed_webrtc_retry(
2556 connection_id,
2557 remote_node_id,
2558 &bootstrap,
2559 "carrier-start-failed",
2560 failed_gate,
2561 )
2562 .await
2563 {
2564 return Ok(());
2565 }
2566 }
2567 return Err(error);
2568 }
2569 let client = self.clone();
2570 let connection_id = connection_id.to_string();
2571 let remote_node_id = remote_node_id.to_string();
2572 tokio::spawn(async move {
2573 let mut completion_phase_observed = false;
2574 loop {
2575 let timeout = if completion_phase_observed { 45 } else { 30 };
2576 tokio::time::sleep(std::time::Duration::from_secs(timeout)).await;
2577 let still_current = client
2578 .native_transport_upgrade_gates
2579 .lock()
2580 .await
2581 .get(&(connection_id.clone(), kind))
2582 .is_some_and(|gate| {
2583 native_transport_upgrade_gate_matches(gate, &bootstrap.upgrade_id)
2584 });
2585 if !still_current {
2586 return;
2587 }
2588 let proving = client
2589 .native_webrtc_carrier_attempts
2590 .read()
2591 .await
2592 .get(&connection_id)
2593 .filter(|attempt| attempt.upgrade_id == bootstrap.upgrade_id)
2594 .is_some_and(|attempt| {
2595 attempt
2596 .completion_started
2597 .load(std::sync::atomic::Ordering::Acquire)
2598 });
2599 if proving && !completion_phase_observed {
2600 completion_phase_observed = true;
2601 continue;
2602 }
2603 {
2604 let retired = client
2605 .fail_native_webrtc_carrier_attempt(
2606 &connection_id,
2607 &remote_node_id,
2608 &bootstrap,
2609 "carrier-timeout",
2610 true,
2611 )
2612 .await;
2613 if let Some(failed_gate) = retired {
2614 let _ = client
2615 .schedule_failed_webrtc_retry(
2616 &connection_id,
2617 &remote_node_id,
2618 &bootstrap,
2619 "carrier-timeout",
2620 failed_gate,
2621 )
2622 .await;
2623 }
2624 }
2625 return;
2626 }
2627 });
2628 Ok(())
2629 })
2630 }
2631
2632 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2636 pub async fn observe_webrtc_availability(&self, observation: &str) -> usize {
2637 if !self
2638 .webrtc_availability
2639 .lock()
2640 .unwrap_or_else(|error| error.into_inner())
2641 .observe(observation)
2642 {
2643 return 0;
2644 }
2645 let mut refreshed = 0;
2646 for record in self.connection_manager.list_active().await {
2647 match self.refresh_native_webrtc_ice(&record.connection_id, false).await {
2648 Ok(true) => refreshed += 1,
2649 Ok(false) => {},
2650 Err(error) => eprintln!("[OpenRTC][WebRTC carrier] availability refresh failed connection_id={} error={error}", record.connection_id),
2651 }
2652 }
2653 refreshed
2654 }
2655
2656 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2657 async fn native_ice_refresh_is_current(
2658 &self,
2659 connection_id: &str,
2660 attempt: &Arc<NativeWebRtcCarrierAttempt>,
2661 ) -> bool {
2662 if !self.is_webrtc_carrier_enabled().await
2663 || !self
2664 .native_webrtc_carrier_attempts
2665 .read()
2666 .await
2667 .get(connection_id)
2668 .is_some_and(|live| Arc::ptr_eq(live, attempt))
2669 {
2670 return false;
2671 }
2672 let Some(connection) = self.get_connection(attempt.remote_endpoint_id).await else {
2673 return false;
2674 };
2675 let stable_id = crate::transport_generation::for_connection(&connection);
2676 if attempt
2677 .ice_refresh_baseline
2678 .lock()
2679 .await
2680 .as_ref()
2681 .is_some_and(|loan| {
2682 loan.policy_epoch != self.current_iroh_carrier_policy_epoch()
2683 || loan.peer_policy_epoch
2684 != self.current_iroh_carrier_peer_policy_epoch(connection_id)
2685 })
2686 {
2687 return false;
2688 }
2689 self.current_native_peer_data_generation(connection_id, Some(stable_id))
2690 .await
2691 .is_some_and(|generation| {
2692 generation.transport_generation
2693 == attempt.generation.transport_generation.saturating_add(1)
2694 && self
2695 .session_admission_block_reason_for_transport(
2696 connection_id,
2697 Some(stable_id),
2698 )
2699 .is_none()
2700 })
2701 }
2702
2703 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2707 pub(super) async fn refresh_native_webrtc_ice(
2708 &self,
2709 connection_id: &str,
2710 peer_requested: bool,
2711 ) -> anyhow::Result<bool> {
2712 let attempt = self
2713 .native_webrtc_carrier_attempts
2714 .read()
2715 .await
2716 .get(connection_id)
2717 .cloned();
2718 let Some(attempt) = attempt else {
2719 return Ok(false);
2720 };
2721 if peer_requested && !attempt.channel.is_initiator() {
2722 return Ok(false);
2723 }
2724 let Some(record) = self
2725 .connection_manager
2726 .get_by_connection_id(connection_id)
2727 .await
2728 else {
2729 return Ok(false);
2730 };
2731 if !self.is_webrtc_carrier_enabled().await
2732 || record.state != crate::connection_manager::ConnectionState::Connected
2733 || record.active_transport != crate::transport_label::WEBRTC
2734 || record.transport_generation
2735 != attempt.generation.transport_generation.saturating_add(1)
2736 || record.endpoint_id.as_deref().or(record.node_id.as_deref())
2737 != Some(attempt.remote_endpoint_id.to_string().as_str())
2738 || self
2739 .session_admission_block_reason_for_transport(
2740 connection_id,
2741 record.transport_stable_id,
2742 )
2743 .is_some()
2744 || attempt.channel.state() != crate::transport::NativeWebRTCState::Connected
2745 {
2746 return Ok(false);
2747 }
2748 let Some(current) = self.get_connection(attempt.remote_endpoint_id).await else {
2749 return Ok(false);
2750 };
2751 if record.transport_stable_id != Some(crate::transport_generation::for_connection(¤t))
2752 {
2753 return Ok(false);
2754 }
2755 let mut gate = attempt.ice_refresh.lock().await;
2756 if !self
2757 .native_webrtc_carrier_attempts
2758 .read()
2759 .await
2760 .get(connection_id)
2761 .is_some_and(|live| Arc::ptr_eq(live, &attempt))
2762 || !gate.reserve(web_time::Instant::now())
2763 {
2764 return Ok(false);
2765 }
2766 self.record_native_iroh_carrier_debug_event(
2767 connection_id,
2768 &attempt.upgrade_id,
2769 "ice-refresh",
2770 Some(if peer_requested {
2771 "peer-network-change"
2772 } else {
2773 "host-network-change"
2774 }),
2775 );
2776 if attempt.channel.is_initiator() {
2777 let policy_epoch = self.current_iroh_carrier_policy_epoch();
2778 let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
2779 let node = self.iroh_node.read().await.as_ref().cloned();
2780 let Some(node) = node else { return Ok(false) };
2781 let Some(baseline) = node
2782 .baseline_for_ice_refresh(attempt.remote_endpoint_id)
2783 .await
2784 else {
2785 self.record_native_iroh_carrier_debug_event(
2786 connection_id,
2787 &attempt.upgrade_id,
2788 "ice-refresh-deferred",
2789 Some("independent-control-path-unavailable"),
2790 );
2791 return Ok(false);
2792 };
2793 if policy_epoch != self.current_iroh_carrier_policy_epoch()
2794 || peer_policy_epoch != self.current_iroh_carrier_peer_policy_epoch(connection_id)
2795 || !self
2796 .native_ice_refresh_is_current(connection_id, &attempt)
2797 .await
2798 {
2799 return Ok(false);
2800 }
2801 let Some(refresh_id) = crate::packet_carrier_transport::new_ice_refresh_id() else {
2802 return Ok(false);
2803 };
2804 *attempt.ice_refresh_baseline.lock().await =
2805 Some(crate::packet_carrier_transport::WebRtcIceRefreshPathLoan {
2806 _preference: baseline,
2807 since: web_time::Instant::now(),
2808 refresh_id: refresh_id.clone(),
2809 policy_epoch,
2810 peer_policy_epoch,
2811 started: false,
2812 });
2813 let sent = self.send_typescript_handshake_over_native_main(connection_id, &serde_json::json!({
2816 "type": "#pluto-signal", "content": {
2817 "transport": "webrtc", "type": "refresh-prepare", "negotiationId": attempt.upgrade_id, "refreshId": refresh_id,
2818 }
2819 })).await;
2820 if let Err(error) = sent {
2821 attempt.ice_refresh_baseline.lock().await.take();
2822 return Err(error);
2823 }
2824 } else {
2825 self.send_typescript_handshake_over_native_main(
2826 connection_id,
2827 &serde_json::json!({
2828 "type": "#pluto-signal",
2829 "content": {
2830 "transport": "webrtc",
2831 "type": "refresh-ice",
2832 "negotiationId": attempt.upgrade_id,
2833 }
2834 }),
2835 )
2836 .await?;
2837 }
2838 Ok(true)
2839 }
2840
2841 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2842 async fn handle_native_webrtc_iroh_carrier_control(
2843 &self,
2844 connection_id: &str,
2845 remote_node_id: Option<&str>,
2846 frame: &serde_json::Value,
2847 ) -> bool {
2848 if let Some(signal) = parse_webrtc_signal_message_for_transport(frame, "webrtc") {
2849 let attempt = self
2850 .native_webrtc_carrier_attempts
2851 .read()
2852 .await
2853 .get(connection_id)
2854 .cloned();
2855 if let Some(attempt) = attempt {
2856 if iroh_webrtc_signal_matches_upgrade(&signal, &attempt.upgrade_id) {
2857 let refresh_id = frame
2858 .get("content")
2859 .and_then(|content| content.get("refreshId"))
2860 .and_then(serde_json::Value::as_str)
2861 .filter(|id| id.len() == 32 && id.bytes().all(|b| b.is_ascii_hexdigit()));
2862 if matches!(
2863 signal.signal_type.as_str(),
2864 "refresh-prepare" | "refresh-ready"
2865 ) && !self
2866 .native_ice_refresh_is_current(connection_id, &attempt)
2867 .await
2868 {
2869 return true;
2870 }
2871 if signal.signal_type == "refresh-prepare" {
2872 let Some(refresh_id) = refresh_id else {
2873 return true;
2874 };
2875 if attempt.channel.is_initiator() {
2876 return true;
2877 }
2878 let current = self
2879 .current_native_peer_data_generation(connection_id, None)
2880 .await;
2881 if !current.is_some_and(|current| {
2882 current.transport_generation
2883 == attempt.generation.transport_generation.saturating_add(1)
2884 && self
2885 .session_admission_block_reason_for_transport(
2886 connection_id,
2887 Some(current.transport_stable_id),
2888 )
2889 .is_none()
2890 }) {
2891 return true;
2892 }
2893 let _refresh = attempt.ice_refresh.lock().await;
2894 if attempt.ice_refresh_baseline.lock().await.is_none() {
2895 let policy_epoch = self.current_iroh_carrier_policy_epoch();
2896 let peer_policy_epoch =
2897 self.current_iroh_carrier_peer_policy_epoch(connection_id);
2898 let node = self.iroh_node.read().await.as_ref().cloned();
2899 let Some(node) = node else { return true };
2900 let Some(baseline) = node
2901 .baseline_for_ice_refresh(attempt.remote_endpoint_id)
2902 .await
2903 else {
2904 return true;
2905 };
2906 if policy_epoch != self.current_iroh_carrier_policy_epoch()
2907 || peer_policy_epoch
2908 != self.current_iroh_carrier_peer_policy_epoch(connection_id)
2909 || !self
2910 .native_ice_refresh_is_current(connection_id, &attempt)
2911 .await
2912 {
2913 return true;
2914 }
2915 *attempt.ice_refresh_baseline.lock().await =
2916 Some(crate::packet_carrier_transport::WebRtcIceRefreshPathLoan {
2917 _preference: baseline,
2918 since: web_time::Instant::now(),
2919 refresh_id: refresh_id.to_owned(),
2920 policy_epoch,
2921 peer_policy_epoch,
2922 started: false,
2923 });
2924 }
2925 if !attempt
2926 .ice_refresh_baseline
2927 .lock()
2928 .await
2929 .as_ref()
2930 .is_some_and(|loan| loan.refresh_id == refresh_id)
2931 {
2932 return true;
2933 }
2934 let _ = self.send_typescript_handshake_over_native_main(connection_id, &serde_json::json!({
2935 "type": "#pluto-signal", "content": {
2936 "transport": "webrtc", "type": "refresh-ready", "negotiationId": attempt.upgrade_id, "refreshId": refresh_id,
2937 }
2938 })).await;
2939 } else if signal.signal_type == "refresh-ready" {
2940 if !attempt.channel.is_initiator() {
2941 return true;
2942 }
2943 let start = {
2944 let mut baseline = attempt.ice_refresh_baseline.lock().await;
2945 baseline
2946 .as_mut()
2947 .filter(|loan| {
2948 !loan.started
2949 && loan.since.elapsed().as_secs() < 30
2950 && Some(loan.refresh_id.as_str()) == refresh_id
2951 })
2952 .map(|loan| {
2953 loan.started = true;
2954 })
2955 .is_some()
2956 };
2957 if start && !matches!(attempt.channel.refresh_ice().await, Ok(true)) {
2958 attempt.ice_refresh_baseline.lock().await.take();
2959 }
2960 } else if signal.signal_type == "refresh-ice" {
2961 let _ = self.refresh_native_webrtc_ice(connection_id, true).await;
2962 } else {
2963 let restart_offer = signal.signal_type == "sdp"
2964 && signal.sdp.as_ref().is_some_and(|sdp| {
2965 sdp.get("type").and_then(serde_json::Value::as_str) == Some("offer")
2966 })
2967 && attempt.channel.state()
2968 == crate::transport::NativeWebRTCState::Connected;
2969 let _refresh = if restart_offer {
2972 Some(attempt.ice_refresh.lock().await)
2973 } else {
2974 None
2975 };
2976 if restart_offer {
2977 let mut baseline = attempt.ice_refresh_baseline.lock().await;
2978 if let Some(loan) = baseline.as_mut() {
2981 loan.started = true;
2982 attempt.channel.mark_remote_ice_refresh();
2983 }
2984 }
2985 if attempt.channel.handle_signal(signal).await.is_err() && restart_offer {
2986 attempt.ice_refresh_baseline.lock().await.take();
2987 }
2988 }
2989 } else {
2990 eprintln!(
2991 "[OpenRTC][WebRTC carrier] ignored stale signal connection_id={} current_upgrade_id={} signal_upgrade_id={:?}",
2992 connection_id, attempt.upgrade_id, signal.negotiation_id,
2993 );
2994 }
2995 }
2996 return true;
2997 }
2998 let Some(bootstrap) =
2999 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(frame)
3000 else {
3001 if frame.get("type").and_then(serde_json::Value::as_str)
3002 == Some(crate::iroh_carrier_bootstrap::CARRIER_BOOTSTRAP_FRAME_TYPE)
3003 {
3004 eprintln!("[OpenRTC][WebRTC carrier] rejected invalid bootstrap envelope={frame}");
3005 }
3006 return false;
3007 };
3008 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc {
3009 return false;
3010 }
3011 let Some(remote_node_id) = remote_node_id
3012 .map(str::trim)
3013 .filter(|value| !value.is_empty())
3014 else {
3015 return true;
3016 };
3017 let kind = IrohPathKind::WebRtc;
3018 let endpoint_id = match remote_node_id.parse::<iroh::EndpointId>() {
3019 Ok(value) => value,
3020 Err(_) => return true,
3021 };
3022 match bootstrap.action {
3023 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
3024 let local_node_id = match self.current_node_id().await {
3025 Some(value) => value,
3026 None => {
3027 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=local-endpoint-unavailable");
3028 return true;
3029 }
3030 };
3031 if local_node_id.as_str() >= remote_node_id {
3032 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=deterministic-initiator");
3033 return true;
3034 }
3035 if !self.is_webrtc_carrier_enabled().await {
3036 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=carrier-disabled");
3037 return true;
3038 }
3039 let duplicate_attempt = self
3040 .native_webrtc_carrier_attempts
3041 .read()
3042 .await
3043 .get(connection_id)
3044 .filter(|attempt| {
3045 native_carrier_duplicate_request_is_live(
3046 &bootstrap,
3047 &attempt.bootstrap,
3048 matches!(
3049 attempt.channel.state(),
3050 crate::transport::NativeWebRTCState::Failed
3051 | crate::transport::NativeWebRTCState::Closed
3052 ),
3053 )
3054 })
3055 .cloned();
3056 if let Some(attempt) = duplicate_attempt {
3057 if attempt.channel.state() == crate::transport::NativeWebRTCState::Connected {
3058 if let Ok(ready) =
3059 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3060 &bootstrap,
3061 )
3062 {
3063 let _ = self.send_iroh_carrier_control(connection_id, &ready).await;
3064 }
3065 }
3066 eprintln!("[OpenRTC][WebRTC carrier] responder reused connection_id={connection_id} reason=duplicate-live-bootstrap");
3067 return true;
3068 }
3069 if !crate::iroh_connection_policy::custom_carrier_base_allows(
3070 self.iroh_path_kind(remote_node_id).await,
3071 IrohPathKind::WebRtc,
3072 false,
3073 ) {
3074 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=carrier-already-selected");
3075 return true;
3076 }
3077 if self.get_connection(endpoint_id).await.is_none() {
3078 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=base-unavailable");
3079 return true;
3080 }
3081 let generation = match self
3086 .current_native_peer_data_generation(connection_id, None)
3087 .await
3088 {
3089 Some(value) => value,
3090 None => {
3091 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=generation-unavailable");
3092 return true;
3093 }
3094 };
3095 if self
3096 .reject_native_iroh_carrier_request_if_base_blocked(
3097 connection_id,
3098 &bootstrap,
3099 generation.transport_stable_id,
3100 "WebRTC",
3101 )
3102 .await
3103 {
3104 return true;
3105 }
3106 let node = match self.iroh_node.read().await.as_ref().cloned() {
3107 Some(value) => value,
3108 None => {
3109 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=node-unavailable");
3110 return true;
3111 }
3112 };
3113 let provider = match self.native_transport_upgrade_provider(kind).await {
3114 Some(value) => value,
3115 None => {
3116 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=provider-unavailable");
3117 return true;
3118 }
3119 };
3120 let gate_key = (connection_id.to_string(), kind);
3121 let current_attempt = self
3122 .native_webrtc_carrier_attempts
3123 .read()
3124 .await
3125 .get(connection_id)
3126 .cloned();
3127 {
3128 let mut gates = self.native_transport_upgrade_gates.lock().await;
3129 if let Some(current) = gates.get(&gate_key).filter(|current| {
3130 self.iroh_carrier_policy_epoch_is_current(current.policy_epoch)
3131 && current.peer_policy_epoch
3132 == self.current_iroh_carrier_peer_policy_epoch(connection_id)
3133 }) {
3134 if native_webrtc_duplicate_bootstrap_is_reserved_or_live(
3135 current,
3136 &bootstrap.upgrade_id,
3137 generation,
3138 current_attempt.as_deref(),
3139 ) {
3140 eprintln!("[OpenRTC][WebRTC carrier] responder reused connection_id={connection_id} reason=duplicate-bootstrap");
3141 return true;
3142 } else if native_transport_upgrade_gate_matches(
3143 current,
3144 &bootstrap.upgrade_id,
3145 ) {
3146 gates.remove(&gate_key);
3150 } else {
3151 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=competing-upgrade");
3152 return true;
3153 }
3154 }
3155 if !reserve_native_transport_upgrade_gate(
3156 &mut gates,
3157 gate_key.clone(),
3158 &bootstrap.upgrade_id,
3159 generation,
3160 self.current_iroh_carrier_policy_epoch(),
3161 self.current_iroh_carrier_peer_policy_epoch(connection_id),
3162 true,
3163 ) {
3164 eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=reservation-rejected");
3165 return true;
3166 }
3167 }
3168 let authorization_expiry = node
3169 .authorize_inbound_replacement(
3170 endpoint_id,
3171 provider.transport_id(),
3172 std::time::Duration::from_secs(45),
3173 )
3174 .await;
3175
3176 let attempt = match self
3185 .create_native_webrtc_carrier_attempt(
3186 connection_id,
3187 remote_node_id,
3188 bootstrap.clone(),
3189 generation,
3190 crate::transport::NativeWebRTCRole::Responder,
3191 true,
3192 )
3193 .await
3194 {
3195 Ok(attempt) => attempt,
3196 Err(error) => {
3197 node.revoke_inbound_replacement_if_current(
3198 endpoint_id,
3199 provider.transport_id(),
3200 authorization_expiry,
3201 )
3202 .await;
3203 let failure_code = native_iroh_carrier_failure_code(&error);
3204 eprintln!(
3205 "[OpenRTC][WebRTC carrier] responder start failed connection_id={} upgrade_id={} failure_code={} error={:#}",
3206 connection_id, bootstrap.upgrade_id, failure_code, error,
3207 );
3208 self.fail_native_webrtc_carrier_attempt(
3209 connection_id,
3210 remote_node_id,
3211 &bootstrap,
3212 failure_code,
3213 true,
3214 )
3215 .await;
3216 return true;
3217 }
3218 };
3219 let client = self.clone();
3220 let connection_id = connection_id.to_string();
3221 let remote_node_id = remote_node_id.to_string();
3222 tokio::spawn(async move {
3223 let result = async {
3224 client
3225 .wait_native_webrtc_carrier_connected(
3226 &connection_id,
3227 &bootstrap.upgrade_id,
3228 )
3229 .await?;
3230 let ready =
3231 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3232 &bootstrap,
3233 )?;
3234 client
3239 .send_iroh_carrier_control(&connection_id, &ready)
3240 .await?;
3241 client
3242 .complete_inbound_native_iroh_carrier_upgrade(
3243 &connection_id,
3244 &remote_node_id,
3245 &bootstrap.upgrade_id,
3246 generation,
3247 bootstrap.base.transport_generation,
3248 bootstrap.base.route_generation,
3249 kind,
3250 bootstrap.carrier.transport_id(),
3251 authorization_expiry,
3252 )
3253 .await?;
3254 let _ = attempt;
3255 Ok::<(), anyhow::Error>(())
3256 }
3257 .await;
3258 node.revoke_inbound_replacement_if_current(
3259 endpoint_id,
3260 provider.transport_id(),
3261 authorization_expiry,
3262 )
3263 .await;
3264 if let Err(error) = result {
3265 let failure_code = native_iroh_carrier_failure_code(&error);
3266 eprintln!(
3267 "[OpenRTC][WebRTC carrier] responder completion failed connection_id={} upgrade_id={} failure_code={} error={:#}",
3268 connection_id, bootstrap.upgrade_id, failure_code, error,
3269 );
3270 client
3271 .fail_native_webrtc_carrier_attempt(
3272 &connection_id,
3273 &remote_node_id,
3274 &bootstrap,
3275 failure_code,
3276 true,
3277 )
3278 .await;
3279 } else {
3280 remove_native_transport_upgrade_gate(
3281 &client.native_transport_upgrade_gates,
3282 &(connection_id.clone(), kind),
3283 &bootstrap.upgrade_id,
3284 )
3285 .await;
3286 }
3287 });
3288 }
3289 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
3290 let attempt = self
3291 .native_webrtc_carrier_attempts
3292 .read()
3293 .await
3294 .get(connection_id)
3295 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3296 .cloned();
3297 let Some(attempt) = attempt else {
3298 return true;
3299 };
3300 if attempt
3301 .completion_started
3302 .swap(true, std::sync::atomic::Ordering::AcqRel)
3303 {
3304 return true;
3305 }
3306 let client = self.clone();
3307 let connection_id = connection_id.to_string();
3308 let remote_node_id = remote_node_id.to_string();
3309 tokio::spawn(async move {
3310 let result = async {
3311 client
3312 .wait_native_webrtc_carrier_connected(
3313 &connection_id,
3314 &bootstrap.upgrade_id,
3315 )
3316 .await?;
3317 client
3318 .complete_native_iroh_carrier_upgrade(
3319 &connection_id,
3320 &remote_node_id,
3321 &bootstrap.upgrade_id,
3322 attempt.generation,
3323 kind,
3324 )
3325 .await
3326 }
3327 .await;
3328 if let Err(error) = result {
3329 let failure_code = native_iroh_carrier_failure_code(&error);
3330 let error_detail = format!("{error:#}");
3331 client.record_native_iroh_carrier_debug_event(
3332 &connection_id,
3333 &bootstrap.upgrade_id,
3334 "completion-failed",
3335 Some(&error_detail),
3336 );
3337 eprintln!(
3338 "[OpenRTC][WebRTC carrier] initiator completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
3339 connection_id, bootstrap.upgrade_id, failure_code,
3340 );
3341 let retired = client
3342 .fail_native_webrtc_carrier_attempt(
3343 &connection_id,
3344 &remote_node_id,
3345 &attempt.bootstrap,
3346 failure_code,
3347 true,
3348 )
3349 .await;
3350 if let Some(failed_gate) = retired {
3351 let _ = client
3352 .schedule_failed_webrtc_retry(
3353 &connection_id,
3354 &remote_node_id,
3355 &attempt.bootstrap,
3356 failure_code,
3357 failed_gate,
3358 )
3359 .await;
3360 }
3361 } else {
3362 remove_native_transport_upgrade_gate(
3363 &client.native_transport_upgrade_gates,
3364 &(connection_id.clone(), kind),
3365 &bootstrap.upgrade_id,
3366 )
3367 .await;
3368 }
3369 });
3370 }
3371 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
3372 let attempt = self
3373 .native_webrtc_carrier_attempts
3374 .read()
3375 .await
3376 .get(connection_id)
3377 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3378 .cloned();
3379 if let Some(attempt) = attempt {
3380 let failure_code = webrtc_peer_failure_code(bootstrap.failure_code.as_deref());
3381 let retired = self
3382 .fail_native_webrtc_carrier_attempt(
3383 connection_id,
3384 remote_node_id,
3385 &attempt.bootstrap,
3386 failure_code,
3387 false,
3388 )
3389 .await;
3390 if let Some(failed_gate) = retired {
3391 let _ = self
3392 .schedule_failed_webrtc_retry(
3393 connection_id,
3394 remote_node_id,
3395 &attempt.bootstrap,
3396 failure_code,
3397 failed_gate,
3398 )
3399 .await;
3400 }
3401 }
3402 }
3403 }
3404 true
3405 }
3406
3407 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3408 async fn create_native_moq_carrier_attempt(
3409 &self,
3410 connection_id: &str,
3411 remote_node_id: &str,
3412 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
3413 generation: crate::client::NativePeerDataGeneration,
3414 ) -> anyhow::Result<Arc<crate::client::NativeMoqCarrierAttempt>> {
3415 let config = self
3416 .transport_config
3417 .read()
3418 .await
3419 .moq
3420 .clone()
3421 .ok_or_else(|| anyhow::anyhow!("MoQ carrier config is unavailable"))?;
3422 let local_node_id = self
3423 .current_node_id()
3424 .await
3425 .ok_or_else(|| anyhow::anyhow!("local Iroh node id is unavailable"))?;
3426 let remote_endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
3427 if let Some(previous) = self
3432 .native_moq_carrier_attempts
3433 .write()
3434 .await
3435 .remove(connection_id)
3436 {
3437 previous.session.close();
3438 let _ = previous.pump.lock().await.take();
3439 }
3440 let packet_session = self
3441 .activate_native_iroh_packet_carrier(IrohPathKind::Moq, remote_endpoint_id)
3442 .await?;
3443 let session = Arc::new(
3444 crate::transport::NativeMoQSession::new_packet_carrier(
3445 &local_node_id,
3446 remote_node_id,
3447 &config,
3448 &bootstrap.carrier_session_id,
3449 )
3450 .await?,
3451 );
3452 if let Err(error) = session.start().await {
3457 return Err(error);
3458 }
3459 let application_key = self
3460 .application_crypto_key_for_connection(Some(connection_id))
3461 .ok_or_else(|| anyhow::anyhow!("MoQ carrier requires an installed application key"))?;
3462 let pump = match crate::native_moq_carrier::NativeMoqCarrierSession::attach(
3463 session.clone(),
3464 packet_session,
3465 bootstrap.frame_expectation()?,
3466 application_key,
3467 )
3468 .await
3469 {
3470 Ok(pump) => pump,
3471 Err(error) => {
3472 session.close_gracefully().await;
3473 return Err(error);
3474 }
3475 };
3476 let attempt = Arc::new(crate::client::NativeMoqCarrierAttempt {
3477 upgrade_id: bootstrap.upgrade_id.clone(),
3478 bootstrap,
3479 generation,
3480 remote_endpoint_id,
3481 session: session.clone(),
3482 pump: tokio::sync::Mutex::new(Some(pump)),
3483 });
3484 if let Some(previous) = self
3485 .native_moq_carrier_attempts
3486 .write()
3487 .await
3488 .insert(connection_id.to_string(), attempt.clone())
3489 {
3490 previous.session.close();
3494 let _ = previous.pump.lock().await.take();
3495 }
3496 Ok(attempt)
3497 }
3498
3499 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3500 async fn wait_native_moq_carrier_ready(
3501 &self,
3502 connection_id: &str,
3503 upgrade_id: &str,
3504 ) -> anyhow::Result<Arc<crate::client::NativeMoqCarrierAttempt>> {
3505 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(25);
3506 loop {
3507 let attempt = self
3508 .native_moq_carrier_attempts
3509 .read()
3510 .await
3511 .get(connection_id)
3512 .filter(|attempt| attempt.upgrade_id == upgrade_id)
3513 .cloned()
3514 .ok_or_else(|| anyhow::anyhow!("MoQ carrier attempt was retired"))?;
3515 match attempt.session.state() {
3516 crate::transport::NativeMoQState::Connected
3517 if attempt.session.is_peer_data_bidirectionally_ready().await =>
3518 {
3519 return Ok(attempt);
3525 }
3526 crate::transport::NativeMoQState::Failed
3527 | crate::transport::NativeMoQState::Closed => {
3528 anyhow::bail!(
3529 "MoQ Draft 14 reliable object-stream carrier negotiation failed: {}",
3530 attempt.session.peer_data_readiness_diagnostic().await,
3531 )
3532 }
3533 _ if tokio::time::Instant::now() >= deadline => {
3534 anyhow::bail!(
3535 "MoQ Draft 14 reliable object-stream carrier negotiation timed out: {}",
3536 attempt.session.peer_data_readiness_diagnostic().await,
3537 )
3538 }
3539 _ => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
3540 }
3541 }
3542 }
3543
3544 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3545 fn spawn_native_moq_carrier_terminal_watcher(
3546 &self,
3547 connection_id: String,
3548 attempt: Arc<crate::client::NativeMoqCarrierAttempt>,
3549 ) {
3550 let client = self.clone();
3551 tokio::spawn(async move {
3552 loop {
3553 let current = client
3554 .native_moq_carrier_attempts
3555 .read()
3556 .await
3557 .get(&connection_id)
3558 .is_some_and(|value| Arc::ptr_eq(value, &attempt));
3559 if !current {
3560 return;
3561 }
3562 if matches!(
3563 attempt.session.state(),
3564 crate::transport::NativeMoQState::Failed
3565 | crate::transport::NativeMoQState::Closed
3566 ) {
3567 let _ = client
3568 .close_current_iroh_carrier_generation_with_reason(
3569 &connection_id,
3570 attempt.remote_endpoint_id,
3571 IrohPathKind::Moq,
3572 attempt.generation.transport_generation.saturating_add(1),
3573 "iroh-carrier-ended",
3574 )
3575 .await;
3576 return;
3577 }
3578 let terminal_reason = attempt
3579 .pump
3580 .lock()
3581 .await
3582 .as_ref()
3583 .and_then(|pump| pump.take_terminal_reason());
3584 if let Some(reason) = terminal_reason {
3585 let _ = client
3586 .close_current_iroh_carrier_generation_with_reason(
3587 &connection_id,
3588 attempt.remote_endpoint_id,
3589 IrohPathKind::Moq,
3590 attempt.generation.transport_generation.saturating_add(1),
3591 reason,
3592 )
3593 .await;
3594 return;
3595 }
3596 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
3597 }
3598 });
3599 }
3600
3601 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3602 async fn retire_native_moq_carrier_attempt(&self, connection_id: &str, upgrade_id: &str) {
3603 let removed = {
3604 let mut attempts = self.native_moq_carrier_attempts.write().await;
3605 if attempts
3606 .get(connection_id)
3607 .is_some_and(|attempt| attempt.upgrade_id == upgrade_id)
3608 {
3609 attempts.remove(connection_id)
3610 } else {
3611 None
3612 }
3613 };
3614 if let Some(attempt) = removed {
3615 attempt.session.close();
3616 let _ = attempt.pump.lock().await.take();
3617 }
3618 }
3619
3620 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3621 async fn fail_native_moq_carrier_attempt(
3622 &self,
3623 connection_id: &str,
3624 remote_node_id: &str,
3625 request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
3626 failure_code: &'static str,
3627 notify_peer: bool,
3628 ) {
3629 let retry_pending = moq_retry_attempt(failure_code, request.attempt).is_some();
3630 self.retire_native_moq_carrier_attempt(connection_id, &request.upgrade_id)
3631 .await;
3632 remove_native_transport_upgrade_gate(
3633 &self.native_transport_upgrade_gates,
3634 &(connection_id.to_string(), IrohPathKind::Moq),
3635 &request.upgrade_id,
3636 )
3637 .await;
3638 if notify_peer {
3639 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
3640 request,
3641 failure_code,
3642 ) {
3643 let _ = self.send_iroh_carrier_control(connection_id, &failed).await;
3644 }
3645 }
3646 eprintln!(
3647 "[OpenRTC][MoQ carrier] attempt failed connection_id={} upgrade_id={} failure_code={}",
3648 connection_id, request.upgrade_id, failure_code,
3649 );
3650 if !retry_pending {
3651 let _ = self
3652 .maybe_start_next_native_iroh_carrier(
3653 connection_id,
3654 remote_node_id,
3655 crate::route_policy::KnownRoute::Moq,
3656 )
3657 .await;
3658 }
3659 }
3660
3661 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3662 async fn retry_stale_moq(
3663 &self,
3664 connection_id: &str,
3665 remote_node_id: &str,
3666 request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
3667 failure_code: &str,
3668 ) {
3669 let Some(attempt) = moq_retry_attempt(failure_code, request.attempt) else {
3670 return;
3671 };
3672 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
3675 if let Err(error) = self
3676 .maybe_start_native_moq_iroh_carrier_attempt(
3677 connection_id,
3678 Some(remote_node_id),
3679 attempt,
3680 )
3681 .await
3682 {
3683 eprintln!(
3684 "[OpenRTC][MoQ carrier] stale-base retry failed connection_id={} attempt={} error={error:#}",
3685 connection_id, attempt,
3686 );
3687 }
3688 }
3689
3690 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3691 pub(super) async fn maybe_start_native_moq_iroh_carrier(
3692 &self,
3693 connection_id: &str,
3694 remote_node_id: Option<&str>,
3695 ) -> anyhow::Result<()> {
3696 self.maybe_start_native_moq_iroh_carrier_attempt(connection_id, remote_node_id, 1)
3697 .await
3698 }
3699
3700 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3701 async fn maybe_start_native_moq_iroh_carrier_attempt(
3702 &self,
3703 connection_id: &str,
3704 remote_node_id: Option<&str>,
3705 attempt_number: u8,
3706 ) -> anyhow::Result<()> {
3707 if !self.is_moq_carrier_enabled().await {
3708 return Ok(());
3709 }
3710 let remote_node_id = remote_node_id
3711 .map(str::trim)
3712 .filter(|value| !value.is_empty())
3713 .ok_or_else(|| anyhow::anyhow!("MoQ carrier requires remote node id"))?;
3714 let local_node_id = self
3715 .current_node_id()
3716 .await
3717 .ok_or_else(|| anyhow::anyhow!("MoQ carrier requires local node id"))?;
3718 if local_node_id.as_str() <= remote_node_id {
3719 return Ok(());
3720 }
3721 let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
3722 let generation = self
3723 .current_native_peer_data_generation(connection_id, None)
3724 .await
3725 .ok_or_else(|| anyhow::anyhow!("MoQ carrier generation is unavailable"))?;
3726 let selected_attempt = self
3727 .native_moq_carrier_attempts
3728 .read()
3729 .await
3730 .get(connection_id)
3731 .filter(|attempt| {
3732 attempt.remote_endpoint_id == endpoint_id
3733 && attempt.session.state() == crate::transport::NativeMoQState::Connected
3734 && attempt.generation.transport_generation.saturating_add(1)
3735 == generation.transport_generation
3736 })
3737 .cloned();
3738 let selected_record = self
3739 .connection_manager
3740 .get_by_connection_id(connection_id)
3741 .await
3742 .is_some_and(|record| {
3743 record.transport_stable_id == Some(generation.transport_stable_id)
3744 && record.active_transport == crate::transport_label::MOQ
3745 });
3746 if selected_attempt.is_some() && selected_record {
3747 return Ok(());
3748 }
3749 if !crate::iroh_connection_policy::custom_carrier_base_allows(
3754 self.iroh_path_kind(remote_node_id).await,
3755 IrohPathKind::Moq,
3756 attempt_number == 2,
3757 ) {
3758 return Ok(());
3759 }
3760 self.get_connection(endpoint_id)
3761 .await
3762 .ok_or_else(|| anyhow::anyhow!("MoQ carrier base is unavailable"))?;
3763 let gate_key = (connection_id.to_string(), IrohPathKind::Moq);
3764 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
3765 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
3766 generation.into(),
3767 attempt_number,
3768 )?;
3769 {
3770 let mut gates = self.native_transport_upgrade_gates.lock().await;
3771 if !reserve_native_transport_upgrade_gate(
3772 &mut gates,
3773 gate_key.clone(),
3774 &bootstrap.upgrade_id,
3775 generation,
3776 self.current_iroh_carrier_policy_epoch(),
3777 self.current_iroh_carrier_peer_policy_epoch(connection_id),
3778 false,
3779 ) {
3780 return Ok(());
3781 }
3782 }
3783 let result = async {
3784 self.create_native_moq_carrier_attempt(
3785 connection_id,
3786 remote_node_id,
3787 bootstrap.clone(),
3788 generation,
3789 )
3790 .await?;
3791 self.send_iroh_carrier_control(connection_id, &bootstrap)
3792 .await?;
3793 Ok::<(), anyhow::Error>(())
3794 }
3795 .await;
3796 if let Err(error) = result {
3797 self.fail_native_moq_carrier_attempt(
3798 connection_id,
3799 remote_node_id,
3800 &bootstrap,
3801 "carrier-start-failed",
3802 true,
3803 )
3804 .await;
3805 return Err(error);
3806 }
3807 let client = self.clone();
3808 let connection_id = connection_id.to_string();
3809 let remote_node_id = remote_node_id.to_string();
3810 tokio::spawn(async move {
3811 tokio::time::sleep(std::time::Duration::from_secs(30)).await;
3812 let still_current = client
3813 .native_transport_upgrade_gates
3814 .lock()
3815 .await
3816 .get(&(connection_id.clone(), IrohPathKind::Moq))
3817 .is_some_and(|gate| {
3818 native_transport_upgrade_gate_matches(gate, &bootstrap.upgrade_id)
3819 });
3820 if still_current {
3821 client
3822 .fail_native_moq_carrier_attempt(
3823 &connection_id,
3824 &remote_node_id,
3825 &bootstrap,
3826 "carrier-timeout",
3827 true,
3828 )
3829 .await;
3830 }
3831 });
3832 Ok(())
3833 }
3834
3835 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3836 async fn handle_native_moq_iroh_carrier_control(
3837 &self,
3838 connection_id: &str,
3839 remote_node_id: Option<&str>,
3840 frame: &serde_json::Value,
3841 ) -> bool {
3842 let Some(bootstrap) =
3843 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(frame)
3844 else {
3845 if frame.get("type").and_then(serde_json::Value::as_str)
3846 == Some(crate::iroh_carrier_bootstrap::CARRIER_BOOTSTRAP_FRAME_TYPE)
3847 {
3848 eprintln!("[OpenRTC][MoQ carrier] rejected invalid bootstrap envelope={frame}");
3849 }
3850 return false;
3851 };
3852 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14 {
3853 return false;
3854 }
3855 let Some(remote_node_id) = remote_node_id
3856 .map(str::trim)
3857 .filter(|value| !value.is_empty())
3858 else {
3859 return true;
3860 };
3861 let endpoint_id = match remote_node_id.parse::<iroh::EndpointId>() {
3862 Ok(value) => value,
3863 Err(_) => return true,
3864 };
3865 match bootstrap.action {
3866 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
3867 let local_node_id = match self.current_node_id().await {
3868 Some(value) => value,
3869 None => return true,
3870 };
3871 if local_node_id.as_str() >= remote_node_id || !self.is_moq_carrier_enabled().await
3872 {
3873 return true;
3874 }
3875 let duplicate_attempt = self
3876 .native_moq_carrier_attempts
3877 .read()
3878 .await
3879 .get(connection_id)
3880 .filter(|attempt| {
3881 native_carrier_duplicate_request_is_live(
3882 &bootstrap,
3883 &attempt.bootstrap,
3884 matches!(
3885 attempt.session.state(),
3886 crate::transport::NativeMoQState::Failed
3887 | crate::transport::NativeMoQState::Closed
3888 ),
3889 )
3890 })
3891 .cloned();
3892 if let Some(attempt) = duplicate_attempt {
3893 if attempt.session.state() == crate::transport::NativeMoQState::Connected
3894 && attempt.session.is_peer_data_subscribed().await
3895 {
3896 if let Ok(ready) =
3897 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3898 &bootstrap,
3899 )
3900 {
3901 let _ = self.send_iroh_carrier_control(connection_id, &ready).await;
3902 }
3903 }
3904 eprintln!("[OpenRTC][MoQ carrier] responder reused connection_id={connection_id} reason=duplicate-live-bootstrap");
3905 return true;
3906 }
3907 if !crate::iroh_connection_policy::custom_carrier_base_allows(
3908 self.iroh_path_kind(remote_node_id).await,
3909 IrohPathKind::Moq,
3910 false,
3911 ) {
3912 return true;
3913 }
3914 if self.get_connection(endpoint_id).await.is_none() {
3915 return true;
3916 }
3917 let generation = match self
3918 .current_native_peer_data_generation(connection_id, None)
3919 .await
3920 {
3921 Some(value) => value,
3922 None => return true,
3923 };
3924 if self
3925 .reject_native_iroh_carrier_request_if_base_blocked(
3926 connection_id,
3927 &bootstrap,
3928 generation.transport_stable_id,
3929 "MoQ",
3930 )
3931 .await
3932 {
3933 return true;
3934 }
3935 let node = match self.iroh_node.read().await.as_ref().cloned() {
3936 Some(value) => value,
3937 None => return true,
3938 };
3939 let provider = match self
3940 .native_transport_upgrade_provider(IrohPathKind::Moq)
3941 .await
3942 {
3943 Some(value) => value,
3944 None => return true,
3945 };
3946 let gate_key = (connection_id.to_string(), IrohPathKind::Moq);
3947 {
3948 let mut gates = self.native_transport_upgrade_gates.lock().await;
3949 if gates.get(&gate_key).is_some_and(|current| {
3954 current.generation == generation
3955 && self.iroh_carrier_policy_epoch_is_current(current.policy_epoch)
3956 && current.peer_policy_epoch
3957 == self.current_iroh_carrier_peer_policy_epoch(connection_id)
3958 }) {
3959 return true;
3960 }
3961 if !reserve_native_transport_upgrade_gate(
3962 &mut gates,
3963 gate_key.clone(),
3964 &bootstrap.upgrade_id,
3965 generation,
3966 self.current_iroh_carrier_policy_epoch(),
3967 self.current_iroh_carrier_peer_policy_epoch(connection_id),
3968 true,
3969 ) {
3970 return true;
3971 }
3972 }
3973 let authorization_expiry = node
3974 .authorize_inbound_replacement(
3975 endpoint_id,
3976 provider.transport_id(),
3977 std::time::Duration::from_secs(45),
3978 )
3979 .await;
3980 let client = self.clone();
3981 let connection_id = connection_id.to_string();
3982 let remote_node_id = remote_node_id.to_string();
3983 tokio::spawn(async move {
3984 let result = async {
3985 client
3986 .create_native_moq_carrier_attempt(
3987 &connection_id,
3988 &remote_node_id,
3989 bootstrap.clone(),
3990 generation,
3991 )
3992 .await?;
3993 let ready =
3994 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3995 &bootstrap,
3996 )?;
3997 client
3998 .send_iroh_carrier_control(&connection_id, &ready)
3999 .await?;
4000 client
4001 .wait_native_moq_carrier_ready(&connection_id, &bootstrap.upgrade_id)
4002 .await?;
4003 client
4004 .complete_inbound_native_iroh_carrier_upgrade(
4005 &connection_id,
4006 &remote_node_id,
4007 &bootstrap.upgrade_id,
4008 generation,
4009 bootstrap.base.transport_generation,
4010 bootstrap.base.route_generation,
4011 IrohPathKind::Moq,
4012 bootstrap.carrier.transport_id(),
4013 authorization_expiry,
4014 )
4015 .await
4016 }
4017 .await;
4018 node.revoke_inbound_replacement_if_current(
4019 endpoint_id,
4020 provider.transport_id(),
4021 authorization_expiry,
4022 )
4023 .await;
4024 if let Err(error) = result {
4025 let failure_code = native_iroh_carrier_failure_code(&error);
4026 let error_detail = format!("{error:#}");
4027 client.record_native_iroh_carrier_debug_event(
4028 &connection_id,
4029 &bootstrap.upgrade_id,
4030 "completion-failed",
4031 Some(&error_detail),
4032 );
4033 eprintln!(
4034 "[OpenRTC][MoQ carrier] responder completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
4035 connection_id, bootstrap.upgrade_id, failure_code,
4036 );
4037 client
4038 .fail_native_moq_carrier_attempt(
4039 &connection_id,
4040 &remote_node_id,
4041 &bootstrap,
4042 failure_code,
4043 true,
4044 )
4045 .await;
4046 client
4047 .retry_stale_moq(
4048 &connection_id,
4049 &remote_node_id,
4050 &bootstrap,
4051 failure_code,
4052 )
4053 .await;
4054 } else {
4055 remove_native_transport_upgrade_gate(
4056 &client.native_transport_upgrade_gates,
4057 &(connection_id.clone(), IrohPathKind::Moq),
4058 &bootstrap.upgrade_id,
4059 )
4060 .await;
4061 if let Some(attempt) = client
4062 .native_moq_carrier_attempts
4063 .read()
4064 .await
4065 .get(&connection_id)
4066 .cloned()
4067 {
4068 client.spawn_native_moq_carrier_terminal_watcher(
4069 connection_id.clone(),
4070 attempt,
4071 );
4072 }
4073 }
4074 });
4075 }
4076 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
4077 let attempt = self
4078 .native_moq_carrier_attempts
4079 .read()
4080 .await
4081 .get(connection_id)
4082 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
4083 .cloned();
4084 let Some(attempt) = attempt else {
4085 return true;
4086 };
4087 let client = self.clone();
4088 let connection_id = connection_id.to_string();
4089 let remote_node_id = remote_node_id.to_string();
4090 tokio::spawn(async move {
4091 let result = async {
4092 client
4093 .wait_native_moq_carrier_ready(&connection_id, &bootstrap.upgrade_id)
4094 .await?;
4095 client
4096 .complete_native_iroh_carrier_upgrade(
4097 &connection_id,
4098 &remote_node_id,
4099 &bootstrap.upgrade_id,
4100 attempt.generation,
4101 IrohPathKind::Moq,
4102 )
4103 .await
4104 }
4105 .await;
4106 if let Err(error) = result {
4107 let failure_code = native_iroh_carrier_failure_code(&error);
4108 let error_detail = format!("{error:#}");
4109 client.record_native_iroh_carrier_debug_event(
4110 &connection_id,
4111 &bootstrap.upgrade_id,
4112 "completion-failed",
4113 Some(&error_detail),
4114 );
4115 eprintln!(
4116 "[OpenRTC][MoQ carrier] initiator completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
4117 connection_id, bootstrap.upgrade_id, failure_code,
4118 );
4119 client
4120 .fail_native_moq_carrier_attempt(
4121 &connection_id,
4122 &remote_node_id,
4123 &attempt.bootstrap,
4124 failure_code,
4125 true,
4126 )
4127 .await;
4128 client
4129 .retry_stale_moq(
4130 &connection_id,
4131 &remote_node_id,
4132 &attempt.bootstrap,
4133 failure_code,
4134 )
4135 .await;
4136 } else {
4137 remove_native_transport_upgrade_gate(
4138 &client.native_transport_upgrade_gates,
4139 &(connection_id.clone(), IrohPathKind::Moq),
4140 &bootstrap.upgrade_id,
4141 )
4142 .await;
4143 if let Some(current) = client
4144 .native_moq_carrier_attempts
4145 .read()
4146 .await
4147 .get(&connection_id)
4148 .cloned()
4149 {
4150 client.spawn_native_moq_carrier_terminal_watcher(
4151 connection_id.clone(),
4152 current,
4153 );
4154 }
4155 }
4156 });
4157 }
4158 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
4159 let request = self
4160 .native_moq_carrier_attempts
4161 .read()
4162 .await
4163 .get(connection_id)
4164 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
4165 .map(|attempt| attempt.bootstrap.clone());
4166 if let Some(request) = request {
4167 let (peer_failure_code, failure_code) =
4168 native_moq_peer_failure_code(bootstrap.failure_code.as_deref());
4169 self.record_native_iroh_carrier_debug_event(
4170 connection_id,
4171 &bootstrap.upgrade_id,
4172 "peer-failed",
4173 Some(peer_failure_code),
4174 );
4175 self.fail_native_moq_carrier_attempt(
4176 connection_id,
4177 remote_node_id,
4178 &request,
4179 failure_code,
4180 false,
4181 )
4182 .await;
4183 self.retry_stale_moq(connection_id, remote_node_id, &request, failure_code)
4184 .await;
4185 }
4186 }
4187 }
4188 true
4189 }
4190
4191 #[cfg(not(target_arch = "wasm32"))]
4192 async fn wait_for_native_ble_switch_admission(
4193 &self,
4194 connection_id: &str,
4195 endpoint_id: iroh::EndpointId,
4196 expected_generation: crate::client::NativePeerDataGeneration,
4197 ) -> anyhow::Result<()> {
4198 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(15);
4199 loop {
4200 let current_transport_stable_id = self
4201 .get_connection(endpoint_id)
4202 .await
4203 .map(|connection| crate::transport_generation::for_connection(&connection));
4204 if current_transport_stable_id != Some(expected_generation.transport_stable_id) {
4205 anyhow::bail!(
4206 "BLE switch request lost its base transport generation: expected={} current={:?}",
4207 expected_generation.transport_stable_id,
4208 current_transport_stable_id,
4209 );
4210 }
4211 if self
4212 .current_native_peer_data_generation(
4213 connection_id,
4214 Some(expected_generation.transport_stable_id),
4215 )
4216 .await
4217 != Some(expected_generation)
4218 {
4219 anyhow::bail!(
4220 "BLE switch request lost its logical generation: expected={:?}",
4221 expected_generation,
4222 );
4223 }
4224
4225 let block = self.session_admission_block_reason_for_transport(
4226 connection_id,
4227 Some(expected_generation.transport_stable_id),
4228 );
4229 match classify_native_ble_switch_admission(block.as_ref()) {
4230 NativeBleSwitchAdmission::Ready => return Ok(()),
4231 NativeBleSwitchAdmission::Reject => {
4232 anyhow::bail!(
4233 "BLE switch request admission rejected: {}",
4234 block
4235 .as_ref()
4236 .map(|(_, reason)| reason.as_str())
4237 .unwrap_or("unknown"),
4238 );
4239 }
4240 NativeBleSwitchAdmission::Wait => {}
4241 }
4242
4243 if tokio::time::Instant::now() >= deadline {
4244 anyhow::bail!(
4245 "timed out waiting for BLE switch admission: {}",
4246 block
4247 .as_ref()
4248 .map(|(_, reason)| reason.as_str())
4249 .unwrap_or("unknown"),
4250 );
4251 }
4252 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4253 }
4254 }
4255
4256 #[cfg(not(target_arch = "wasm32"))]
4257 async fn reserve_native_ble_upgrade_attempt(
4258 &self,
4259 connection_id: &str,
4260 generation: crate::client::NativePeerDataGeneration,
4261 ) -> Option<u8> {
4262 let mut attempts = self.native_ble_upgrade_attempts.lock().await;
4263 let (state, attempt) =
4264 reserve_native_ble_attempt_state(attempts.get(connection_id).copied(), generation);
4265 attempts.insert(connection_id.to_string(), state);
4266 attempt
4267 }
4268
4269 #[cfg(not(target_arch = "wasm32"))]
4270 async fn current_native_ble_upgrade_attempt(
4271 &self,
4272 connection_id: &str,
4273 generation: crate::client::NativePeerDataGeneration,
4274 ) -> Option<u8> {
4275 self.native_ble_upgrade_attempts
4276 .lock()
4277 .await
4278 .get(connection_id)
4279 .filter(|state| state.generation == generation)
4280 .map(|state| state.attempts)
4281 }
4282
4283 #[cfg(not(target_arch = "wasm32"))]
4284 async fn clear_native_ble_upgrade_attempts(
4285 &self,
4286 connection_id: &str,
4287 generation: crate::client::NativePeerDataGeneration,
4288 ) {
4289 let mut attempts = self.native_ble_upgrade_attempts.lock().await;
4290 if attempts
4291 .get(connection_id)
4292 .is_some_and(|state| state.generation == generation)
4293 {
4294 attempts.remove(connection_id);
4295 }
4296 }
4297
4298 #[cfg(not(target_arch = "wasm32"))]
4299 pub(crate) async fn wake_native_ble_recovery(&self, reason: &'static str) -> usize {
4300 if !self.is_ble_transport_enabled().await {
4301 return 0;
4302 }
4303
4304 let active_gates = self
4305 .native_transport_upgrade_gates
4306 .lock()
4307 .await
4308 .keys()
4309 .filter(|(_, kind)| *kind == IrohPathKind::Ble)
4310 .map(|(connection_id, _)| connection_id.clone())
4311 .collect::<std::collections::HashSet<_>>();
4312 let reset_count = {
4313 let mut attempts = self.native_ble_upgrade_attempts.lock().await;
4314 let before = attempts.len();
4315 attempts.retain(|connection_id, state| {
4316 native_ble_attempt_state_after_recovery_wake(
4317 *state,
4318 active_gates.contains(connection_id),
4319 )
4320 .is_some()
4321 });
4322 before.saturating_sub(attempts.len())
4323 };
4324
4325 let ble_capable_connections = self
4326 .native_peer_transport_capabilities
4327 .read()
4328 .await
4329 .iter()
4330 .filter(|(_, capabilities)| capabilities.contains(&NativePeerTransportCapability::Ble))
4331 .map(|(connection_id, _)| connection_id.clone())
4332 .collect::<std::collections::HashSet<_>>();
4333 let mut eligible_records = 0usize;
4334 for record in self.connection_manager.list_all().await {
4335 if !ble_capable_connections.contains(&record.connection_id)
4336 || !matches!(
4337 record.state,
4338 crate::connection_manager::ConnectionState::Connecting
4339 | crate::connection_manager::ConnectionState::Connected
4340 )
4341 {
4342 continue;
4343 }
4344 let Some(remote_node_id) = record
4345 .endpoint_id
4346 .as_deref()
4347 .or(record.node_id.as_deref())
4348 .map(str::trim)
4349 .filter(|value| !value.is_empty())
4350 else {
4351 continue;
4352 };
4353 eligible_records = eligible_records.saturating_add(1);
4354 if let Err(error) = self
4355 .maybe_start_native_ble_upgrade(&record.connection_id, Some(remote_node_id))
4356 .await
4357 {
4358 eprintln!(
4359 "[OpenRTC][BLE] recovery wake failed connection_id={} remote_node_id={} reason={} error={:#}",
4360 record.connection_id, remote_node_id, reason, error,
4361 );
4362 }
4363 }
4364
4365 eprintln!(
4366 "[OpenRTC][BLE] recovery wake reason={} reset_attempt_budgets={} eligible_records={}",
4367 reason, reset_count, eligible_records,
4368 );
4369 eligible_records
4370 }
4371
4372 #[cfg(not(target_arch = "wasm32"))]
4373 fn schedule_native_ble_upgrade_retry(
4374 &self,
4375 connection_id: String,
4376 remote_node_id: String,
4377 expected_generation: crate::client::NativePeerDataGeneration,
4378 failed_attempt: u8,
4379 reason: &'static str,
4380 ) {
4381 let Some(delay) =
4382 native_ble_retry_delay(failed_attempt, uuid::Uuid::new_v4().as_u128() as u64)
4383 else {
4384 eprintln!(
4385 "[OpenRTC][BLE] retry exhausted connection_id={} remote_node_id={} attempts={} reason={}",
4386 connection_id, remote_node_id, failed_attempt, reason,
4387 );
4388 return;
4389 };
4390 let client = self.clone();
4391 tokio::spawn(async move {
4392 tokio::time::sleep(delay).await;
4393 let attempt_is_current = client
4394 .current_native_ble_upgrade_attempt(&connection_id, expected_generation)
4395 .await
4396 == Some(failed_attempt);
4397 let generation_is_current = client
4398 .current_native_peer_data_generation(
4399 &connection_id,
4400 Some(expected_generation.transport_stable_id),
4401 )
4402 .await
4403 == Some(expected_generation);
4404 if !attempt_is_current || !generation_is_current {
4405 return;
4406 }
4407 eprintln!(
4408 "[OpenRTC][BLE] retrying upgrade connection_id={} remote_node_id={} next_attempt={} reason={}",
4409 connection_id,
4410 remote_node_id,
4411 failed_attempt.saturating_add(1),
4412 reason,
4413 );
4414 if let Err(error) = client
4415 .maybe_start_native_ble_upgrade(&connection_id, Some(&remote_node_id))
4416 .await
4417 {
4418 eprintln!(
4419 "[OpenRTC][BLE] retry setup failed connection_id={} remote_node_id={} error={}",
4420 connection_id, remote_node_id, error,
4421 );
4422 }
4423 });
4424 }
4425
4426 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
4427 async fn complete_inbound_native_ble_upgrade(
4428 &self,
4429 connection_id: &str,
4430 remote_node_id: &str,
4431 upgrade_id: &str,
4432 expected_generation: crate::client::NativePeerDataGeneration,
4433 proof_generation: crate::client::NativePeerDataGeneration,
4434 replacement_transport_id: u64,
4435 authorization_expires_at: std::time::Instant,
4436 ) -> anyhow::Result<()> {
4437 self.complete_inbound_native_iroh_carrier_upgrade(
4438 connection_id,
4439 remote_node_id,
4440 upgrade_id,
4441 expected_generation,
4442 proof_generation.transport_generation,
4443 proof_generation.route_generation,
4444 IrohPathKind::Ble,
4445 replacement_transport_id,
4446 authorization_expires_at,
4447 )
4448 .await
4449 }
4450
4451 #[cfg(all(not(target_arch = "wasm32"), not(feature = "iroh-carrier-core")))]
4452 async fn complete_inbound_native_ble_upgrade(
4453 &self,
4454 _connection_id: &str,
4455 _remote_node_id: &str,
4456 _upgrade_id: &str,
4457 _expected_generation: crate::client::NativePeerDataGeneration,
4458 _proof_generation: crate::client::NativePeerDataGeneration,
4459 _replacement_transport_id: u64,
4460 _authorization_expires_at: std::time::Instant,
4461 ) -> anyhow::Result<()> {
4462 anyhow::bail!("BLE replacement requires the Iroh custom-carrier core")
4463 }
4464
4465 #[cfg(not(target_arch = "wasm32"))]
4466 async fn accept_native_ble_switch_request(
4467 &self,
4468 connection_id: &str,
4469 remote_node_id: &str,
4470 upgrade_id: &str,
4471 expected_generation: crate::client::NativePeerDataGeneration,
4472 proof_generation: crate::client::NativePeerDataGeneration,
4473 ) -> anyhow::Result<bool> {
4474 let Some(local_node_id) = self.current_node_id().await else {
4475 return Ok(false);
4476 };
4477 let current_path = self.iroh_path_kind(remote_node_id).await;
4478 if !should_accept_native_ble_switch_request(&local_node_id, remote_node_id, current_path) {
4479 return Ok(false);
4480 }
4481 let remote_endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
4482 let provider = self
4483 .native_transport_upgrade_provider(IrohPathKind::Ble)
4484 .await
4485 .ok_or_else(|| anyhow::anyhow!("BLE upgrade provider is unavailable"))?;
4486 let replacement_transport_id = provider.transport_id();
4487 let native_node = self
4488 .iroh_node
4489 .read()
4490 .await
4491 .as_ref()
4492 .cloned()
4493 .ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
4494
4495 let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4496 {
4497 let mut gates = self.native_transport_upgrade_gates.lock().await;
4498 if !reserve_native_transport_upgrade_gate(
4499 &mut gates,
4500 gate_key.clone(),
4501 upgrade_id,
4502 expected_generation,
4503 self.current_iroh_carrier_policy_epoch(),
4504 self.current_iroh_carrier_peer_policy_epoch(connection_id),
4505 true,
4506 ) {
4507 return Ok(false);
4508 }
4509 }
4510 let authorization_expires_at = native_node
4511 .authorize_inbound_replacement(
4512 remote_endpoint_id,
4513 replacement_transport_id,
4514 std::time::Duration::from_secs(45),
4515 )
4516 .await;
4517
4518 if let Err(error) = self
4519 .send_native_ble_upgrade_control(
4520 connection_id,
4521 "switch-ready",
4522 upgrade_id,
4523 proof_generation,
4524 )
4525 .await
4526 {
4527 remove_native_transport_upgrade_gate(
4528 &self.native_transport_upgrade_gates,
4529 &gate_key,
4530 upgrade_id,
4531 )
4532 .await;
4533 native_node
4534 .revoke_inbound_replacement_if_current(
4535 remote_endpoint_id,
4536 replacement_transport_id,
4537 authorization_expires_at,
4538 )
4539 .await;
4540 return Err(error);
4541 }
4542
4543 eprintln!(
4544 "[OpenRTC][BLE] switch request accepted connection_id={} remote_node_id={} upgrade_id={}",
4545 connection_id, remote_node_id, upgrade_id,
4546 );
4547 let client = self.clone();
4548 let responder_upgrade_id = upgrade_id.to_string();
4549 let responder_connection_id = connection_id.to_string();
4550 let responder_remote_node_id = remote_node_id.to_string();
4551 tokio::spawn(async move {
4552 let result = client
4553 .complete_inbound_native_ble_upgrade(
4554 &responder_connection_id,
4555 &responder_remote_node_id,
4556 &responder_upgrade_id,
4557 expected_generation,
4558 proof_generation,
4559 replacement_transport_id,
4560 authorization_expires_at,
4561 )
4562 .await;
4563 native_node
4564 .revoke_inbound_replacement_if_current(
4565 remote_endpoint_id,
4566 replacement_transport_id,
4567 authorization_expires_at,
4568 )
4569 .await;
4570 remove_native_transport_upgrade_gate(
4571 &client.native_transport_upgrade_gates,
4572 &gate_key,
4573 &responder_upgrade_id,
4574 )
4575 .await;
4576 if let Err(error) = result {
4577 eprintln!(
4578 "[OpenRTC][BLE] inbound replacement completion failed connection_id={} remote_node_id={} upgrade_id={} error={:#}",
4579 responder_connection_id,
4580 responder_remote_node_id,
4581 responder_upgrade_id,
4582 error,
4583 );
4584 }
4585 });
4586 Ok(true)
4587 }
4588
4589 #[cfg(not(target_arch = "wasm32"))]
4590 pub(super) async fn maybe_start_native_ble_upgrade(
4591 &self,
4592 connection_id: &str,
4593 remote_node_id: Option<&str>,
4594 ) -> anyhow::Result<()> {
4595 let remote_node_id = remote_node_id
4596 .map(str::trim)
4597 .filter(|value| !value.is_empty())
4598 .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires a remote node id"))?;
4599 let local_node_id = self
4600 .current_node_id()
4601 .await
4602 .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires an initialized native node"))?;
4603 let current_path = self.iroh_path_kind(remote_node_id).await;
4604 if !should_initiate_native_ble_upgrade(local_node_id.as_str(), remote_node_id, current_path)
4605 {
4606 eprintln!(
4607 "[OpenRTC][BLE] upgrade decision connection_id={} local_node_id={} remote_node_id={} current_path={:?} role=responder-or-better-path",
4608 connection_id, local_node_id, remote_node_id, current_path,
4609 );
4610 return Ok(());
4611 }
4612 eprintln!(
4613 "[OpenRTC][BLE] upgrade decision connection_id={} local_node_id={} remote_node_id={} current_path={:?} role=initiator",
4614 connection_id, local_node_id, remote_node_id, current_path,
4615 );
4616
4617 let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
4618 let transport_stable_id = self
4619 .get_connection(endpoint_id)
4620 .await
4621 .map(|connection| crate::transport_generation::for_connection(&connection))
4622 .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires a live base transport"))?;
4623 let expected_generation = self
4624 .current_native_peer_data_generation(connection_id, Some(transport_stable_id))
4625 .await
4626 .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires a current peer generation"))?;
4627
4628 let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4629 let upgrade_id = uuid::Uuid::new_v4().to_string();
4630 {
4631 let mut gates = self.native_transport_upgrade_gates.lock().await;
4632 if !reserve_native_transport_upgrade_gate(
4633 &mut gates,
4634 gate_key.clone(),
4635 &upgrade_id,
4636 expected_generation,
4637 self.current_iroh_carrier_policy_epoch(),
4638 self.current_iroh_carrier_peer_policy_epoch(connection_id),
4639 false,
4640 ) {
4641 return Ok(());
4642 }
4643 }
4644 let Some(attempt) = self
4645 .reserve_native_ble_upgrade_attempt(connection_id, expected_generation)
4646 .await
4647 else {
4648 remove_native_transport_upgrade_gate(
4649 &self.native_transport_upgrade_gates,
4650 &gate_key,
4651 &upgrade_id,
4652 )
4653 .await;
4654 eprintln!(
4655 "[OpenRTC][BLE] upgrade skipped connection_id={} remote_node_id={} reason=attempts-exhausted",
4656 connection_id, remote_node_id,
4657 );
4658 return Ok(());
4659 };
4660
4661 let result = async {
4662 let provider = self
4663 .native_transport_upgrade_provider(IrohPathKind::Ble)
4664 .await
4665 .ok_or_else(|| anyhow::anyhow!("BLE upgrade provider is unavailable"))?;
4666 let _ = provider.prepare_endpoint_addr(endpoint_id).await?;
4669 self.send_native_ble_upgrade_control(
4670 connection_id,
4671 "switch-request",
4672 &upgrade_id,
4673 expected_generation,
4674 )
4675 .await?;
4676 eprintln!(
4677 "[OpenRTC][BLE] prepared upgrade connection_id={} remote_node_id={} upgrade_id={}",
4678 connection_id, remote_node_id, upgrade_id
4679 );
4680 Ok::<(), anyhow::Error>(())
4681 }
4682 .await;
4683
4684 if result.is_err() {
4685 let removed = remove_native_transport_upgrade_gate(
4686 &self.native_transport_upgrade_gates,
4687 &gate_key,
4688 &upgrade_id,
4689 )
4690 .await;
4691 if removed {
4692 self.schedule_native_ble_upgrade_retry(
4693 connection_id.to_string(),
4694 remote_node_id.to_string(),
4695 expected_generation,
4696 attempt,
4697 "setup-failed",
4698 );
4699 }
4700 } else {
4701 let client = self.clone();
4702 let gates = self.native_transport_upgrade_gates.clone();
4703 let connection_id = connection_id.to_string();
4704 let remote_node_id = remote_node_id.to_string();
4705 tokio::spawn(async move {
4706 tokio::time::sleep(NATIVE_BLE_SWITCH_RESPONSE_TIMEOUT).await;
4707 if remove_native_transport_upgrade_gate(&gates, &gate_key, &upgrade_id).await {
4708 client.schedule_native_ble_upgrade_retry(
4709 connection_id,
4710 remote_node_id,
4711 expected_generation,
4712 attempt,
4713 "switch-response-timeout",
4714 );
4715 }
4716 });
4717 }
4718 result
4719 }
4720
4721 #[cfg(not(target_arch = "wasm32"))]
4722 async fn handle_native_ble_upgrade_control(
4723 &self,
4724 connection_id: &str,
4725 remote_node_id: Option<&str>,
4726 frame: &serde_json::Value,
4727 ) -> bool {
4728 if frame.get("type").and_then(|value| value.as_str()) != Some("transport-upgrade")
4729 || frame.get("transport").and_then(|value| value.as_str()) != Some("ble")
4730 {
4731 return false;
4732 }
4733
4734 let Some(remote_node_id) = remote_node_id
4735 .map(str::trim)
4736 .filter(|value| !value.is_empty())
4737 else {
4738 return true;
4739 };
4740 let action = frame
4741 .get("action")
4742 .and_then(|value| value.as_str())
4743 .unwrap_or_default();
4744 let Some(upgrade_id) = frame
4745 .get("upgradeId")
4746 .and_then(|value| value.as_str())
4747 .map(str::trim)
4748 .filter(|value| !value.is_empty())
4749 .filter(|value| {
4750 (8..=96).contains(&value.len())
4751 && value
4752 .bytes()
4753 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
4754 })
4755 else {
4756 eprintln!("[OpenRTC][BLE] ignored control frame with invalid upgrade id");
4757 return true;
4758 };
4759 let Some(proof_generation) = frame
4760 .get("base")
4761 .cloned()
4762 .and_then(|value| serde_json::from_value(value).ok())
4763 .filter(|generation: &crate::client::NativePeerDataGeneration| {
4764 generation.transport_stable_id != 0 && generation.transport_generation != 0
4765 })
4766 else {
4767 eprintln!(
4768 "[OpenRTC][BLE] ignored control frame without a valid base-generation fence upgrade_id={}",
4769 upgrade_id,
4770 );
4771 return true;
4772 };
4773
4774 if !self.is_ble_transport_enabled().await {
4775 return true;
4776 }
4777
4778 let Some(local_node_id) = self.current_node_id().await else {
4779 return true;
4780 };
4781 let current_path = self.iroh_path_kind(remote_node_id).await;
4782 eprintln!(
4783 "[OpenRTC][BLE] control received connection_id={} remote_node_id={} action={} upgrade_id={} proof_base={:?} current_path={:?}",
4784 connection_id, remote_node_id, action, upgrade_id, proof_generation, current_path,
4785 );
4786
4787 match action {
4788 "switch-request" => {
4789 if !should_accept_native_ble_switch_request(
4790 &local_node_id,
4791 remote_node_id,
4792 current_path,
4793 ) {
4794 return true;
4795 }
4796 let Ok(endpoint_id) = remote_node_id.parse::<iroh::EndpointId>() else {
4797 return true;
4798 };
4799 let Some(transport_stable_id) = self
4800 .get_connection(endpoint_id)
4801 .await
4802 .map(|connection| crate::transport_generation::for_connection(&connection))
4803 else {
4804 return true;
4805 };
4806 let Some(expected_generation) = self
4807 .current_native_peer_data_generation(connection_id, Some(transport_stable_id))
4808 .await
4809 else {
4810 return true;
4811 };
4812 let admission_block = self.session_admission_block_reason_for_transport(
4813 connection_id,
4814 Some(transport_stable_id),
4815 );
4816 match classify_native_ble_switch_admission(admission_block.as_ref()) {
4817 NativeBleSwitchAdmission::Reject => {
4818 eprintln!(
4819 "[OpenRTC][BLE] switch request rejected connection_id={} remote_node_id={} upgrade_id={} reason={}",
4820 connection_id,
4821 remote_node_id,
4822 upgrade_id,
4823 admission_block
4824 .as_ref()
4825 .map(|(_, reason)| reason.as_str())
4826 .unwrap_or("unknown"),
4827 );
4828 return true;
4829 }
4830 NativeBleSwitchAdmission::Wait => {
4831 let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4832 {
4833 let mut gates = self.native_transport_upgrade_gates.lock().await;
4834 if !reserve_native_transport_upgrade_gate(
4835 &mut gates,
4836 gate_key.clone(),
4837 upgrade_id,
4838 expected_generation,
4839 self.current_iroh_carrier_policy_epoch(),
4840 self.current_iroh_carrier_peer_policy_epoch(connection_id),
4841 false,
4842 ) {
4843 return true;
4844 }
4845 }
4846 eprintln!(
4847 "[OpenRTC][BLE] switch request deferred connection_id={} remote_node_id={} upgrade_id={} transport_stable_id={} reason={}",
4848 connection_id,
4849 remote_node_id,
4850 upgrade_id,
4851 transport_stable_id,
4852 admission_block
4853 .as_ref()
4854 .map(|(_, reason)| reason.as_str())
4855 .unwrap_or("unknown"),
4856 );
4857 let client = self.clone();
4858 let connection_id = connection_id.to_string();
4859 let remote_node_id = remote_node_id.to_string();
4860 let upgrade_id = upgrade_id.to_string();
4861 tokio::spawn(async move {
4862 let result = client
4863 .wait_for_native_ble_switch_admission(
4864 &connection_id,
4865 endpoint_id,
4866 expected_generation,
4867 )
4868 .await;
4869 if let Err(error) = result {
4870 remove_native_transport_upgrade_gate(
4871 &client.native_transport_upgrade_gates,
4872 &(connection_id.clone(), IrohPathKind::Ble),
4873 &upgrade_id,
4874 )
4875 .await;
4876 eprintln!(
4877 "[OpenRTC][BLE] deferred switch request failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4878 connection_id, remote_node_id, upgrade_id, error,
4879 );
4880 return;
4881 }
4882 if let Err(error) = client
4883 .accept_native_ble_switch_request(
4884 &connection_id,
4885 &remote_node_id,
4886 &upgrade_id,
4887 expected_generation,
4888 proof_generation,
4889 )
4890 .await
4891 {
4892 eprintln!(
4893 "[OpenRTC][BLE] deferred switch request failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4894 connection_id, remote_node_id, upgrade_id, error,
4895 );
4896 }
4897 });
4898 return true;
4899 }
4900 NativeBleSwitchAdmission::Ready => {}
4901 }
4902 if let Err(error) = self
4903 .accept_native_ble_switch_request(
4904 connection_id,
4905 remote_node_id,
4906 upgrade_id,
4907 expected_generation,
4908 proof_generation,
4909 )
4910 .await
4911 {
4912 eprintln!(
4913 "[OpenRTC][BLE] switch request failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4914 connection_id, remote_node_id, upgrade_id, error,
4915 );
4916 }
4917 }
4918 "switch-ready" => {
4919 let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4920 let completion_gate_id = format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}{upgrade_id}");
4921 let expected_generation = {
4922 let mut gates = self.native_transport_upgrade_gates.lock().await;
4923 gates.get_mut(&gate_key).and_then(|current| {
4924 transition_native_ble_upgrade_gate_to_completion(
4925 current,
4926 upgrade_id,
4927 proof_generation,
4928 )
4929 })
4930 };
4931 let Some(expected_generation) = expected_generation else {
4932 eprintln!(
4933 "[OpenRTC][BLE] switch ready ignored connection_id={} remote_node_id={} upgrade_id={} reason=generation-mismatch",
4934 connection_id, remote_node_id, upgrade_id,
4935 );
4936 return true;
4937 };
4938 eprintln!(
4939 "[OpenRTC][BLE] switch ready accepted connection_id={} remote_node_id={} upgrade_id={}",
4940 connection_id, remote_node_id, upgrade_id,
4941 );
4942 let Ok(endpoint_id) = remote_node_id.parse::<iroh::EndpointId>() else {
4943 return true;
4944 };
4945 let client = self.clone();
4946 let connection_id = connection_id.to_string();
4947 let remote = remote_node_id.to_string();
4948 let upgrade_id = upgrade_id.to_string();
4949 let attempt = self
4950 .current_native_ble_upgrade_attempt(connection_id.as_str(), expected_generation)
4951 .await;
4952 tokio::spawn(async move {
4953 let gate_key = (connection_id.clone(), IrohPathKind::Ble);
4954 if let Err(error) = client
4955 .wait_for_native_ble_switch_admission(
4956 &connection_id,
4957 endpoint_id,
4958 expected_generation,
4959 )
4960 .await
4961 {
4962 let removed = remove_native_transport_upgrade_gate(
4963 &client.native_transport_upgrade_gates,
4964 &gate_key,
4965 &completion_gate_id,
4966 )
4967 .await;
4968 if removed {
4969 if let Some(attempt) = attempt {
4970 client.schedule_native_ble_upgrade_retry(
4971 connection_id.clone(),
4972 remote.clone(),
4973 expected_generation,
4974 attempt,
4975 "admission-wait-failed",
4976 );
4977 }
4978 }
4979 eprintln!(
4980 "[OpenRTC][BLE] switch ready admission failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4981 connection_id, remote, upgrade_id, error,
4982 );
4983 return;
4984 }
4985 let result = client
4986 .complete_native_ble_upgrade(
4987 &connection_id,
4988 &remote,
4989 &upgrade_id,
4990 expected_generation,
4991 )
4992 .await;
4993 let removed = remove_native_transport_upgrade_gate(
4994 &client.native_transport_upgrade_gates,
4995 &gate_key,
4996 &completion_gate_id,
4997 )
4998 .await;
4999 match result {
5000 Ok(()) => {
5001 client
5002 .clear_native_ble_upgrade_attempts(
5003 &connection_id,
5004 expected_generation,
5005 )
5006 .await;
5007 }
5008 Err(error) => {
5009 if removed {
5010 if let Some(attempt) = attempt {
5011 client.schedule_native_ble_upgrade_retry(
5012 connection_id.clone(),
5013 remote.clone(),
5014 expected_generation,
5015 attempt,
5016 "replacement-failed",
5017 );
5018 }
5019 }
5020 eprintln!(
5021 "[OpenRTC][BLE] upgrade failed connection_id={} remote_node_id={} upgrade_id={} error={}",
5022 connection_id, remote, upgrade_id, error
5023 );
5024 }
5025 }
5026 });
5027 }
5028 _ => {}
5029 }
5030 true
5031 }
5032
5033 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5034 pub(crate) async fn receive_native_iroh_carrier_candidate_proof(
5035 &self,
5036 connection_id: &str,
5037 send: iroh::endpoint::SendStream,
5038 recv: iroh::endpoint::RecvStream,
5039 upgrade_id: &str,
5040 proof_base_transport_generation: u64,
5041 proof_base_route_generation: u64,
5042 kind: IrohPathKind,
5043 transport_id: u64,
5044 ) -> anyhow::Result<(
5045 crate::application_crypto_streams::PeerSendStream,
5046 crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5047 crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5048 )> {
5049 let (send, mut recv) =
5050 self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5051 let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5052 let probe: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5053 serde_json::from_slice(&payload)?;
5054 let expected_carrier = match kind {
5055 IrohPathKind::Ble => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::Ble,
5056 IrohPathKind::WebRtc => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
5057 IrohPathKind::Moq => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
5058 _ => anyhow::bail!("unsupported inbound Iroh carrier proof kind {kind:?}"),
5059 };
5060 anyhow::ensure!(
5061 probe.frame_type == crate::iroh_carrier_proof::CARRIER_CANDIDATE_PROOF_TYPE
5062 && probe.role == crate::iroh_carrier_proof::CarrierCandidateProofRole::Probe
5063 && probe.carrier == expected_carrier
5064 && probe.transport_id == transport_id
5065 && probe.upgrade_id == upgrade_id
5066 && probe.base_transport_generation == proof_base_transport_generation
5067 && probe.base_route_generation == proof_base_route_generation,
5068 "carrier candidate proof does not match the authorized native attempt: expected_carrier={:?} actual_carrier={:?} expected_transport_id={} actual_transport_id={} expected_upgrade_id={} actual_upgrade_id={} expected_base_transport_generation={} actual_base_transport_generation={} expected_base_route_generation={} actual_base_route_generation={}",
5069 expected_carrier,
5070 probe.carrier,
5071 transport_id,
5072 probe.transport_id,
5073 upgrade_id,
5074 probe.upgrade_id,
5075 proof_base_transport_generation,
5076 probe.base_transport_generation,
5077 proof_base_route_generation,
5078 probe.base_route_generation,
5079 );
5080 probe.validate()?;
5081 let ack = crate::iroh_carrier_proof::CarrierCandidateProofFrame::ack_from(&probe)?;
5082 Ok((send, probe, ack))
5083 }
5084
5085 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5086 pub(crate) async fn send_native_iroh_carrier_candidate_ack(
5087 &self,
5088 mut send: crate::application_crypto_streams::PeerSendStream,
5089 ack: &crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5090 ) -> anyhow::Result<()> {
5091 send.write_all(&serde_json::to_vec(&ack)?).await?;
5092 send.finish_and_wait_for_peer(
5093 crate::iroh_carrier_proof::CARRIER_CANDIDATE_PROOF_DELIVERY_TIMEOUT,
5094 )
5095 .await?;
5096 Ok(())
5097 }
5098
5099 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5100 async fn receive_native_iroh_carrier_commit(
5101 &self,
5102 connection_id: &str,
5103 connection: &iroh::endpoint::Connection,
5104 probe: &crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5105 ) -> anyhow::Result<(
5106 crate::application_crypto_streams::PeerSendStream,
5107 crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5108 )> {
5109 let (send, recv) = connection.accept_bi().await?;
5110 let (send, mut recv) =
5111 self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5112 let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5113 let commit: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5114 serde_json::from_slice(&payload)?;
5115 anyhow::ensure!(
5116 commit.matches_commit_for_probe(probe),
5117 "carrier commit does not match the proven candidate"
5118 );
5119 let committed =
5120 crate::iroh_carrier_proof::CarrierCandidateProofFrame::committed_from(&commit)?;
5121 Ok((send, committed))
5122 }
5123
5124 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5125 async fn confirm_outbound_native_iroh_carrier_commit(
5126 &self,
5127 connection_id: &str,
5128 connection: &iroh::endpoint::Connection,
5129 probe: &crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5130 ) -> anyhow::Result<()> {
5131 let commit = crate::iroh_carrier_proof::CarrierCandidateProofFrame::commit_from(probe)?;
5132 let (send, recv) = connection.open_bi().await?;
5133 let (mut send, mut recv) =
5134 self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5135 send.write_all(&serde_json::to_vec(&commit)?).await?;
5136 send.finish()?;
5137 let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5138 let committed: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5139 serde_json::from_slice(&payload)?;
5140 anyhow::ensure!(
5141 committed.matches_committed_for_commit(&commit),
5142 "carrier commit acknowledgement does not match the current candidate"
5143 );
5144 Ok(())
5145 }
5146
5147 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5148 async fn prove_outbound_native_iroh_carrier_candidate(
5149 &self,
5150 connection_id: &str,
5151 connection: &iroh::endpoint::Connection,
5152 upgrade_id: &str,
5153 expected_generation: crate::client::NativePeerDataGeneration,
5154 kind: IrohPathKind,
5155 transport_id: u64,
5156 ) -> anyhow::Result<crate::iroh_carrier_proof::CarrierCandidateProofFrame> {
5157 anyhow::ensure!(
5158 self.application_crypto_key_for_connection(Some(connection_id))
5159 .is_some(),
5160 "carrier candidate proof requires an installed application-crypto key"
5161 );
5162 let carrier = match kind {
5163 IrohPathKind::Ble => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::Ble,
5164 IrohPathKind::WebRtc => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
5165 IrohPathKind::Moq => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
5166 _ => anyhow::bail!("unsupported outbound Iroh carrier proof kind {kind:?}"),
5167 };
5168 let probe = crate::iroh_carrier_proof::CarrierCandidateProofFrame::probe(
5169 carrier,
5170 transport_id,
5171 upgrade_id,
5172 expected_generation.transport_generation,
5173 expected_generation.route_generation,
5174 )?;
5175 let (send, recv) = connection.open_bi().await?;
5176 let (mut send, mut recv) =
5177 self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5178 send.write_all(&serde_json::to_vec(&probe)?).await?;
5179 send.finish()?;
5180 let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5181 let ack: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5182 serde_json::from_slice(&payload)?;
5183 anyhow::ensure!(
5184 ack.matches_probe(&probe),
5185 "carrier candidate proof acknowledgement did not match the current probe"
5186 );
5187 Ok(probe)
5188 }
5189
5190 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5191 async fn complete_native_iroh_carrier_upgrade(
5192 &self,
5193 connection_id: &str,
5194 remote_node_id: &str,
5195 upgrade_id: &str,
5196 expected_generation: crate::client::NativePeerDataGeneration,
5197 kind: IrohPathKind,
5198 ) -> anyhow::Result<()> {
5199 anyhow::ensure!(
5200 matches!(
5201 kind,
5202 IrohPathKind::Ble | IrohPathKind::WebRtc | IrohPathKind::Moq
5203 ),
5204 "unsupported Iroh packet carrier: {kind:?}"
5205 );
5206 let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
5207 self.record_native_iroh_carrier_debug_event(
5208 connection_id,
5209 upgrade_id,
5210 "candidate-outbound-provider-start",
5211 Some(kind.transport_label()),
5212 );
5213 let provider = self
5214 .native_transport_upgrade_provider(kind)
5215 .await
5216 .ok_or_else(|| anyhow::anyhow!("{kind:?} carrier provider is unavailable"))?;
5217 self.record_native_iroh_carrier_debug_event(
5218 connection_id,
5219 upgrade_id,
5220 "candidate-outbound-provider-ready",
5221 Some(kind.transport_label()),
5222 );
5223 let endpoint_addr = provider.prepare_endpoint_addr(endpoint_id).await?;
5224 self.record_native_iroh_carrier_debug_event(
5225 connection_id,
5226 upgrade_id,
5227 "candidate-outbound-address-ready",
5228 Some(kind.transport_label()),
5229 );
5230 let gate_key = (connection_id.to_string(), kind);
5231 let policy_epoch = self.current_iroh_carrier_policy_epoch();
5232 let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
5233 anyhow::ensure!(
5234 self.native_transport_upgrade_gates
5235 .lock()
5236 .await
5237 .get(&gate_key)
5238 .is_some_and(|gate| {
5239 native_transport_upgrade_gate_matches(gate, upgrade_id)
5240 && gate.policy_epoch == policy_epoch
5241 && gate.peer_policy_epoch == peer_policy_epoch
5242 }),
5243 "{kind:?} replacement belongs to a retired upgrade"
5244 );
5245 let node = self
5246 .iroh_node
5247 .read()
5248 .await
5249 .as_ref()
5250 .cloned()
5251 .ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
5252 self.record_native_iroh_carrier_debug_event(
5253 connection_id,
5254 upgrade_id,
5255 "candidate-outbound-dial-start",
5256 Some(kind.transport_label()),
5257 );
5258 let candidate = node
5259 .dial_replacement_candidate(endpoint_id, endpoint_addr, provider.transport_id())
5260 .await?;
5261 self.record_native_iroh_carrier_debug_event(
5262 connection_id,
5263 upgrade_id,
5264 "candidate-outbound-dial-ready",
5265 Some(kind.transport_label()),
5266 );
5267 let authorization_fence = self.capture_native_iroh_carrier_authorization_fence(
5268 connection_id,
5269 expected_generation,
5270 kind,
5271 policy_epoch,
5272 )?;
5273 self.record_native_iroh_carrier_debug_event(
5274 connection_id,
5275 upgrade_id,
5276 "candidate-outbound-proof-start",
5277 Some(kind.transport_label()),
5278 );
5279 let probe = self
5280 .prove_outbound_native_iroh_carrier_candidate(
5281 connection_id,
5282 candidate.connection(),
5283 upgrade_id,
5284 expected_generation,
5285 kind,
5286 provider.transport_id(),
5287 )
5288 .await?;
5289 self.record_native_iroh_carrier_debug_event(
5290 connection_id,
5291 upgrade_id,
5292 "candidate-outbound-proof-ready",
5293 Some(kind.transport_label()),
5294 );
5295 self.confirm_outbound_native_iroh_carrier_commit(
5300 connection_id,
5301 candidate.connection(),
5302 &probe,
5303 )
5304 .await?;
5305 self.record_native_iroh_carrier_debug_event(
5306 connection_id,
5307 upgrade_id,
5308 "candidate-outbound-commit-confirmed",
5309 Some(kind.transport_label()),
5310 );
5311 let committed_replacement = self
5312 .commit_proven_native_iroh_carrier_candidate(
5313 connection_id,
5314 remote_node_id,
5315 upgrade_id,
5316 expected_generation,
5317 kind,
5318 authorization_fence,
5319 node,
5320 candidate,
5321 )
5322 .await?;
5323 self.record_native_iroh_carrier_debug_event(
5324 connection_id,
5325 upgrade_id,
5326 "candidate-outbound-installed",
5327 Some(kind.transport_label()),
5328 );
5329 let replacement_transport_stable_id = committed_replacement
5330 .logical_result()
5331 .transport_stable_id
5332 .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement has no stable ID"))?;
5333 committed_replacement
5334 .finish(crate::lifecycle_reason::REASON_NATIVE_CUSTOM_TRANSPORT_UPGRADE.as_bytes());
5335 self.settle_committed_native_iroh_carrier_application_route(
5336 connection_id,
5337 upgrade_id,
5338 kind,
5339 replacement_transport_stable_id,
5340 )
5341 .await;
5342 Ok(())
5343 }
5344
5345 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5346 async fn complete_inbound_native_iroh_carrier_upgrade(
5347 &self,
5348 connection_id: &str,
5349 remote_node_id: &str,
5350 upgrade_id: &str,
5351 expected_generation: crate::client::NativePeerDataGeneration,
5352 proof_base_transport_generation: u64,
5353 proof_base_route_generation: u64,
5354 kind: IrohPathKind,
5355 transport_id: u64,
5356 authorization_expires_at: std::time::Instant,
5357 ) -> anyhow::Result<()> {
5358 let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
5359 let policy_epoch = self.current_iroh_carrier_policy_epoch();
5360 let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
5361 anyhow::ensure!(
5362 self.native_transport_upgrade_gates
5363 .lock()
5364 .await
5365 .get(&(connection_id.to_string(), kind))
5366 .is_some_and(|gate| {
5367 native_transport_upgrade_gate_matches(gate, upgrade_id)
5368 && gate.policy_epoch == policy_epoch
5369 && gate.peer_policy_epoch == peer_policy_epoch
5370 }),
5371 "{kind:?} inbound replacement belongs to a retired upgrade"
5372 );
5373 let node = self
5374 .iroh_node
5375 .read()
5376 .await
5377 .as_ref()
5378 .cloned()
5379 .ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
5380 self.record_native_iroh_carrier_debug_event(
5381 connection_id,
5382 upgrade_id,
5383 "candidate-inbound-wait-start",
5384 Some(kind.transport_label()),
5385 );
5386 let candidate = node
5387 .wait_for_inbound_replacement_candidate(
5388 endpoint_id,
5389 transport_id,
5390 authorization_expires_at,
5391 std::time::Duration::from_secs(40),
5392 )
5393 .await?;
5394 self.record_native_iroh_carrier_debug_event(
5395 connection_id,
5396 upgrade_id,
5397 "candidate-inbound-arrived",
5398 Some(kind.transport_label()),
5399 );
5400 let authorization_fence = self.capture_native_iroh_carrier_authorization_fence(
5401 connection_id,
5402 expected_generation,
5403 kind,
5404 policy_epoch,
5405 )?;
5406 let (send, recv) = candidate.connection().accept_bi().await?;
5407 self.record_native_iroh_carrier_debug_event(
5408 connection_id,
5409 upgrade_id,
5410 "candidate-inbound-proof-start",
5411 Some(kind.transport_label()),
5412 );
5413 let (send, probe, ack) = self
5414 .receive_native_iroh_carrier_candidate_proof(
5415 connection_id,
5416 send,
5417 recv,
5418 upgrade_id,
5419 proof_base_transport_generation,
5420 proof_base_route_generation,
5421 kind,
5422 transport_id,
5423 )
5424 .await?;
5425 self.record_native_iroh_carrier_debug_event(
5426 connection_id,
5427 upgrade_id,
5428 "candidate-inbound-proof-ready",
5429 Some(kind.transport_label()),
5430 );
5431 anyhow::ensure!(
5432 self.native_iroh_carrier_pre_ack_authority_is_current(
5433 connection_id,
5434 kind,
5435 upgrade_id,
5436 &authorization_fence,
5437 )
5438 .await,
5439 "{kind:?} inbound carrier authority changed before candidate acknowledgement"
5440 );
5441 self.send_native_iroh_carrier_candidate_ack(send, &ack)
5442 .await?;
5443 self.record_native_iroh_carrier_debug_event(
5444 connection_id,
5445 upgrade_id,
5446 "candidate-inbound-proof-acknowledged",
5447 Some(kind.transport_label()),
5448 );
5449 let (commit_send, committed) = self
5450 .receive_native_iroh_carrier_commit(connection_id, candidate.connection(), &probe)
5451 .await?;
5452 self.record_native_iroh_carrier_debug_event(
5453 connection_id,
5454 upgrade_id,
5455 "candidate-inbound-commit-received",
5456 Some(kind.transport_label()),
5457 );
5458 let committed_replacement = self
5459 .commit_proven_native_iroh_carrier_candidate(
5460 connection_id,
5461 remote_node_id,
5462 upgrade_id,
5463 expected_generation,
5464 kind,
5465 authorization_fence,
5466 node,
5467 candidate,
5468 )
5469 .await?;
5470 self.record_native_iroh_carrier_debug_event(
5471 connection_id,
5472 upgrade_id,
5473 "candidate-inbound-installed",
5474 Some(kind.transport_label()),
5475 );
5476 self.send_native_iroh_carrier_candidate_ack(commit_send, &committed)
5477 .await?;
5478 let replacement_transport_stable_id = committed_replacement
5479 .logical_result()
5480 .transport_stable_id
5481 .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement has no stable ID"))?;
5482 committed_replacement
5483 .finish(crate::lifecycle_reason::REASON_NATIVE_CUSTOM_TRANSPORT_UPGRADE.as_bytes());
5484 self.settle_committed_native_iroh_carrier_application_route(
5485 connection_id,
5486 upgrade_id,
5487 kind,
5488 replacement_transport_stable_id,
5489 )
5490 .await;
5491 Ok(())
5492 }
5493
5494 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5500 async fn settle_committed_native_iroh_carrier_application_route(
5501 &self,
5502 connection_id: &str,
5503 upgrade_id: &str,
5504 kind: IrohPathKind,
5505 replacement_transport_stable_id: u64,
5506 ) {
5507 self.record_native_iroh_carrier_debug_event(
5508 connection_id,
5509 upgrade_id,
5510 "application-crypto-confirmation-start",
5511 Some(kind.transport_label()),
5512 );
5513 let result = async {
5514 self.ensure_connection_application_crypto(connection_id, 20_000)
5515 .await?;
5516 anyhow::ensure!(
5517 self.connection_application_crypto_is_confirmed(
5518 connection_id,
5519 Some(replacement_transport_stable_id),
5520 ),
5521 "{kind:?} replacement key transcript did not confirm the committed generation"
5522 );
5523 anyhow::ensure!(
5524 self.native_application_stream_admitted_for_transport(
5525 connection_id,
5526 replacement_transport_stable_id,
5527 ),
5528 "{kind:?} replacement key transcript did not settle admission: {}",
5529 self.native_application_stream_pending_diagnostic(
5530 connection_id,
5531 replacement_transport_stable_id,
5532 ),
5533 );
5534 anyhow::ensure!(
5535 self.confirm_managed_connection_readiness_from_transport_proof(
5536 connection_id,
5537 replacement_transport_stable_id,
5538 )
5539 .await,
5540 "{kind:?} replacement key transcript belongs to a retired generation"
5541 );
5542 Ok::<(), anyhow::Error>(())
5543 }
5544 .await;
5545
5546 match result {
5547 Ok(()) => {
5548 self.record_native_iroh_carrier_debug_event(
5549 connection_id,
5550 upgrade_id,
5551 "application-crypto-confirmed",
5552 Some(kind.transport_label()),
5553 );
5554 eprintln!(
5555 "[OpenRTC][Iroh carrier] generation settled connection_id={} carrier={} transport_stable_id={}",
5556 connection_id,
5557 kind.transport_label(),
5558 replacement_transport_stable_id,
5559 );
5560 }
5561 Err(error) => {
5562 let error_detail = format!("{error:#}");
5563 self.record_native_iroh_carrier_debug_event(
5564 connection_id,
5565 upgrade_id,
5566 "application-crypto-confirmation-failed",
5567 Some(&error_detail),
5568 );
5569 eprintln!(
5570 "[OpenRTC][Iroh carrier] committed generation remains application-pending connection_id={} carrier={} transport_stable_id={} error={error:#}",
5571 connection_id,
5572 kind.transport_label(),
5573 replacement_transport_stable_id,
5574 );
5575 }
5576 }
5577 }
5578
5579 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5580 pub(super) fn capture_native_iroh_carrier_authorization_fence(
5581 &self,
5582 connection_id: &str,
5583 expected_generation: crate::client::NativePeerDataGeneration,
5584 kind: IrohPathKind,
5585 policy_epoch: u64,
5586 ) -> anyhow::Result<NativeIrohCarrierAuthorizationFence> {
5587 anyhow::ensure!(
5588 self.native_application_stream_admitted_for_transport(
5589 connection_id,
5590 expected_generation.transport_stable_id,
5591 ),
5592 "{kind:?} replacement base is not fully admitted: {}",
5593 self.native_application_stream_pending_diagnostic(
5594 connection_id,
5595 expected_generation.transport_stable_id,
5596 ),
5597 );
5598 Ok(NativeIrohCarrierAuthorizationFence {
5599 inbound_admission_was_current: self.inbound_session_token_admitted_for_transport(
5600 connection_id,
5601 expected_generation.transport_stable_id,
5602 ),
5603 remote_admission_proof: self
5604 .remote_session_admission_proofs
5605 .read()
5606 .ok()
5607 .and_then(|proofs| proofs.get(connection_id).cloned())
5608 .filter(|proof| {
5609 proof.transport_stable_id == expected_generation.transport_stable_id
5610 }),
5611 admission_fingerprint: self
5612 .session_token_registry
5613 .admission_fingerprint(connection_id),
5614 application_security_epoch_fingerprint: self
5615 .session_token_registry
5616 .application_security_epoch_fingerprint(connection_id),
5617 policy_epoch,
5618 })
5619 }
5620
5621 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5622 pub(super) fn native_iroh_carrier_authorization_epoch_is_current(
5623 &self,
5624 connection_id: &str,
5625 authorization_fence: &NativeIrohCarrierAuthorizationFence,
5626 ) -> bool {
5627 matches!(
5628 self.session_admission(connection_id),
5629 crate::session_token::SessionAdmission::Accepted { .. }
5630 ) && self
5631 .session_token_registry
5632 .admission_fingerprint(connection_id)
5633 == authorization_fence.admission_fingerprint
5634 && self
5635 .session_token_registry
5636 .application_security_epoch_fingerprint(connection_id)
5637 == authorization_fence.application_security_epoch_fingerprint
5638 && self.iroh_carrier_policy_epoch_is_current(authorization_fence.policy_epoch)
5639 }
5640
5641 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5642 pub(super) async fn native_iroh_carrier_pre_ack_authority_is_current(
5643 &self,
5644 connection_id: &str,
5645 kind: IrohPathKind,
5646 upgrade_id: &str,
5647 authorization_fence: &NativeIrohCarrierAuthorizationFence,
5648 ) -> bool {
5649 self.native_iroh_carrier_upgrade_is_current(
5655 connection_id,
5656 kind,
5657 upgrade_id,
5658 authorization_fence.policy_epoch,
5659 )
5660 .await
5661 && self.native_iroh_carrier_authorization_epoch_is_current(
5662 connection_id,
5663 authorization_fence,
5664 )
5665 }
5666
5667 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5668 async fn native_iroh_carrier_upgrade_is_current(
5669 &self,
5670 connection_id: &str,
5671 kind: IrohPathKind,
5672 upgrade_id: &str,
5673 policy_epoch: u64,
5674 ) -> bool {
5675 let gates = self.native_transport_upgrade_gates.lock().await;
5676 let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
5677 gates
5678 .get(&(connection_id.to_string(), kind))
5679 .is_some_and(|gate| {
5680 native_transport_upgrade_gate_matches(gate, upgrade_id)
5681 && gate.policy_epoch == policy_epoch
5682 && gate.peer_policy_epoch == peer_policy_epoch
5683 && self.iroh_carrier_policy_epoch_is_current(policy_epoch)
5684 })
5685 }
5686
5687 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5688 async fn commit_proven_native_iroh_carrier_candidate(
5689 &self,
5690 connection_id: &str,
5691 remote_node_id: &str,
5692 upgrade_id: &str,
5693 expected_generation: crate::client::NativePeerDataGeneration,
5694 kind: IrohPathKind,
5695 authorization_fence: NativeIrohCarrierAuthorizationFence,
5696 node: crate::native_node::IrohNativeNode,
5697 candidate: crate::native_node::PendingReplacementConnection,
5698 ) -> anyhow::Result<
5699 crate::native_node::CommittedReplacement<crate::connection_manager::ConnectionRecord>,
5700 > {
5701 anyhow::ensure!(
5702 self.native_iroh_carrier_upgrade_is_current(
5703 connection_id,
5704 kind,
5705 upgrade_id,
5706 authorization_fence.policy_epoch,
5707 )
5708 .await,
5709 "{kind:?} replacement belongs to a retired policy or upgrade"
5710 );
5711 let authorization_fence_for_commit = authorization_fence.clone();
5712 let NativeIrohCarrierAuthorizationFence {
5713 inbound_admission_was_current,
5714 remote_admission_proof,
5715 ..
5716 } = authorization_fence;
5717 let client = self.clone();
5718 let connection_id_for_commit = connection_id.to_string();
5719 let upgrade_id_for_commit = upgrade_id.to_string();
5720 let policy_epoch_for_commit = authorization_fence_for_commit.policy_epoch;
5721 let transport_source = Some(format!("{}-upgrade", kind.transport_label()));
5722 let committed_replacement = node
5723 .commit_replacement_candidate(
5724 candidate,
5725 expected_generation.transport_stable_id,
5726 move |replacement_transport_stable_id| async move {
5727 let mut gates = client.native_transport_upgrade_gates.lock().await;
5728 let peer_policy_epoch =
5729 client.current_iroh_carrier_peer_policy_epoch(&connection_id_for_commit);
5730 anyhow::ensure!(
5731 gates
5732 .get(&(connection_id_for_commit.clone(), kind))
5733 .is_some_and(|gate| {
5734 native_transport_upgrade_gate_matches(gate, &upgrade_id_for_commit)
5735 && gate.policy_epoch == policy_epoch_for_commit
5736 && gate.peer_policy_epoch == peer_policy_epoch
5737 && client.iroh_carrier_policy_epoch_is_current(
5738 policy_epoch_for_commit,
5739 )
5740 }),
5741 "{kind:?} replacement upgrade changed before atomic commit"
5742 );
5743 anyhow::ensure!(
5744 client.native_iroh_carrier_authorization_epoch_is_current(
5745 &connection_id_for_commit,
5746 &authorization_fence_for_commit,
5747 ),
5748 "{kind:?} replacement authorization epoch changed before atomic commit"
5749 );
5750 let installed = client
5751 .begin_crypto_bound_transport_replacement_commit(
5752 &connection_id_for_commit,
5753 expected_generation.transport_stable_id,
5754 expected_generation.transport_generation,
5755 expected_generation.route_generation,
5756 replacement_transport_stable_id,
5757 transport_source,
5758 )
5759 .await;
5760 if installed.is_some() && kind == IrohPathKind::WebRtc {
5761 gates.remove(&(connection_id_for_commit.clone(), kind));
5764 }
5765 drop(gates);
5766 installed
5767 .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement generation is stale"))
5768 },
5769 )
5770 .await?;
5771 #[cfg(feature = "transport-webrtc")]
5774 if kind == IrohPathKind::WebRtc {
5775 if let Some(attempt) = self
5776 .native_webrtc_carrier_attempts
5777 .read()
5778 .await
5779 .get(connection_id)
5780 .filter(|attempt| attempt.upgrade_id == upgrade_id)
5781 .cloned()
5782 {
5783 self.spawn_native_webrtc_carrier_terminal_watcher(
5784 connection_id.to_string(),
5785 remote_node_id.parse()?,
5786 attempt,
5787 );
5788 }
5789 }
5790 #[cfg(not(feature = "transport-webrtc"))]
5791 let _ = remote_node_id;
5792 let committed = committed_replacement.logical_result();
5793 let replacement_transport_stable_id = committed
5794 .transport_stable_id
5795 .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement has no stable ID"))?;
5796 self.retire_stale_native_main_route(connection_id, replacement_transport_stable_id)
5797 .await;
5798 anyhow::ensure!(
5799 committed.transport_stable_id == Some(replacement_transport_stable_id)
5800 && committed.transport_generation
5801 == expected_generation.transport_generation.saturating_add(1),
5802 "{kind:?} replacement failed atomic generation commit"
5803 );
5804 if inbound_admission_was_current {
5805 self.bind_inbound_session_token_transport(
5806 connection_id,
5807 replacement_transport_stable_id,
5808 );
5809 }
5810 if let Some(mut proof) = remote_admission_proof {
5811 proof.transport_stable_id = replacement_transport_stable_id;
5812 if let Ok(mut proofs) = self.remote_session_admission_proofs.write() {
5813 proofs.insert(connection_id.to_string(), proof);
5814 self.notify_connection_application_route_update();
5815 }
5816 }
5817 eprintln!(
5818 "[OpenRTC][Iroh carrier] admission proof rebound connection_id={} carrier={} transport_stable_id={} route=[{}]",
5819 connection_id,
5820 kind.transport_label(),
5821 replacement_transport_stable_id,
5822 self.native_application_stream_pending_diagnostic(
5823 connection_id,
5824 replacement_transport_stable_id,
5825 ),
5826 );
5827 anyhow::ensure!(
5828 self.report_transport_status_for_current_generation(
5829 connection_id,
5830 kind.transport_label(),
5831 None,
5832 )
5833 .await
5834 .is_some(),
5835 "{kind:?} replacement could not publish its proven route"
5836 );
5837 Ok(committed_replacement)
5851 }
5852
5853 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5854 async fn complete_native_ble_upgrade(
5855 &self,
5856 connection_id: &str,
5857 remote_node_id: &str,
5858 upgrade_id: &str,
5859 expected_generation: crate::client::NativePeerDataGeneration,
5860 ) -> anyhow::Result<()> {
5861 self.complete_native_iroh_carrier_upgrade(
5862 connection_id,
5863 remote_node_id,
5864 upgrade_id,
5865 expected_generation,
5866 IrohPathKind::Ble,
5867 )
5868 .await
5869 }
5870
5871 #[cfg(all(not(target_arch = "wasm32"), not(feature = "iroh-carrier-core")))]
5872 async fn complete_native_ble_upgrade(
5873 &self,
5874 _connection_id: &str,
5875 _remote_node_id: &str,
5876 _upgrade_id: &str,
5877 _expected_generation: crate::client::NativePeerDataGeneration,
5878 ) -> anyhow::Result<()> {
5879 anyhow::bail!("BLE replacement requires the Iroh custom-carrier core")
5880 }
5881
5882 #[cfg(not(target_arch = "wasm32"))]
5883 pub(crate) async fn migrate_remote_admission_proof_for_authorized_ble_upgrade(
5884 &self,
5885 connection_id: &str,
5886 endpoint_id: iroh::EndpointId,
5887 upgrade_id: &str,
5888 ) -> bool {
5889 let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
5890 let authorized = self
5891 .native_transport_upgrade_gates
5892 .lock()
5893 .await
5894 .get(&gate_key)
5895 .is_some_and(|expected| native_transport_upgrade_gate_matches(expected, upgrade_id));
5896 if !authorized || self.iroh_path_kind(&endpoint_id.to_string()).await != IrohPathKind::Ble {
5897 return false;
5898 }
5899 if !matches!(
5900 self.session_admission(connection_id),
5901 crate::session_token::SessionAdmission::Accepted {
5902 mechanism: crate::session_token::AdmissionMechanism::SessionToken,
5903 ..
5904 }
5905 ) {
5906 return false;
5907 }
5908 let Some(stable_id) = self
5909 .get_connection(endpoint_id)
5910 .await
5911 .map(|connection| crate::transport_generation::for_connection(&connection))
5912 else {
5913 return false;
5914 };
5915 let migrated = self
5916 .remote_session_admission_proofs
5917 .write()
5918 .ok()
5919 .and_then(|mut proofs| {
5920 let proof = proofs.get_mut(connection_id)?;
5921 let previous_stable_id = proof.transport_stable_id;
5922 proof.transport_stable_id = stable_id;
5923 Some(previous_stable_id)
5924 });
5925 if let Some(previous_stable_id) = migrated {
5926 self.notify_connection_application_route_update();
5927 eprintln!(
5928 "[OpenRTC][BLE][session-admission] migrated remote proof connection_id={} endpoint_id={} upgrade_id={} previous_stable_id={} stable_id={}",
5929 connection_id, endpoint_id, upgrade_id, previous_stable_id, stable_id
5930 );
5931 true
5932 } else {
5933 false
5934 }
5935 }
5936
5937 #[cfg(not(target_arch = "wasm32"))]
5938 pub(crate) async fn migrate_remote_admission_proof_for_pending_ble_upgrade(
5939 &self,
5940 connection_id: &str,
5941 endpoint_id: iroh::EndpointId,
5942 ) -> bool {
5943 let upgrade_id = self
5944 .native_transport_upgrade_gates
5945 .lock()
5946 .await
5947 .get(&(connection_id.to_string(), IrohPathKind::Ble))
5948 .map(|gate| gate.upgrade_id.clone());
5949 let Some(upgrade_id) = upgrade_id else {
5950 return false;
5951 };
5952 self.migrate_remote_admission_proof_for_authorized_ble_upgrade(
5953 connection_id,
5954 endpoint_id,
5955 &upgrade_id,
5956 )
5957 .await
5958 }
5959
5960 pub(crate) async fn maybe_handle_typescript_json_frame(
5961 &self,
5962 connection_id: &str,
5963 remote_node_id: Option<&str>,
5964 frame: &serde_json::Value,
5965 ) {
5966 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
5967 if self
5968 .handle_native_webrtc_iroh_carrier_control(connection_id, remote_node_id, frame)
5969 .await
5970 {
5971 return;
5972 }
5973 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
5974 if self
5975 .handle_native_moq_iroh_carrier_control(connection_id, remote_node_id, frame)
5976 .await
5977 {
5978 return;
5979 }
5980 #[cfg(not(target_arch = "wasm32"))]
5981 if self
5982 .handle_native_ble_upgrade_control(connection_id, remote_node_id, frame)
5983 .await
5984 {
5985 return;
5986 }
5987 }
5988
5989 pub async fn send_peer(&self, id: &str, data: &[u8]) -> anyhow::Result<()> {
5995 if data.is_empty() {
5996 return Ok(());
5997 }
5998 if data.len() > crate::native_protocol::MAX_PEER_MESSAGE_BYTES {
5999 anyhow::bail!(
6000 "peer message exceeds {}-byte limit; use a named stream for bulk data",
6001 crate::native_protocol::MAX_PEER_MESSAGE_BYTES,
6002 );
6003 }
6004 let protected = self.protect_outbound_peer_payload(id, data).await?;
6005 self.send_peer_over_iroh(id, &protected).await
6006 }
6007
6008 async fn protect_outbound_peer_payload(
6009 &self,
6010 id: &str,
6011 data: &[u8],
6012 ) -> anyhow::Result<Vec<u8>> {
6013 let mut connection_ids = Vec::new();
6014 if let Some(record) = self.connection_manager.get_by_connection_id(id).await {
6015 connection_ids.push(record.connection_id);
6016 } else if let Some(record) = self.connection_manager.best_connection_for_peer(id).await {
6017 connection_ids.push(record.connection_id);
6018 }
6019
6020 for connection_id in &connection_ids {
6021 if self
6022 .application_crypto_key_for_connection(Some(connection_id))
6023 .is_some()
6024 {
6025 return self.protect_outbound_application_payload(connection_id, data);
6026 }
6027 }
6028 if self.connection_ids_requiring_application_crypto(&connection_ids) {
6029 anyhow::bail!("application crypto required for peer send but no key is installed");
6030 }
6031 Ok(data.to_vec())
6032 }
6033}
6034
6035#[cfg(not(target_arch = "wasm32"))]
6037fn frame_iroh_peer_application_payload(data: &[u8]) -> anyhow::Result<Vec<u8>> {
6038 let frame_len = 1usize
6039 .checked_add(data.len())
6040 .and_then(|len| u32::try_from(len).ok())
6041 .ok_or_else(|| anyhow::anyhow!("peer application frame is too large"))?;
6042 let mut frame = Vec::with_capacity(4 + frame_len as usize);
6043 frame.extend_from_slice(&frame_len.to_be_bytes());
6044 frame.push(0x00);
6045 frame.extend_from_slice(data);
6046 Ok(frame)
6047}
6048
6049#[cfg(not(target_arch = "wasm32"))]
6056fn frame_typescript_native_main_json(message: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
6057 let serialized = serde_json::to_vec(message)?;
6058 let frame_len = serialized
6059 .len()
6060 .checked_add(1)
6061 .and_then(|len| u32::try_from(len).ok())
6062 .ok_or_else(|| anyhow::anyhow!("TypeScript native-main frame is too large"))?;
6063 let mut frame = Vec::with_capacity(4 + frame_len as usize);
6064 frame.extend_from_slice(&frame_len.to_be_bytes());
6065 frame.push(0x00);
6066 frame.extend_from_slice(&serialized);
6067 Ok(frame)
6068}
6069
6070#[cfg(not(target_arch = "wasm32"))]
6071impl Client {
6072 pub async fn send_peer_over_iroh(&self, id: &str, data: &[u8]) -> anyhow::Result<()> {
6076 let record = if let Some(r) = self.connection_manager.get_by_connection_id(id).await {
6077 r
6078 } else if let Some(r) = self.connection_manager.best_connection_for_peer(id).await {
6079 r
6080 } else {
6081 anyhow::bail!("send_peer_over_iroh: no connection record for id={}", id);
6082 };
6083
6084 let frame = frame_iroh_peer_application_payload(data)?;
6090 self.send_peer_application_frame(&record.connection_id, &frame, Some(5_000))
6091 .await?;
6092
6093 let transport_label = self.iroh_path_kind(id).await.transport_label();
6094 if let Some(transport_stable_id) = record.transport_stable_id {
6095 let _ = self
6096 .report_transport_status_for_generation(
6097 &record.connection_id,
6098 transport_label,
6099 None,
6100 transport_stable_id,
6101 record.transport_generation,
6102 record.route_generation,
6103 )
6104 .await;
6105 }
6106
6107 Ok(())
6108 }
6109
6110 async fn maybe_negotiate_typescript_application_crypto(
6111 &self,
6112 connection_id: &str,
6113 remote_node_id: Option<&str>,
6114 transport_stable_id: Option<u64>,
6115 handshake: &crate::native_protocol::TypeScriptHandshake,
6116 ) -> Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]> {
6117 let remote_public = handshake.application_key_agreement_public_key?;
6118 let remote_node_id = remote_node_id
6119 .map(str::trim)
6120 .filter(|value| !value.is_empty())?;
6121 let outcome = self
6122 .accept_application_key_handshake(
6123 connection_id,
6124 remote_node_id,
6125 transport_stable_id,
6126 handshake.action.as_deref(),
6127 remote_public,
6128 )
6129 .await
6130 .ok()?;
6131 outcome.key_changed.then_some(outcome.local_public_key)
6132 }
6133
6134 pub(crate) async fn send_typescript_capability_update(
6135 &self,
6136 connection_id: &str,
6137 reason: &str,
6138 application_key_agreement_public_key: Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]>,
6139 ) -> anyhow::Result<()> {
6140 self.send_typescript_handshake_update(
6141 connection_id,
6142 reason,
6143 application_key_agreement_public_key,
6144 "capability-update",
6145 )
6146 .await
6147 }
6148
6149 pub async fn ensure_connection_application_crypto(
6156 &self,
6157 connection_id: &str,
6158 timeout_ms: u64,
6159 ) -> anyhow::Result<()> {
6160 let connection_id = connection_id.trim();
6161 anyhow::ensure!(!connection_id.is_empty(), "connection id is required");
6162 let timeout_ms = timeout_ms.max(1);
6163 let initial = self
6164 .connection_manager
6165 .get_by_connection_id(connection_id)
6166 .await
6167 .ok_or_else(|| anyhow::anyhow!("connection {connection_id} is not registered"))?;
6168 let initial_endpoint = initial
6169 .endpoint_id
6170 .as_deref()
6171 .or(initial.node_id.as_deref())
6172 .map(ToOwned::to_owned)
6173 .ok_or_else(|| anyhow::anyhow!("connection {connection_id} has no endpoint"))?;
6174 let initial_transport_stable_id = initial.transport_stable_id;
6175 let initial_transport_generation = initial.transport_generation;
6176 let initial_route_generation = initial.route_generation;
6177 self.set_connection_application_crypto_required(connection_id);
6178
6179 let started = std::time::Instant::now();
6180 let retry_delays_ms = [0_u64, 250, 750];
6181 for (attempt, delay_ms) in retry_delays_ms.into_iter().enumerate() {
6182 if delay_ms > 0 {
6183 let remaining = timeout_ms.saturating_sub(started.elapsed().as_millis() as u64);
6184 if remaining == 0 {
6185 break;
6186 }
6187 tokio::time::sleep(std::time::Duration::from_millis(delay_ms.min(remaining))).await;
6188 }
6189 self.assert_application_crypto_generation(
6190 connection_id,
6191 &initial_endpoint,
6192 initial_transport_stable_id,
6193 initial_transport_generation,
6194 initial_route_generation,
6195 )
6196 .await?;
6197 if self
6198 .application_crypto_key_for_connection(Some(connection_id))
6199 .is_some()
6200 && self.connection_application_crypto_is_confirmed(
6201 connection_id,
6202 initial_transport_stable_id,
6203 )
6204 {
6205 return Ok(());
6206 }
6207 let public_key = self
6208 .get_or_create_connection_key_agreement(connection_id)
6209 .map_err(|error| {
6210 anyhow::anyhow!("application key agreement initialization failed: {error:?}")
6211 })?
6212 .public_key_bytes();
6213 self.send_typescript_capability_update(
6214 connection_id,
6215 if attempt == 0 {
6216 "application-crypto-required"
6217 } else {
6218 "application-crypto-retry"
6219 },
6220 Some(public_key),
6221 )
6222 .await?;
6223
6224 let attempt_deadline_ms = ((attempt + 1) as u64 * timeout_ms / 3).max(1);
6225 loop {
6226 if self
6227 .application_crypto_key_for_connection(Some(connection_id))
6228 .is_some()
6229 && self.connection_application_crypto_is_confirmed(
6230 connection_id,
6231 initial_transport_stable_id,
6232 )
6233 {
6234 return Ok(());
6235 }
6236 if started.elapsed().as_millis() as u64 >= attempt_deadline_ms
6237 || started.elapsed().as_millis() as u64 >= timeout_ms
6238 {
6239 break;
6240 }
6241 self.assert_application_crypto_generation(
6242 connection_id,
6243 &initial_endpoint,
6244 initial_transport_stable_id,
6245 initial_transport_generation,
6246 initial_route_generation,
6247 )
6248 .await?;
6249 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
6250 }
6251 }
6252
6253 anyhow::bail!(
6254 "connection {connection_id} reciprocal application key agreement timed out after {timeout_ms}ms"
6255 )
6256 }
6257
6258 async fn send_typescript_handshake_update(
6259 &self,
6260 connection_id: &str,
6261 reason: &str,
6262 application_key_agreement_public_key: Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]>,
6263 action: &str,
6264 ) -> anyhow::Result<()> {
6265 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
6266
6267 let local_webrtc_enabled = self.local_typescript_webrtc_capability().await;
6268 let local_moq_enabled = self.local_typescript_moq_capability().await;
6269 let local_ble_enabled = self.is_ble_transport_enabled().await;
6270 let dedicated_carrier_control = local_webrtc_enabled || local_moq_enabled;
6271 let native_admission_route_ready = {
6272 #[cfg(not(target_arch = "wasm32"))]
6273 {
6274 self.current_native_peer_data_generation(connection_id, None)
6275 .await
6276 .is_some_and(|generation| {
6277 self.native_admission_route_is_ready_for_transport(
6278 connection_id,
6279 Some(generation.transport_stable_id),
6280 )
6281 })
6282 }
6283 #[cfg(target_arch = "wasm32")]
6284 {
6285 true
6286 }
6287 };
6288 let dedicated_carrier_control_ready = dedicated_carrier_control_is_ready(
6289 dedicated_carrier_control,
6290 self.connection_application_crypto_is_confirmed(
6291 connection_id,
6292 self.connection_manager
6293 .get_by_connection_id(connection_id)
6294 .await
6295 .and_then(|record| record.transport_stable_id),
6296 ),
6297 native_admission_route_ready,
6298 );
6299 let application_key_agreement_public_key =
6303 application_key_agreement_public_key.or_else(|| {
6304 self.get_or_create_connection_key_agreement(connection_id)
6305 .ok()
6306 .map(|agreement| agreement.public_key_bytes())
6307 });
6308 let mut capability_frame = serde_json::json!({
6309 "type": "handshake",
6310 "action": action,
6311 "capabilities": {
6312 "carrierSchema": 2,
6313 "webrtc": local_webrtc_enabled,
6314 "moq": local_moq_enabled,
6315 "dedicatedCarrierControl": dedicated_carrier_control,
6316 "dedicatedCarrierControlReady": dedicated_carrier_control_ready,
6317 "ble": local_ble_enabled,
6318 }
6319 });
6320 if let Some(public_key) = application_key_agreement_public_key {
6321 capability_frame["capabilities"]["applicationKeyAgreement"] =
6322 serde_json::Value::Bool(true);
6323 capability_frame["applicationKeyAgreement"] = serde_json::json!({
6324 "algorithm": crate::key_agreement::KEY_ALGORITHM,
6325 "publicKey": URL_SAFE_NO_PAD.encode(public_key),
6326 });
6327 }
6328 eprintln!(
6329 "[OpenRTC][capability] sending update connection_id={} reason={} action={} webrtc={} moq={} ble={} application_key_agreement={}",
6330 connection_id,
6331 reason,
6332 action,
6333 local_webrtc_enabled,
6334 local_moq_enabled,
6335 local_ble_enabled,
6336 application_key_agreement_public_key.is_some(),
6337 );
6338 #[cfg(not(target_arch = "wasm32"))]
6339 let persistent_native_control = self.native_admission_stream_is_persistent(connection_id);
6340 #[cfg(target_arch = "wasm32")]
6341 let persistent_native_control = false;
6342 if application_key_reply_uses_fresh_signal(action, persistent_native_control) {
6343 self.send_typescript_handshake_over_fresh_signal(connection_id, &capability_frame)
6344 .await
6345 } else {
6346 self.send_typescript_handshake_over_native_main(connection_id, &capability_frame)
6347 .await
6348 }
6349 }
6350
6351 async fn send_typescript_handshake_over_fresh_signal(
6352 &self,
6353 connection_id: &str,
6354 message: &serde_json::Value,
6355 ) -> anyhow::Result<()> {
6356 let record = self
6357 .connection_manager
6358 .get_by_connection_id(connection_id)
6359 .await
6360 .ok_or_else(|| anyhow::anyhow!("connection {connection_id} is not registered"))?;
6361 let endpoint_id = record
6362 .endpoint_id
6363 .as_deref()
6364 .or(record.node_id.as_deref())
6365 .ok_or_else(|| anyhow::anyhow!("connection {connection_id} has no endpoint"))?
6366 .parse::<iroh::EndpointId>()?;
6367 let frame = frame_typescript_native_main_json(message)?;
6368 let (mut send, _recv) = self.open_bi_internal(endpoint_id).await?;
6369 let label = b"signal";
6370 send.write_all(&[0x00]).await?;
6371 send.write_all(&(label.len() as u32).to_be_bytes()).await?;
6372 send.write_all(label).await?;
6373 send.write_all(&frame).await?;
6374 crate::application_crypto_streams::PeerSendStream::plain(send)
6375 .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
6376 .await
6377 .map_err(anyhow::Error::msg)
6378 }
6379
6380 async fn send_typescript_handshake_over_native_main(
6381 &self,
6382 id: &str,
6383 message: &serde_json::Value,
6384 ) -> anyhow::Result<()> {
6385 let record = if let Some(r) = self.connection_manager.get_by_connection_id(id).await {
6386 r
6387 } else if let Some(r) = self.connection_manager.best_connection_for_peer(id).await {
6388 r
6389 } else {
6390 anyhow::bail!(
6391 "send_typescript_handshake_over_native_main: no connection record for id={}",
6392 id
6393 );
6394 };
6395
6396 let endpoint_str = record
6397 .endpoint_id
6398 .as_deref()
6399 .or(record.node_id.as_deref())
6400 .ok_or_else(|| {
6401 anyhow::anyhow!(
6402 "send_typescript_handshake_over_native_main: missing endpoint for connection_id={}",
6403 record.connection_id
6404 )
6405 })?;
6406
6407 let endpoint_id = endpoint_str.parse::<iroh::EndpointId>().map_err(|e| {
6408 anyhow::anyhow!(
6409 "send_typescript_handshake_over_native_main: invalid endpoint_id={} error={}",
6410 endpoint_str,
6411 e
6412 )
6413 })?;
6414
6415 let frame = frame_typescript_native_main_json(message)?;
6416
6417 self.send_native_main_frame(
6418 id,
6419 endpoint_id,
6420 message
6421 .get("type")
6422 .and_then(|value| value.as_str())
6423 .unwrap_or("control"),
6424 &frame,
6425 )
6426 .await
6427 .map_err(anyhow::Error::msg)
6428 }
6429}
6430
6431#[cfg(target_arch = "wasm32")]
6434impl Client {
6435 pub async fn send_peer_over_iroh(&self, id: &str, data: &[u8]) -> anyhow::Result<()> {
6436 let _ = (id, data);
6437 Err(anyhow::anyhow!(
6438 "send_peer_over_iroh: not available on WASM — use TypeScript Connection.sendTyped()"
6439 ))
6440 }
6441}
6442
6443#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6444fn parse_webrtc_signal_message_for_transport(
6445 frame: &serde_json::Value,
6446 expected_transport: &str,
6447) -> Option<crate::transport::WebRTCSignalMessage> {
6448 if frame.get("type").and_then(|value| value.as_str()) != Some("#pluto-signal") {
6449 return None;
6450 }
6451
6452 let content = frame.get("content")?;
6453 let transport = content
6454 .get("transport")
6455 .and_then(|value| value.as_str())
6456 .unwrap_or("webrtc");
6457 if transport != expected_transport {
6458 return None;
6459 }
6460
6461 let signal_type = content
6462 .get("type")
6463 .and_then(|value| value.as_str())
6464 .map(ToOwned::to_owned)
6465 .unwrap_or_else(|| "candidate".to_string());
6466
6467 Some(crate::transport::WebRTCSignalMessage {
6468 transport: transport.to_string(),
6469 signal_type,
6470 negotiation_id: content
6471 .get("negotiationId")
6472 .and_then(|value| value.as_str())
6473 .map(str::trim)
6474 .filter(|value| !value.is_empty())
6475 .map(ToOwned::to_owned),
6476 sdp: content.get("sdp").cloned(),
6477 candidate: content.get("candidate").cloned(),
6478 })
6479}
6480
6481#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6482fn iroh_webrtc_signal_matches_upgrade(
6483 signal: &crate::transport::WebRTCSignalMessage,
6484 upgrade_id: &str,
6485) -> bool {
6486 signal.negotiation_id.as_deref() == Some(upgrade_id)
6487}
6488
6489fn should_send_typescript_capability_update(
6490 local_webrtc_enabled: bool,
6491 local_moq_enabled: bool,
6492 local_ble_enabled: bool,
6493 has_application_key_agreement_public_key: bool,
6494 incoming_action: Option<&str>,
6495) -> bool {
6496 match incoming_action {
6497 Some("capability-update" | "response" | "ack") => return false,
6501 _ => {}
6502 }
6503
6504 has_application_key_agreement_public_key
6505 || local_webrtc_enabled
6506 || local_moq_enabled
6507 || local_ble_enabled
6508}
6509
6510fn application_key_reply_uses_fresh_signal(action: &str, persistent_native_control: bool) -> bool {
6511 matches!(action, "response" | "ack") && !persistent_native_control
6512}
6513
6514#[cfg(not(target_arch = "wasm32"))]
6515fn effective_typescript_transport_capability(
6516 uses_canonical_carrier_schema: bool,
6517 advertised: bool,
6518 known: bool,
6519) -> bool {
6520 if uses_canonical_carrier_schema {
6521 advertised
6522 } else {
6523 known
6524 }
6525}
6526
6527fn typescript_application_key_reply_action(
6528 incoming_action: Option<&str>,
6529 has_application_key: bool,
6530 _application_key_changed: bool,
6531 application_crypto_was_confirmed: bool,
6532) -> Option<&'static str> {
6533 if !has_application_key {
6534 return None;
6535 }
6536 match incoming_action {
6537 Some("ack") if !application_crypto_was_confirmed => Some("capability-update"),
6543 Some("ack") => None,
6544 Some("response") => Some("ack"),
6545 _ => Some("response"),
6546 }
6547}
6548
6549#[cfg(not(target_arch = "wasm32"))]
6550fn should_initiate_native_ble_upgrade(
6551 local_node_id: &str,
6552 remote_node_id: &str,
6553 current_path: IrohPathKind,
6554) -> bool {
6555 local_node_id > remote_node_id
6556 && !matches!(
6557 current_path,
6558 IrohPathKind::DirectQuic | IrohPathKind::DirectLan | IrohPathKind::Ble
6559 )
6560}
6561
6562#[cfg(not(target_arch = "wasm32"))]
6563fn should_accept_native_ble_switch_request(
6564 local_node_id: &str,
6565 remote_node_id: &str,
6566 current_path: IrohPathKind,
6567) -> bool {
6568 local_node_id < remote_node_id
6569 && matches!(current_path, IrohPathKind::Relay | IrohPathKind::Unknown)
6570}
6571
6572#[cfg(not(target_arch = "wasm32"))]
6573fn native_transport_upgrade_gate_matches(
6574 current: &crate::client::NativeTransportUpgradeGate,
6575 upgrade_id: &str,
6576) -> bool {
6577 current.upgrade_id == upgrade_id
6578 || current
6579 .upgrade_id
6580 .strip_prefix(NATIVE_BLE_COMPLETION_GATE_PREFIX)
6581 .is_some_and(|completing_upgrade_id| completing_upgrade_id == upgrade_id)
6582}
6583
6584#[cfg(not(target_arch = "wasm32"))]
6585fn transition_native_ble_upgrade_gate_to_completion(
6586 current: &mut crate::client::NativeTransportUpgradeGate,
6587 upgrade_id: &str,
6588 proof_generation: crate::client::NativePeerDataGeneration,
6589) -> Option<crate::client::NativePeerDataGeneration> {
6590 if current.upgrade_id != upgrade_id || current.generation != proof_generation {
6591 return None;
6592 }
6593
6594 current.upgrade_id = format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}{upgrade_id}");
6595 Some(current.generation)
6596}
6597
6598#[cfg(all(
6603 not(target_arch = "wasm32"),
6604 any(feature = "transport-webrtc", feature = "transport-moq")
6605))]
6606fn native_carrier_duplicate_request_is_live(
6607 request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
6608 attempt: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
6609 terminal: bool,
6610) -> bool {
6611 !terminal && request == attempt
6612}
6613
6614#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6617fn native_webrtc_duplicate_bootstrap_is_reserved_or_live(
6618 gate: &crate::client::NativeTransportUpgradeGate,
6619 upgrade_id: &str,
6620 generation: crate::client::NativePeerDataGeneration,
6621 attempt: Option<&crate::client::NativeWebRtcCarrierAttempt>,
6622) -> bool {
6623 if !native_transport_upgrade_gate_matches(gate, upgrade_id) || gate.generation != generation {
6624 return false;
6625 }
6626
6627 attempt.is_none_or(|attempt| {
6635 native_webrtc_duplicate_bootstrap_matches_live_attempt(
6636 gate,
6637 upgrade_id,
6638 generation,
6639 &attempt.upgrade_id,
6640 attempt.generation,
6641 attempt.channel.state(),
6642 )
6643 })
6644}
6645
6646#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6647fn native_webrtc_duplicate_bootstrap_matches_live_attempt(
6648 gate: &crate::client::NativeTransportUpgradeGate,
6649 upgrade_id: &str,
6650 generation: crate::client::NativePeerDataGeneration,
6651 attempt_upgrade_id: &str,
6652 attempt_generation: crate::client::NativePeerDataGeneration,
6653 attempt_state: crate::transport::NativeWebRTCState,
6654) -> bool {
6655 native_transport_upgrade_gate_matches(gate, upgrade_id)
6656 && gate.generation == generation
6657 && attempt_upgrade_id == upgrade_id
6658 && attempt_generation == generation
6659 && !matches!(
6660 attempt_state,
6661 crate::transport::NativeWebRTCState::Failed
6662 | crate::transport::NativeWebRTCState::Closed
6663 )
6664}
6665
6666#[cfg(not(target_arch = "wasm32"))]
6667fn reserve_native_transport_upgrade_gate(
6668 gates: &mut std::collections::HashMap<
6669 (String, IrohPathKind),
6670 crate::client::NativeTransportUpgradeGate,
6671 >,
6672 key: (String, IrohPathKind),
6673 upgrade_id: &str,
6674 generation: crate::client::NativePeerDataGeneration,
6675 policy_epoch: u64,
6676 peer_policy_epoch: u64,
6677 replace_existing: bool,
6678) -> bool {
6679 if gates.iter().any(|((connection_id, kind), current)| {
6686 connection_id == &key.0
6687 && *kind != key.1
6688 && current.generation == generation
6689 && current.policy_epoch == policy_epoch
6690 && current.peer_policy_epoch == peer_policy_epoch
6691 }) {
6692 return false;
6693 }
6694 if let Some(current) = gates.get(&key) {
6695 if current.policy_epoch == policy_epoch && current.peer_policy_epoch == peer_policy_epoch {
6696 if !replace_existing && current.generation == generation {
6697 return false;
6698 }
6699 if current.upgrade_id == upgrade_id {
6700 return replace_existing;
6701 }
6702 }
6703 }
6704 gates.insert(
6705 key,
6706 crate::client::NativeTransportUpgradeGate {
6707 upgrade_id: upgrade_id.to_string(),
6708 generation,
6709 policy_epoch,
6710 peer_policy_epoch,
6711 },
6712 );
6713 true
6714}
6715
6716#[cfg(not(target_arch = "wasm32"))]
6717async fn remove_native_transport_upgrade_gate(
6718 gates: &std::sync::Arc<
6719 tokio::sync::Mutex<
6720 std::collections::HashMap<
6721 (String, IrohPathKind),
6722 crate::client::NativeTransportUpgradeGate,
6723 >,
6724 >,
6725 >,
6726 key: &(String, IrohPathKind),
6727 upgrade_id: &str,
6728) -> bool {
6729 let mut gates = gates.lock().await;
6730 if gates
6731 .get(key)
6732 .is_some_and(|current| current.upgrade_id == upgrade_id)
6733 {
6734 gates.remove(key);
6735 true
6736 } else {
6737 false
6738 }
6739}
6740
6741#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6742async fn remove_native_transport_upgrade_gate_for_generation(
6743 gates: &std::sync::Arc<
6744 tokio::sync::Mutex<
6745 std::collections::HashMap<
6746 (String, IrohPathKind),
6747 crate::client::NativeTransportUpgradeGate,
6748 >,
6749 >,
6750 >,
6751 key: &(String, IrohPathKind),
6752 upgrade_id: &str,
6753 generation: crate::client::NativePeerDataGeneration,
6754) -> bool {
6755 let mut gates = gates.lock().await;
6756 if gates
6757 .get(key)
6758 .is_some_and(|current| current.upgrade_id == upgrade_id && current.generation == generation)
6759 {
6760 gates.remove(key);
6761 true
6762 } else {
6763 false
6764 }
6765}
6766
6767#[cfg(test)]
6768mod tests {
6769 #[cfg(not(target_arch = "wasm32"))]
6770 use super::effective_typescript_transport_capability;
6771 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
6772 use crate::Client;
6773
6774 #[cfg(all(
6775 not(target_arch = "wasm32"),
6776 any(feature = "transport-webrtc", feature = "transport-moq")
6777 ))]
6778 #[tokio::test]
6779 async fn carrier_policy_epoch_changes_only_with_effective_config() {
6780 let client = Client::new_with_app_tag(
6781 crate::test_constants::TEST_PROJECT_ID.to_string(),
6782 "test-app".to_string(),
6783 Box::new(|| None),
6784 );
6785 let initial = client.current_iroh_carrier_policy_epoch();
6786 let mut config = client.transport_config().await;
6787
6788 client
6789 .update_transport_config(config.clone())
6790 .await
6791 .expect("idempotent carrier policy update");
6792 assert_eq!(client.current_iroh_carrier_policy_epoch(), initial);
6793
6794 config.optimize_for = crate::route_policy::RouteOptimization::LowestLatency;
6795 client
6796 .update_transport_config(config)
6797 .await
6798 .expect("changed carrier policy update");
6799 assert_eq!(
6800 client.current_iroh_carrier_policy_epoch(),
6801 initial.saturating_add(1),
6802 );
6803 }
6804
6805 #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
6806 #[tokio::test]
6807 async fn capability_withdrawal_is_serialized_with_the_native_proof_fence() {
6808 let client = Client::new_with_app_tag(
6809 crate::test_constants::TEST_PROJECT_ID.to_string(),
6810 "test-app".to_string(),
6811 Box::new(|| None),
6812 );
6813 let generation = crate::client::NativePeerDataGeneration {
6814 transport_stable_id: 7,
6815 transport_generation: 11,
6816 route_generation: 13,
6817 };
6818 let policy_epoch = client.current_iroh_carrier_policy_epoch();
6819 let peer_epoch = client.current_iroh_carrier_peer_policy_epoch("peer-a");
6820 {
6821 let mut capabilities = client.native_peer_transport_capabilities.write().await;
6822 capabilities.insert(
6823 "peer-a".to_string(),
6824 [crate::client::NativePeerTransportCapability::Moq]
6825 .into_iter()
6826 .collect(),
6827 );
6828 capabilities.insert(
6829 "peer-b".to_string(),
6830 [crate::client::NativePeerTransportCapability::Moq]
6831 .into_iter()
6832 .collect(),
6833 );
6834 }
6835 {
6836 let mut gates = client.native_transport_upgrade_gates.lock().await;
6837 for (connection_id, upgrade_id) in [("peer-a", "attempt-a"), ("peer-b", "attempt-b")] {
6838 gates.insert(
6839 (connection_id.to_string(), crate::client::IrohPathKind::Moq),
6840 crate::client::NativeTransportUpgradeGate {
6841 upgrade_id: upgrade_id.to_string(),
6842 generation,
6843 policy_epoch,
6844 peer_policy_epoch: peer_epoch,
6845 },
6846 );
6847 }
6848 }
6849
6850 let held_gate = client.native_transport_upgrade_gates.lock().await;
6851 let updating = client.clone();
6852 let update = tokio::spawn(async move {
6853 updating
6854 .maybe_handle_typescript_handshake_capabilities(
6855 "peer-a",
6856 None,
6857 None,
6858 &crate::native_protocol::TypeScriptHandshake {
6859 action: Some("capability-update".to_string()),
6860 session_token: None,
6861 session_token_payload: None,
6862 claimed_device_id: None,
6863 capabilities: Some(crate::native_protocol::HandshakeFeatures {
6864 carrier_schema: Some(2),
6865 webrtc: Some(false),
6866 moq: Some(false),
6867 dedicated_carrier_control: Some(false),
6868 dedicated_carrier_control_ready: Some(false),
6869 ble: Some(false),
6870 application_key_agreement: Some(false),
6871 }),
6872 application_key_agreement_public_key: None,
6873 },
6874 )
6875 .await;
6876 });
6877 tokio::task::yield_now().await;
6878 assert!(
6879 client
6880 .native_peer_transport_capabilities
6881 .read()
6882 .await
6883 .get("peer-a")
6884 .is_some_and(|capabilities| capabilities
6885 .contains(&crate::client::NativePeerTransportCapability::Moq)),
6886 "capability mutation must wait behind the proof/commit fence",
6887 );
6888 drop(held_gate);
6889 update.await.expect("capability update task");
6890
6891 assert!(
6892 !client
6893 .native_iroh_carrier_upgrade_is_current(
6894 "peer-a",
6895 crate::client::IrohPathKind::Moq,
6896 "attempt-a",
6897 policy_epoch,
6898 )
6899 .await
6900 );
6901 assert!(
6902 client
6903 .native_iroh_carrier_upgrade_is_current(
6904 "peer-b",
6905 crate::client::IrohPathKind::Moq,
6906 "attempt-b",
6907 policy_epoch,
6908 )
6909 .await
6910 );
6911 }
6912
6913 #[cfg(not(target_arch = "wasm32"))]
6914 use super::{
6915 application_key_reply_uses_fresh_signal, classify_native_ble_switch_admission,
6916 frame_iroh_peer_application_payload, native_ble_attempt_state_after_recovery_wake,
6917 native_ble_retry_delay, native_control_stream_rank, native_transport_upgrade_gate_matches,
6918 remove_native_transport_upgrade_gate, reserve_native_ble_attempt_state,
6919 reserve_native_transport_upgrade_gate, should_accept_native_ble_switch_request,
6920 should_drain_replaced_native_control_frame, should_initiate_native_ble_upgrade,
6921 should_replace_native_control_stream, should_send_typescript_capability_update,
6922 transition_native_ble_upgrade_gate_to_completion, typescript_application_key_reply_action,
6923 NativeBleSwitchAdmission, NativeControlStreamOwner,
6924 MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES, NATIVE_BLE_COMPLETION_GATE_PREFIX,
6925 NATIVE_BLE_MAX_UPGRADE_ATTEMPTS,
6926 };
6927
6928 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
6929 use super::{moq_retry_attempt, native_moq_peer_failure_code};
6930
6931 #[cfg(all(
6932 not(target_arch = "wasm32"),
6933 any(feature = "transport-webrtc", feature = "transport-moq")
6934 ))]
6935 use super::native_carrier_duplicate_request_is_live;
6936
6937 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6938 use super::{
6939 dedicated_carrier_control_is_ready, native_webrtc_duplicate_bootstrap_is_reserved_or_live,
6940 native_webrtc_duplicate_bootstrap_matches_live_attempt,
6941 remove_native_transport_upgrade_gate_for_generation, webrtc_retry_attempt,
6942 };
6943
6944 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6945 #[test]
6946 fn native_dedicated_control_readiness_requires_current_admission_route() {
6947 assert!(!dedicated_carrier_control_is_ready(true, true, false));
6948 assert!(!dedicated_carrier_control_is_ready(true, false, true));
6949 assert!(dedicated_carrier_control_is_ready(true, true, true));
6950 assert!(!dedicated_carrier_control_is_ready(false, true, true));
6951 }
6952
6953 #[cfg(all(
6954 not(target_arch = "wasm32"),
6955 any(feature = "transport-webrtc", feature = "transport-moq")
6956 ))]
6957 #[test]
6958 fn exact_live_native_carrier_replay_survives_base_generation_replacement() {
6959 let request = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request_with_ids(
6960 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
6961 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
6962 transport_stable_id: 3,
6963 transport_generation: 2,
6964 route_generation: 0,
6965 },
6966 1,
6967 [7; 16],
6968 [8; 16],
6969 );
6970 let mut competing = request.clone();
6971 competing.upgrade_id = "09".repeat(16);
6972
6973 assert!(native_carrier_duplicate_request_is_live(
6974 &request, &request, false,
6975 ));
6976 assert!(!native_carrier_duplicate_request_is_live(
6977 &request, &request, true,
6978 ));
6979 assert!(!native_carrier_duplicate_request_is_live(
6980 &competing, &request, false,
6981 ));
6982 }
6983
6984 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6985 #[tokio::test]
6986 async fn repeated_typescript_application_key_offer_is_not_reciprocated_twice() {
6987 let client = Client::new_with_app_tag(
6988 crate::test_constants::TEST_PROJECT_ID.to_string(),
6989 "test-app".to_string(),
6990 Box::new(|| None),
6991 );
6992 *client.node_id.write().await = Some("local-node".to_string());
6993 let remote_agreement =
6994 crate::key_agreement::KeyAgreement::generate().expect("remote agreement");
6995 let handshake = crate::native_protocol::TypeScriptHandshake {
6996 action: Some("capability-update".to_string()),
6997 session_token: None,
6998 session_token_payload: None,
6999 claimed_device_id: None,
7000 capabilities: Some(crate::native_protocol::HandshakeFeatures {
7001 carrier_schema: Some(2),
7002 webrtc: Some(true),
7003 moq: Some(false),
7004 dedicated_carrier_control: Some(true),
7005 dedicated_carrier_control_ready: Some(false),
7006 ble: Some(false),
7007 application_key_agreement: Some(true),
7008 }),
7009 application_key_agreement_public_key: Some(remote_agreement.public_key_bytes()),
7010 };
7011
7012 assert!(client
7013 .maybe_negotiate_typescript_application_crypto(
7014 "connection-1",
7015 Some("remote-node"),
7016 None,
7017 &handshake,
7018 )
7019 .await
7020 .is_some());
7021 assert!(client
7022 .maybe_negotiate_typescript_application_crypto(
7023 "connection-1",
7024 Some("remote-node"),
7025 None,
7026 &handshake,
7027 )
7028 .await
7029 .is_none());
7030 assert!(client.connection_requires_application_crypto_confirmation("connection-1"));
7031 assert!(!client.connection_application_crypto_is_confirmed("connection-1", None));
7032 assert_eq!(
7033 client.session_admission_block_reason("connection-1"),
7034 Some((false, "application-crypto-confirmation-pending".to_string())),
7035 );
7036
7037 let mut response = handshake;
7042 response.action = Some("response".to_string());
7043 assert!(client
7044 .maybe_negotiate_typescript_application_crypto(
7045 "connection-1",
7046 Some("remote-node"),
7047 None,
7048 &response,
7049 )
7050 .await
7051 .is_none());
7052 assert!(!client.connection_application_crypto_is_confirmed("connection-1", None));
7053 assert_eq!(
7054 client.session_admission_block_reason("connection-1"),
7055 Some((false, "application-crypto-confirmation-pending".to_string())),
7056 );
7057 }
7058
7059 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
7060 #[tokio::test]
7061 async fn native_webrtc_terminal_cleanup_retires_only_its_exact_upgrade_gate() {
7062 let generation = crate::client::NativePeerDataGeneration {
7063 transport_stable_id: 10,
7064 transport_generation: 2,
7065 route_generation: 1,
7066 };
7067 let key = (
7068 "connection-1".to_string(),
7069 crate::client::IrohPathKind::WebRtc,
7070 );
7071 let gates =
7072 std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::from([
7073 (
7074 key.clone(),
7075 crate::client::NativeTransportUpgradeGate {
7076 upgrade_id: "terminal-attempt".to_string(),
7077 generation,
7078 policy_epoch: 0,
7079 peer_policy_epoch: 0,
7080 },
7081 ),
7082 ])));
7083
7084 assert!(remove_native_transport_upgrade_gate(&gates, &key, "terminal-attempt").await);
7085 assert!(gates.lock().await.is_empty());
7086
7087 gates.lock().await.insert(
7088 key.clone(),
7089 crate::client::NativeTransportUpgradeGate {
7090 upgrade_id: "replacement-attempt".to_string(),
7091 generation,
7092 policy_epoch: 0,
7093 peer_policy_epoch: 0,
7094 },
7095 );
7096 assert!(
7097 !remove_native_transport_upgrade_gate(&gates, &key, "terminal-attempt").await,
7098 "a delayed terminal watcher must not retire a replacement attempt's gate",
7099 );
7100 assert_eq!(
7101 gates
7102 .lock()
7103 .await
7104 .get(&key)
7105 .map(|gate| gate.upgrade_id.as_str()),
7106 Some("replacement-attempt"),
7107 );
7108
7109 let next_generation = crate::client::NativePeerDataGeneration {
7110 transport_stable_id: 11,
7111 transport_generation: 3,
7112 route_generation: 2,
7113 };
7114 assert!(
7115 !remove_native_transport_upgrade_gate_for_generation(
7116 &gates,
7117 &key,
7118 "replacement-attempt",
7119 next_generation,
7120 )
7121 .await
7122 );
7123 assert!(
7124 remove_native_transport_upgrade_gate_for_generation(
7125 &gates,
7126 &key,
7127 "replacement-attempt",
7128 generation,
7129 )
7130 .await
7131 );
7132 assert!(gates.lock().await.is_empty());
7133 }
7134
7135 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
7136 #[test]
7137 fn native_webrtc_duplicate_bootstrap_requires_live_same_base_generation() {
7138 let base = crate::client::NativePeerDataGeneration {
7139 transport_stable_id: 10,
7140 transport_generation: 2,
7141 route_generation: 1,
7142 };
7143 let replacement = crate::client::NativePeerDataGeneration {
7144 transport_stable_id: 11,
7145 transport_generation: 3,
7146 route_generation: 0,
7147 };
7148 let gate = crate::client::NativeTransportUpgradeGate {
7149 upgrade_id: "upgrade-1".to_string(),
7150 generation: base,
7151 policy_epoch: 0,
7152 peer_policy_epoch: 0,
7153 };
7154
7155 assert!(native_webrtc_duplicate_bootstrap_is_reserved_or_live(
7156 &gate,
7157 "upgrade-1",
7158 base,
7159 None,
7160 ));
7161
7162 assert!(native_webrtc_duplicate_bootstrap_matches_live_attempt(
7163 &gate,
7164 "upgrade-1",
7165 base,
7166 "upgrade-1",
7167 base,
7168 crate::transport::NativeWebRTCState::Connecting,
7169 ));
7170 assert!(!native_webrtc_duplicate_bootstrap_matches_live_attempt(
7171 &gate,
7172 "upgrade-1",
7173 replacement,
7174 "upgrade-1",
7175 base,
7176 crate::transport::NativeWebRTCState::Connected,
7177 ));
7178 assert!(!native_webrtc_duplicate_bootstrap_matches_live_attempt(
7179 &gate,
7180 "upgrade-1",
7181 base,
7182 "upgrade-1",
7183 base,
7184 crate::transport::NativeWebRTCState::Closed,
7185 ));
7186 assert!(!native_webrtc_duplicate_bootstrap_matches_live_attempt(
7187 &gate,
7188 "upgrade-1",
7189 base,
7190 "other-upgrade",
7191 base,
7192 crate::transport::NativeWebRTCState::Connected,
7193 ));
7194 }
7195
7196 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
7197 #[test]
7198 fn webrtc_carrier_retries_one_transient_candidate_failure() {
7199 for failure_code in [
7200 "carrier-base-generation-stale",
7201 "carrier-start-failed",
7202 "carrier-timeout",
7203 "carrier-proof-failed",
7204 ] {
7205 assert_eq!(webrtc_retry_attempt(failure_code, 1), Some(2));
7206 assert_eq!(webrtc_retry_attempt(failure_code, 2), None);
7207 }
7208 assert_eq!(webrtc_retry_attempt("peer-rejected-carrier", 1), None);
7209 assert_eq!(
7210 webrtc_retry_attempt("carrier-authorization-rejected", 1),
7211 None,
7212 );
7213 }
7214
7215 #[test]
7216 fn sends_typescript_capability_update_for_application_key_without_webrtc() {
7217 assert!(should_send_typescript_capability_update(
7218 false,
7219 false,
7220 false,
7221 true,
7222 Some("hello")
7223 ));
7224 }
7225
7226 #[test]
7227 fn application_key_reply_protocol_is_advertise_response_ack() {
7228 assert_eq!(
7229 typescript_application_key_reply_action(Some("capability-update"), true, true, false,),
7230 Some("response"),
7231 );
7232 assert_eq!(
7233 typescript_application_key_reply_action(Some("hello"), true, true, false),
7234 Some("response"),
7235 );
7236 assert_eq!(
7237 typescript_application_key_reply_action(Some("response"), true, false, false),
7238 Some("ack"),
7239 );
7240 assert_eq!(
7241 typescript_application_key_reply_action(Some("ack"), true, true, false),
7242 Some("capability-update"),
7243 );
7244 assert_eq!(
7245 typescript_application_key_reply_action(Some("ack"), true, false, true),
7246 None,
7247 );
7248 assert_eq!(
7249 typescript_application_key_reply_action(Some("capability-update"), false, false, false,),
7250 None,
7251 );
7252 assert_eq!(
7253 typescript_application_key_reply_action(Some("capability-update"), true, false, true,),
7254 Some("response"),
7255 );
7256 assert_eq!(
7257 typescript_application_key_reply_action(Some("response"), true, false, true),
7258 Some("ack"),
7259 );
7260 }
7261
7262 #[test]
7263 fn application_key_reply_preserves_persistent_native_control_route() {
7264 assert!(!application_key_reply_uses_fresh_signal("response", true));
7265 assert!(!application_key_reply_uses_fresh_signal("ack", true));
7266 assert!(application_key_reply_uses_fresh_signal("response", false));
7267 assert!(application_key_reply_uses_fresh_signal("ack", false));
7268 assert!(!application_key_reply_uses_fresh_signal(
7269 "capability-update",
7270 false,
7271 ));
7272 }
7273
7274 #[cfg(not(target_arch = "wasm32"))]
7275 #[test]
7276 fn key_only_transcript_preserves_last_canonical_transport_capability() {
7277 assert!(effective_typescript_transport_capability(
7278 false, false, true
7279 ));
7280 assert!(!effective_typescript_transport_capability(
7281 false, true, false
7282 ));
7283 assert!(!effective_typescript_transport_capability(
7284 true, false, true
7285 ));
7286 assert!(effective_typescript_transport_capability(true, true, false));
7287 }
7288
7289 #[test]
7290 fn advertises_local_transport_before_remote_capability_is_known() {
7291 assert!(should_send_typescript_capability_update(
7292 true,
7293 false,
7294 false,
7295 false,
7296 Some("hello")
7297 ));
7298 assert!(!should_send_typescript_capability_update(
7299 false,
7300 true,
7301 false,
7302 false,
7303 Some("response")
7304 ));
7305 assert!(should_send_typescript_capability_update(
7306 false, false, true, false, None
7307 ));
7308 assert!(!should_send_typescript_capability_update(
7309 false,
7310 false,
7311 false,
7312 false,
7313 Some("hello")
7314 ));
7315 assert!(!should_send_typescript_capability_update(
7316 false,
7317 false,
7318 true,
7319 false,
7320 Some("capability-update")
7321 ));
7322 assert!(!should_send_typescript_capability_update(
7323 true,
7324 false,
7325 false,
7326 true,
7327 Some("capability-update")
7328 ));
7329 assert!(!should_send_typescript_capability_update(
7330 true,
7331 true,
7332 true,
7333 true,
7334 Some("ack")
7335 ));
7336 }
7337
7338 #[cfg(not(target_arch = "wasm32"))]
7339 #[test]
7340 fn native_ble_upgrade_has_one_initiator_and_preserves_better_paths() {
7341 assert!(should_initiate_native_ble_upgrade(
7342 "node-z",
7343 "node-a",
7344 crate::client::IrohPathKind::Relay,
7345 ));
7346 assert!(should_send_typescript_capability_update(
7347 true,
7348 true,
7349 true,
7350 false,
7351 Some("hello")
7352 ));
7353 assert!(!should_initiate_native_ble_upgrade(
7354 "node-a",
7355 "node-z",
7356 crate::client::IrohPathKind::Relay,
7357 ));
7358 for path in [
7359 crate::client::IrohPathKind::DirectQuic,
7360 crate::client::IrohPathKind::DirectLan,
7361 crate::client::IrohPathKind::Ble,
7362 ] {
7363 assert!(!should_initiate_native_ble_upgrade(
7364 "node-z", "node-a", path,
7365 ));
7366 }
7367 assert!(should_initiate_native_ble_upgrade(
7368 "node-z",
7369 "node-a",
7370 crate::client::IrohPathKind::Unknown,
7371 ));
7372 assert!(should_accept_native_ble_switch_request(
7373 "node-a",
7374 "node-z",
7375 crate::client::IrohPathKind::Relay,
7376 ));
7377 assert!(!should_accept_native_ble_switch_request(
7378 "node-z",
7379 "node-a",
7380 crate::client::IrohPathKind::Relay,
7381 ));
7382 assert!(!should_accept_native_ble_switch_request(
7383 "node-a",
7384 "node-z",
7385 crate::client::IrohPathKind::Ble,
7386 ));
7387 }
7388
7389 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
7390 #[test]
7391 fn moq_carrier_retries_one_stale_base_generation() {
7392 assert_eq!(
7393 moq_retry_attempt("carrier-base-generation-stale", 1),
7394 Some(2),
7395 );
7396 assert_eq!(moq_retry_attempt("carrier-base-generation-stale", 2), None,);
7397 assert_eq!(moq_retry_attempt("carrier-proof-failed", 1), None,);
7398 assert!(crate::iroh_connection_policy::custom_carrier_base_allows(
7399 crate::client::IrohPathKind::Moq,
7400 crate::client::IrohPathKind::Moq,
7401 true,
7402 ));
7403 assert!(!crate::iroh_connection_policy::custom_carrier_base_allows(
7404 crate::client::IrohPathKind::Moq,
7405 crate::client::IrohPathKind::Moq,
7406 false,
7407 ));
7408 }
7409
7410 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
7411 #[test]
7412 fn moq_peer_failure_preserves_proof_code_without_broad_retry() {
7413 let (diagnostic, lifecycle) = native_moq_peer_failure_code(Some("carrier-proof-failed"));
7414 assert_eq!(diagnostic, "carrier-proof-failed");
7415 assert_eq!(lifecycle, "carrier-proof-failed");
7416 assert_eq!(moq_retry_attempt(lifecycle, 1), None);
7417
7418 let (diagnostic, lifecycle) =
7419 native_moq_peer_failure_code(Some("carrier-adapter-specific"));
7420 assert_eq!(diagnostic, "carrier-adapter-specific");
7421 assert_eq!(lifecycle, "peer-rejected-carrier");
7422 }
7423
7424 #[cfg(not(target_arch = "wasm32"))]
7425 #[test]
7426 fn native_ble_switch_waits_for_pending_admission_without_accepting_rejection() {
7427 let pending = (false, "native-main-route-pending".to_string());
7428 let rejected = (true, "session-token-rejected".to_string());
7429
7430 assert_eq!(
7431 classify_native_ble_switch_admission(None),
7432 NativeBleSwitchAdmission::Ready,
7433 );
7434 assert_eq!(
7435 classify_native_ble_switch_admission(Some(&pending)),
7436 NativeBleSwitchAdmission::Wait,
7437 );
7438 assert_eq!(
7439 classify_native_ble_switch_admission(Some(&rejected)),
7440 NativeBleSwitchAdmission::Reject,
7441 );
7442 }
7443
7444 #[cfg(not(target_arch = "wasm32"))]
7445 #[test]
7446 fn native_ble_retry_is_bounded_jittered_and_reset_by_typed_recovery_wakes() {
7447 let generation_one = crate::client::NativePeerDataGeneration {
7448 transport_stable_id: 10,
7449 transport_generation: 2,
7450 route_generation: 1,
7451 };
7452 let generation_two = crate::client::NativePeerDataGeneration {
7453 transport_stable_id: 11,
7454 transport_generation: 3,
7455 route_generation: 0,
7456 };
7457
7458 let (state, attempt) = reserve_native_ble_attempt_state(None, generation_one);
7459 assert_eq!(attempt, Some(1));
7460 assert_eq!(native_ble_retry_delay(1, 0).unwrap().as_millis(), 800);
7461 assert_eq!(native_ble_retry_delay(1, 40).unwrap().as_millis(), 1_200);
7462 let (state, attempt) = reserve_native_ble_attempt_state(Some(state), generation_one);
7463 assert_eq!(attempt, Some(2));
7464 assert_eq!(native_ble_retry_delay(2, 0).unwrap().as_millis(), 1_600);
7465 assert_eq!(native_ble_retry_delay(2, 40).unwrap().as_millis(), 2_400);
7466 let (state, attempt) = reserve_native_ble_attempt_state(Some(state), generation_one);
7467 assert_eq!(attempt, Some(3));
7468 assert_eq!(native_ble_retry_delay(3, 20), None);
7469 let (exhausted, attempt) = reserve_native_ble_attempt_state(Some(state), generation_one);
7470 assert_eq!(attempt, None);
7471 assert_eq!(exhausted.attempts, NATIVE_BLE_MAX_UPGRADE_ATTEMPTS);
7472
7473 assert_eq!(
7474 native_ble_attempt_state_after_recovery_wake(exhausted, true),
7475 Some(exhausted),
7476 );
7477 let reset_after_wake = native_ble_attempt_state_after_recovery_wake(exhausted, false);
7478 let (reset, attempt) = reserve_native_ble_attempt_state(reset_after_wake, generation_one);
7479 assert_eq!(attempt, Some(1));
7480 assert_eq!(reset.generation, generation_one);
7481
7482 let (reset, attempt) = reserve_native_ble_attempt_state(Some(exhausted), generation_two);
7483 assert_eq!(attempt, Some(1));
7484 assert_eq!(reset.generation, generation_two);
7485 }
7486
7487 #[cfg(not(target_arch = "wasm32"))]
7488 #[test]
7489 fn native_carriers_cannot_race_the_same_logical_generation() {
7490 let generation = crate::client::NativePeerDataGeneration {
7491 transport_stable_id: 10,
7492 transport_generation: 2,
7493 route_generation: 1,
7494 };
7495 let connection_id = "connection-1".to_string();
7496 let mut gates = std::collections::HashMap::new();
7497
7498 assert!(reserve_native_transport_upgrade_gate(
7499 &mut gates,
7500 (connection_id.clone(), crate::client::IrohPathKind::Moq),
7501 "moq-attempt",
7502 generation,
7503 7,
7504 0,
7505 false,
7506 ));
7507 assert!(!reserve_native_transport_upgrade_gate(
7508 &mut gates,
7509 (connection_id.clone(), crate::client::IrohPathKind::WebRtc),
7510 "webrtc-attempt",
7511 generation,
7512 7,
7513 0,
7514 false,
7515 ));
7516 assert_eq!(gates.len(), 1);
7517
7518 assert!(reserve_native_transport_upgrade_gate(
7519 &mut gates,
7520 (
7521 "connection-2".to_string(),
7522 crate::client::IrohPathKind::WebRtc,
7523 ),
7524 "unrelated-peer-policy-change",
7525 generation,
7526 7,
7527 1,
7528 false,
7529 ));
7530 assert!(!reserve_native_transport_upgrade_gate(
7531 &mut gates,
7532 (connection_id.clone(), crate::client::IrohPathKind::WebRtc),
7533 "still-fenced-for-peer-one",
7534 generation,
7535 7,
7536 0,
7537 false,
7538 ));
7539
7540 assert!(reserve_native_transport_upgrade_gate(
7541 &mut gates,
7542 (connection_id.clone(), crate::client::IrohPathKind::WebRtc),
7543 "peer-one-capability-changed",
7544 generation,
7545 7,
7546 1,
7547 false,
7548 ));
7549
7550 assert!(reserve_native_transport_upgrade_gate(
7551 &mut gates,
7552 (connection_id, crate::client::IrohPathKind::WebRtc),
7553 "policy-switched-webrtc-attempt",
7554 generation,
7555 8,
7556 0,
7557 false,
7558 ));
7559 assert_eq!(gates.len(), 3);
7560 }
7561
7562 #[cfg(not(target_arch = "wasm32"))]
7563 #[tokio::test]
7564 async fn native_ble_upgrade_gate_cleanup_is_generation_fenced() {
7565 let generation_one = crate::client::NativePeerDataGeneration {
7566 transport_stable_id: 10,
7567 transport_generation: 2,
7568 route_generation: 1,
7569 };
7570 let generation_two = crate::client::NativePeerDataGeneration {
7571 transport_stable_id: 11,
7572 transport_generation: 3,
7573 route_generation: 0,
7574 };
7575 let gates =
7576 std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::from([
7577 (
7578 ("connection-1".to_string(), crate::client::IrohPathKind::Ble),
7579 crate::client::NativeTransportUpgradeGate {
7580 upgrade_id: "upgrade-new".to_string(),
7581 generation: generation_one,
7582 policy_epoch: 0,
7583 peer_policy_epoch: 0,
7584 },
7585 ),
7586 ])));
7587 let key = ("connection-1".to_string(), crate::client::IrohPathKind::Ble);
7588
7589 assert!(!remove_native_transport_upgrade_gate(&gates, &key, "upgrade-old").await);
7590 assert_eq!(
7591 gates
7592 .lock()
7593 .await
7594 .get(&key)
7595 .map(|gate| gate.upgrade_id.as_str()),
7596 Some("upgrade-new")
7597 );
7598
7599 assert!(!reserve_native_transport_upgrade_gate(
7600 &mut *gates.lock().await,
7601 key.clone(),
7602 "same-generation-retry",
7603 generation_one,
7604 0,
7605 0,
7606 false,
7607 ));
7608 assert!(reserve_native_transport_upgrade_gate(
7609 &mut *gates.lock().await,
7610 key.clone(),
7611 "same-generation-remote-retry",
7612 generation_one,
7613 0,
7614 0,
7615 true,
7616 ));
7617 assert!(reserve_native_transport_upgrade_gate(
7618 &mut *gates.lock().await,
7619 key.clone(),
7620 "replacement-generation",
7621 generation_two,
7622 0,
7623 0,
7624 false,
7625 ));
7626 assert_eq!(
7627 gates
7628 .lock()
7629 .await
7630 .get(&key)
7631 .map(|gate| (gate.upgrade_id.as_str(), gate.generation)),
7632 Some(("replacement-generation", generation_two)),
7633 );
7634
7635 assert!(reserve_native_transport_upgrade_gate(
7636 &mut *gates.lock().await,
7637 key.clone(),
7638 "policy-epoch-replacement",
7639 generation_two,
7640 1,
7641 0,
7642 false,
7643 ));
7644 assert_eq!(
7645 gates
7646 .lock()
7647 .await
7648 .get(&key)
7649 .map(|gate| (gate.upgrade_id.as_str(), gate.policy_epoch)),
7650 Some(("policy-epoch-replacement", 1)),
7651 );
7652
7653 assert!(!remove_native_transport_upgrade_gate(&gates, &key, "upgrade-new").await);
7654 assert!(
7655 !remove_native_transport_upgrade_gate(&gates, &key, "replacement-generation").await
7656 );
7657 assert!(
7658 remove_native_transport_upgrade_gate(&gates, &key, "policy-epoch-replacement").await
7659 );
7660 assert!(!gates.lock().await.contains_key(&key));
7661
7662 let completion_gate = format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}upgrade-completing");
7663 gates.lock().await.insert(
7664 key.clone(),
7665 crate::client::NativeTransportUpgradeGate {
7666 upgrade_id: completion_gate.clone(),
7667 generation: generation_two,
7668 policy_epoch: 0,
7669 peer_policy_epoch: 0,
7670 },
7671 );
7672 let completion = gates.lock().await.get(&key).cloned().unwrap();
7673 assert!(native_transport_upgrade_gate_matches(
7674 &completion,
7675 "upgrade-completing",
7676 ));
7677 assert!(
7678 !remove_native_transport_upgrade_gate(&gates, &key, "upgrade-completing").await,
7679 "the response timer must not retire an upgrade after switch-ready",
7680 );
7681 assert!(
7682 remove_native_transport_upgrade_gate(&gates, &key, completion_gate.as_str()).await,
7683 "the completion owner must retire its phase-specific gate",
7684 );
7685 }
7686
7687 #[cfg(not(target_arch = "wasm32"))]
7688 #[test]
7689 fn native_ble_switch_ready_requires_the_exact_echoed_generation() {
7690 let generation = crate::client::NativePeerDataGeneration {
7691 transport_stable_id: 10,
7692 transport_generation: 2,
7693 route_generation: 1,
7694 };
7695 let mut gate = crate::client::NativeTransportUpgradeGate {
7696 upgrade_id: "upgrade-exact".to_string(),
7697 generation,
7698 policy_epoch: 7,
7699 peer_policy_epoch: 0,
7700 };
7701 let mismatched_generation = crate::client::NativePeerDataGeneration {
7702 route_generation: generation.route_generation + 1,
7703 ..generation
7704 };
7705
7706 assert_eq!(
7707 transition_native_ble_upgrade_gate_to_completion(
7708 &mut gate,
7709 "upgrade-exact",
7710 mismatched_generation,
7711 ),
7712 None,
7713 );
7714 assert_eq!(gate.upgrade_id, "upgrade-exact");
7715 assert_eq!(gate.generation, generation);
7716 assert_eq!(gate.policy_epoch, 7);
7717
7718 assert_eq!(
7719 transition_native_ble_upgrade_gate_to_completion(
7720 &mut gate,
7721 "upgrade-exact",
7722 generation,
7723 ),
7724 Some(generation),
7725 );
7726 assert_eq!(
7727 gate.upgrade_id,
7728 format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}upgrade-exact"),
7729 );
7730 assert_eq!(gate.generation, generation);
7731 assert_eq!(gate.policy_epoch, 7);
7732 }
7733
7734 #[cfg(not(target_arch = "wasm32"))]
7735 #[test]
7736 fn native_control_stream_rank_converges_on_same_quic_stream() {
7737 let client_stream =
7738 iroh::endpoint::StreamId::new(iroh::endpoint::Side::Client, iroh::endpoint::Dir::Bi, 0);
7739 let server_stream =
7740 iroh::endpoint::StreamId::new(iroh::endpoint::Side::Server, iroh::endpoint::Dir::Bi, 0);
7741
7742 assert!(
7743 native_control_stream_rank(client_stream) < native_control_stream_rank(server_stream)
7744 );
7745 }
7746
7747 #[cfg(not(target_arch = "wasm32"))]
7748 #[test]
7749 fn native_control_stream_replacement_is_generation_fenced() {
7750 let generation_one = NativeControlStreamOwner {
7751 transport_stable_id: 10,
7752 stream_rank: 4,
7753 };
7754 let same_generation_better_stream = NativeControlStreamOwner {
7755 transport_stable_id: 10,
7756 stream_rank: 2,
7757 };
7758 let same_generation_worse_stream = NativeControlStreamOwner {
7759 transport_stable_id: 10,
7760 stream_rank: 6,
7761 };
7762 let generation_two = NativeControlStreamOwner {
7763 transport_stable_id: 11,
7764 stream_rank: 8,
7765 };
7766
7767 assert!(should_replace_native_control_stream(
7768 generation_one,
7769 same_generation_better_stream,
7770 ));
7771 assert!(!should_replace_native_control_stream(
7772 generation_one,
7773 same_generation_worse_stream,
7774 ));
7775 assert!(should_replace_native_control_stream(
7776 generation_one,
7777 generation_two,
7778 ));
7779 }
7780
7781 #[cfg(not(target_arch = "wasm32"))]
7782 #[test]
7783 fn replaced_native_control_stream_drain_is_strictly_bounded() {
7784 assert!(should_drain_replaced_native_control_frame(0));
7785 assert!(should_drain_replaced_native_control_frame(
7786 MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES - 1
7787 ));
7788 assert!(!should_drain_replaced_native_control_frame(
7789 MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES
7790 ));
7791 }
7792
7793 #[cfg(not(target_arch = "wasm32"))]
7794 #[test]
7795 fn iroh_peer_payload_uses_application_stream_framing_not_native_main() {
7796 let frame = frame_iroh_peer_application_payload(b"hello").unwrap();
7797
7798 assert_eq!(u32::from_be_bytes(frame[..4].try_into().unwrap()), 6);
7799 assert_eq!(frame[4], 0x00);
7800 assert_eq!(&frame[5..], b"hello");
7801 assert_ne!(&frame[5..], b"main");
7802 }
7803}