From a98f8f08a6f8b6836c4e85d51a72200901573295 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 07:51:57 +0500 Subject: [PATCH 1/7] feat(net): qualify optional RTT-driven path selection with bounded refresh --- Cargo.lock | 2 - Cargo.toml | 5 + crates/rds-net/src/backends/iroh.rs | 4 + crates/rds-net/src/backends/iroh/latency.rs | 101 + crates/rds-net/src/config.rs | 7 + crates/rds-net/src/config/tests.rs | 31 + crates/rds-net/src/lib.rs | 15 + crates/rds-net/tests/iroh_selector_refresh.rs | 112 + crates/rds-net/tests/packetization.rs | 1 + docs/architecture.md | 12 + docs/endpoint-configuration.md | 19 + .../rds-latency-path-preference-20261006.md | 23 + vendor/iroh/.cargo_vcs_info.json | 6 + vendor/iroh/Cargo.lock | 5161 +++++++++++++++++ vendor/iroh/Cargo.toml | 539 ++ vendor/iroh/Cargo.toml.orig | 256 + vendor/iroh/DEVELOPMENT.md | 49 + vendor/iroh/LICENSE-BSD3 | 33 + vendor/iroh/RDS-PATCH.md | 14 + vendor/iroh/README.md | 183 + vendor/iroh/build.rs | 10 + vendor/iroh/docs/local_relays.md | 36 + vendor/iroh/docs/relays.md | 9 + vendor/iroh/examples/0rtt.rs | 181 + vendor/iroh/examples/auth-hook.rs | 355 ++ vendor/iroh/examples/connect-unreliable.rs | 95 + vendor/iroh/examples/connect.rs | 101 + vendor/iroh/examples/custom-transport.rs | 205 + vendor/iroh/examples/echo-no-router.rs | 121 + vendor/iroh/examples/echo.rs | 112 + vendor/iroh/examples/home-relay-status.rs | 89 + vendor/iroh/examples/incoming-filter.rs | 76 + vendor/iroh/examples/listen-unreliable.rs | 99 + vendor/iroh/examples/listen.rs | 116 + vendor/iroh/examples/monitor-connections.rs | 159 + vendor/iroh/examples/pq-only-key-exchange.rs | 94 + .../iroh/examples/prefer-pq-key-exchange.rs | 90 + vendor/iroh/examples/remote-info.rs | 447 ++ vendor/iroh/examples/screening-connection.rs | 150 + vendor/iroh/examples/search.rs | 228 + vendor/iroh/examples/transfer.rs | 1372 +++++ vendor/iroh/release.toml | 1 + vendor/iroh/src/address_lookup.rs | 1483 +++++ vendor/iroh/src/address_lookup/dns.rs | 136 + vendor/iroh/src/address_lookup/memory.rs | 306 + vendor/iroh/src/address_lookup/metrics.rs | 48 + vendor/iroh/src/address_lookup/pkarr.rs | 683 +++ vendor/iroh/src/defaults.rs | 156 + vendor/iroh/src/endpoint.rs | 4232 ++++++++++++++ vendor/iroh/src/endpoint/bind.rs | 252 + vendor/iroh/src/endpoint/connection.rs | 1682 ++++++ vendor/iroh/src/endpoint/hooks.rs | 171 + vendor/iroh/src/endpoint/presets.rs | 184 + vendor/iroh/src/endpoint/quic.rs | 717 +++ vendor/iroh/src/lib.rs | 306 + vendor/iroh/src/metrics.rs | 57 + vendor/iroh/src/net_report.rs | 1255 ++++ vendor/iroh/src/net_report/defaults.rs | 39 + vendor/iroh/src/net_report/metrics.rs | 17 + vendor/iroh/src/net_report/options.rs | 120 + vendor/iroh/src/net_report/probes.rs | 266 + vendor/iroh/src/net_report/report.rs | 221 + vendor/iroh/src/net_report/reportgen.rs | 997 ++++ vendor/iroh/src/portmapper.rs | 100 + vendor/iroh/src/protocol.rs | 1132 ++++ vendor/iroh/src/runtime.rs | 135 + vendor/iroh/src/socket.rs | 2867 +++++++++ .../src/socket/biased_rtt_path_selector.rs | 323 ++ vendor/iroh/src/socket/concurrent_read_map.rs | 70 + vendor/iroh/src/socket/mapped_addrs.rs | 376 ++ vendor/iroh/src/socket/metrics.rs | 126 + vendor/iroh/src/socket/remote_map.rs | 661 +++ .../src/socket/remote_map/remote_state.rs | 1635 ++++++ .../remote_map/remote_state/path_state.rs | 691 +++ .../remote_map/remote_state/path_watcher.rs | 556 ++ .../remote_map/remote_state/remote_info.rs | 94 + vendor/iroh/src/socket/transports.rs | 1486 +++++ vendor/iroh/src/socket/transports/custom.rs | 105 + vendor/iroh/src/socket/transports/ip.rs | 548 ++ vendor/iroh/src/socket/transports/relay.rs | 478 ++ .../iroh/src/socket/transports/relay/actor.rs | 2030 +++++++ vendor/iroh/src/test_utils.rs | 576 ++ vendor/iroh/src/test_utils/qlog.rs | 87 + vendor/iroh/src/test_utils/test_transport.rs | 744 +++ vendor/iroh/src/tls.rs | 147 + vendor/iroh/src/tls/misc.rs | 125 + vendor/iroh/src/tls/name.rs | 67 + vendor/iroh/src/tls/resolver.rs | 100 + vendor/iroh/src/tls/verifier.rs | 211 + vendor/iroh/src/util.rs | 71 + vendor/iroh/tests/integration.rs | 154 + vendor/iroh/tests/patchbay.rs | 416 ++ vendor/iroh/tests/patchbay/degrade.rs | 309 + vendor/iroh/tests/patchbay/nat.rs | 174 + vendor/iroh/tests/patchbay/relay.rs | 348 ++ vendor/iroh/tests/patchbay/switch-uplink.rs | 239 + vendor/iroh/tests/patchbay/util.rs | 531 ++ 97 files changed, 41757 insertions(+), 2 deletions(-) create mode 100644 crates/rds-net/src/backends/iroh/latency.rs create mode 100644 crates/rds-net/tests/iroh_selector_refresh.rs create mode 100644 docs/reports/rds-latency-path-preference-20261006.md create mode 100644 vendor/iroh/.cargo_vcs_info.json create mode 100644 vendor/iroh/Cargo.lock create mode 100644 vendor/iroh/Cargo.toml create mode 100644 vendor/iroh/Cargo.toml.orig create mode 100644 vendor/iroh/DEVELOPMENT.md create mode 100644 vendor/iroh/LICENSE-BSD3 create mode 100644 vendor/iroh/RDS-PATCH.md create mode 100644 vendor/iroh/README.md create mode 100644 vendor/iroh/build.rs create mode 100644 vendor/iroh/docs/local_relays.md create mode 100644 vendor/iroh/docs/relays.md create mode 100644 vendor/iroh/examples/0rtt.rs create mode 100644 vendor/iroh/examples/auth-hook.rs create mode 100644 vendor/iroh/examples/connect-unreliable.rs create mode 100644 vendor/iroh/examples/connect.rs create mode 100644 vendor/iroh/examples/custom-transport.rs create mode 100644 vendor/iroh/examples/echo-no-router.rs create mode 100644 vendor/iroh/examples/echo.rs create mode 100644 vendor/iroh/examples/home-relay-status.rs create mode 100644 vendor/iroh/examples/incoming-filter.rs create mode 100644 vendor/iroh/examples/listen-unreliable.rs create mode 100644 vendor/iroh/examples/listen.rs create mode 100644 vendor/iroh/examples/monitor-connections.rs create mode 100644 vendor/iroh/examples/pq-only-key-exchange.rs create mode 100644 vendor/iroh/examples/prefer-pq-key-exchange.rs create mode 100644 vendor/iroh/examples/remote-info.rs create mode 100644 vendor/iroh/examples/screening-connection.rs create mode 100644 vendor/iroh/examples/search.rs create mode 100644 vendor/iroh/examples/transfer.rs create mode 100644 vendor/iroh/release.toml create mode 100644 vendor/iroh/src/address_lookup.rs create mode 100644 vendor/iroh/src/address_lookup/dns.rs create mode 100644 vendor/iroh/src/address_lookup/memory.rs create mode 100644 vendor/iroh/src/address_lookup/metrics.rs create mode 100644 vendor/iroh/src/address_lookup/pkarr.rs create mode 100644 vendor/iroh/src/defaults.rs create mode 100644 vendor/iroh/src/endpoint.rs create mode 100644 vendor/iroh/src/endpoint/bind.rs create mode 100644 vendor/iroh/src/endpoint/connection.rs create mode 100644 vendor/iroh/src/endpoint/hooks.rs create mode 100644 vendor/iroh/src/endpoint/presets.rs create mode 100644 vendor/iroh/src/endpoint/quic.rs create mode 100644 vendor/iroh/src/lib.rs create mode 100644 vendor/iroh/src/metrics.rs create mode 100644 vendor/iroh/src/net_report.rs create mode 100644 vendor/iroh/src/net_report/defaults.rs create mode 100644 vendor/iroh/src/net_report/metrics.rs create mode 100644 vendor/iroh/src/net_report/options.rs create mode 100644 vendor/iroh/src/net_report/probes.rs create mode 100644 vendor/iroh/src/net_report/report.rs create mode 100644 vendor/iroh/src/net_report/reportgen.rs create mode 100644 vendor/iroh/src/portmapper.rs create mode 100644 vendor/iroh/src/protocol.rs create mode 100644 vendor/iroh/src/runtime.rs create mode 100644 vendor/iroh/src/socket.rs create mode 100644 vendor/iroh/src/socket/biased_rtt_path_selector.rs create mode 100644 vendor/iroh/src/socket/concurrent_read_map.rs create mode 100644 vendor/iroh/src/socket/mapped_addrs.rs create mode 100644 vendor/iroh/src/socket/metrics.rs create mode 100644 vendor/iroh/src/socket/remote_map.rs create mode 100644 vendor/iroh/src/socket/remote_map/remote_state.rs create mode 100644 vendor/iroh/src/socket/remote_map/remote_state/path_state.rs create mode 100644 vendor/iroh/src/socket/remote_map/remote_state/path_watcher.rs create mode 100644 vendor/iroh/src/socket/remote_map/remote_state/remote_info.rs create mode 100644 vendor/iroh/src/socket/transports.rs create mode 100644 vendor/iroh/src/socket/transports/custom.rs create mode 100644 vendor/iroh/src/socket/transports/ip.rs create mode 100644 vendor/iroh/src/socket/transports/relay.rs create mode 100644 vendor/iroh/src/socket/transports/relay/actor.rs create mode 100644 vendor/iroh/src/test_utils.rs create mode 100644 vendor/iroh/src/test_utils/qlog.rs create mode 100644 vendor/iroh/src/test_utils/test_transport.rs create mode 100644 vendor/iroh/src/tls.rs create mode 100644 vendor/iroh/src/tls/misc.rs create mode 100644 vendor/iroh/src/tls/name.rs create mode 100644 vendor/iroh/src/tls/resolver.rs create mode 100644 vendor/iroh/src/tls/verifier.rs create mode 100644 vendor/iroh/src/util.rs create mode 100644 vendor/iroh/tests/integration.rs create mode 100644 vendor/iroh/tests/patchbay.rs create mode 100644 vendor/iroh/tests/patchbay/degrade.rs create mode 100644 vendor/iroh/tests/patchbay/nat.rs create mode 100644 vendor/iroh/tests/patchbay/relay.rs create mode 100644 vendor/iroh/tests/patchbay/switch-uplink.rs create mode 100644 vendor/iroh/tests/patchbay/util.rs diff --git a/Cargo.lock b/Cargo.lock index 9aa57e9..364b4a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2096,8 +2096,6 @@ checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "iroh" version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885787b892b5e2507c701f132ecbd45d2bad4bbb75157a19427087c16dadb833" dependencies = [ "backon", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 74e8c38..36c8b6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +exclude = ["vendor/iroh"] resolver = "3" members = [ "crates/rds-agent", @@ -104,3 +105,7 @@ rust_2018_idioms = "warn" [profile.release] lto = "thin" codegen-units = 1 + +# Exact published Iroh1.3 source plus opt-in periodic selector refresh. +[patch.crates-io] +iroh = { path = "vendor/iroh" } diff --git a/crates/rds-net/src/backends/iroh.rs b/crates/rds-net/src/backends/iroh.rs index 4b02ca6..f20cbb6 100644 --- a/crates/rds-net/src/backends/iroh.rs +++ b/crates/rds-net/src/backends/iroh.rs @@ -10,6 +10,7 @@ use std::str::FromStr; use iroh::{Endpoint, RelayMap, RelayMode}; use crate::{EndpointAddr, EndpointConfig, EndpointId, RelayUrl, TransportAddr}; +mod latency; /// Adapter conversions between the owned shared types and iroh-base. /// @@ -115,6 +116,9 @@ pub async fn bind_endpoint(config: EndpointConfig) -> anyhow::Result { crate::Transports::DirectOnly => builder.clear_relay_transports(), crate::Transports::RelayOnly => builder.clear_ip_transports(), }; + if config.path_preference == crate::PathPreference::Latency { + builder = builder.path_selector(std::sync::Arc::new(latency::LatencySelector)); + } // Tuning on top of iroh's multipath-aware defaults: // - BBRv3 remains our default; explicit Cubic selection enables // same-path qualification without changing priorities or windows. diff --git a/crates/rds-net/src/backends/iroh/latency.rs b/crates/rds-net/src/backends/iroh/latency.rs new file mode 100644 index 0000000..bb0e172 --- /dev/null +++ b/crates/rds-net/src/backends/iroh/latency.rs @@ -0,0 +1,101 @@ +//! Rank existing paths by RTT without making a relay permanently secondary. +use std::time::Duration; + +use iroh::endpoint::transports::{PathSelection, PathSelectionContext, PathSelector}; + +const SWITCH_GAIN: Duration = Duration::from_millis(5); + +#[derive(Debug)] +pub(super) struct LatencySelector; + +impl PathSelector for LatencySelector { + fn refresh_interval(&self) -> Option { + Some(Duration::from_secs(1)) + } + + fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection { + let choice = choose(ctx.paths().filter_map(|path| { + let rtt = path.stats()?.rtt; + let current = Some(path.network_path()) == ctx.current(); + Some((path, rtt, current)) + })); + let mut selection = PathSelection::none(); + if let Some(path) = choice { + selection.set(&path); + } + selection + } +} + +fn choose(paths: impl Iterator) -> Option { + let mut best: Option<(T, Duration)> = None; + let mut current: Option = None; + for (path, rtt, selected) in paths { + if selected && current.is_none_or(|old| rtt < old) { + current = Some(rtt); + } + if best.as_ref().is_none_or(|(_, old)| rtt < *old) { + best = Some((path, rtt)); + } + } + let (path, rtt) = best?; + if current.is_none_or(|old| old.saturating_sub(rtt) >= SWITCH_GAIN) { + Some(path) + } else { + // The Iroh selector contract interprets an empty selection as keep current. + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, PartialEq, Clone, Copy)] + enum Route { + Direct, + Relay, + } + + #[test] + fn usable_direct_path_does_not_hide_a_faster_relay() { + let paths = [ + (Route::Direct, Duration::from_millis(170), true), + (Route::Relay, Duration::from_millis(65), false), + ]; + assert_eq!(choose(paths.into_iter()), Some(Route::Relay)); + assert_eq!(choose(paths.into_iter().rev()), Some(Route::Relay)); + } + + #[test] + fn stickiness_ties_missing_current_and_extreme_values_are_explicit() { + let ms = Duration::from_millis; + assert_eq!( + choose([(Route::Direct, ms(69), true), (Route::Relay, ms(65), false)].into_iter()), + None + ); + assert_eq!( + choose([(Route::Direct, ms(70), true), (Route::Relay, ms(65), false)].into_iter()), + Some(Route::Relay) + ); + assert_eq!( + choose([(Route::Direct, ms(65), true), (Route::Relay, ms(65), false)].into_iter()), + None + ); + assert_eq!( + choose([(Route::Relay, ms(65), false)].into_iter()), + Some(Route::Relay) + ); + assert_eq!( + choose( + [ + (Route::Direct, Duration::MAX, true), + (Route::Relay, Duration::MAX, false) + ] + .into_iter() + ), + None + ); + assert_eq!(choose(std::iter::empty::<(Route, Duration, bool)>()), None); + } +} diff --git a/crates/rds-net/src/config.rs b/crates/rds-net/src/config.rs index 3e61fbd..007f06b 100644 --- a/crates/rds-net/src/config.rs +++ b/crates/rds-net/src/config.rs @@ -130,6 +130,8 @@ pub struct EndpointSettings { pub transports: crate::Transports, #[serde(default, skip_serializing_if = "is_adaptive_packetization")] pub packetization: crate::Packetization, + #[serde(default, skip_serializing_if = "is_backend_path_preference")] + pub path_preference: crate::PathPreference, #[serde(default, skip_serializing_if = "is_bbr3")] pub congestion_control: crate::CongestionControl, } @@ -143,6 +145,9 @@ fn is_adaptive_packetization(value: &crate::Packetization) -> bool { fn is_bbr3(value: &crate::CongestionControl) -> bool { *value == crate::CongestionControl::Bbr3 } +fn is_backend_path_preference(value: &crate::PathPreference) -> bool { + *value == crate::PathPreference::BackendDefault +} impl Default for EndpointSettings { fn default() -> Self { @@ -154,6 +159,7 @@ impl Default for EndpointSettings { max_multipath_paths: None, transports: crate::Transports::default(), packetization: crate::Packetization::default(), + path_preference: crate::PathPreference::default(), congestion_control: crate::CongestionControl::default(), } } @@ -256,6 +262,7 @@ impl EndpointSettings { max_multipath_paths: self.max_multipath_paths, transports: self.transports, packetization: self.packetization, + path_preference: self.path_preference, congestion_control: self.congestion_control, discovery: self.backend == Backend::Iroh, ..Default::default() diff --git a/crates/rds-net/src/config/tests.rs b/crates/rds-net/src/config/tests.rs index 85da393..cbd4163 100644 --- a/crates/rds-net/src/config/tests.rs +++ b/crates/rds-net/src/config/tests.rs @@ -1,5 +1,36 @@ use super::*; +#[test] +fn latency_path_preference_is_explicit_and_cannot_expand_transport_scope() { + let legacy = EndpointSettings::from_json(br#"{"schema_version":1}"#).unwrap(); + assert_eq!( + legacy.path_preference, + crate::PathPreference::BackendDefault + ); + assert!( + !serde_json::to_string(&legacy) + .unwrap() + .contains("path_preference") + ); + let latency = EndpointSettings::from_json(br#"{"schema_version":1,"path_preference":"latency","max_multipath_paths":1,"transports":"relay-only","relay":{"mode":"iroh","urls":["http://127.0.0.1:3340"]}}"#).unwrap(); + assert_eq!( + EndpointSettings::from_json(&serde_json::to_vec(&latency).unwrap()).unwrap(), + latency + ); + let lowered = latency + .apply(EndpointOverrides::default()) + .unwrap() + .into_endpoint() + .unwrap(); + assert_eq!(lowered.path_preference, crate::PathPreference::Latency); + assert_eq!(lowered.transports, crate::Transports::RelayOnly); + assert_eq!(lowered.max_multipath_paths, Some(1)); + assert!( + EndpointSettings::from_json(br#"{"schema_version":1,"path_preference":"unknown"}"#) + .is_err() + ); +} + #[test] fn explicit_congestion_selection_preserves_legacy_defaults_and_lowers_exactly() { let legacy = EndpointSettings::from_json(br#"{"schema_version":1}"#).unwrap(); diff --git a/crates/rds-net/src/lib.rs b/crates/rds-net/src/lib.rs index 0581652..aadb0d3 100644 --- a/crates/rds-net/src/lib.rs +++ b/crates/rds-net/src/lib.rs @@ -124,6 +124,18 @@ pub enum Packetization { Conservative, } +/// Preference among already permitted, established transport paths. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PathPreference { + /// Retain the backend's policy (Iroh prefers direct over relay). + #[default] + BackendDefault, + /// Prefer lower measured RTT regardless of direct/relay kind, with stickiness. + /// The owned Noq backend already uses this policy. + Latency, +} + /// Explicit congestion-controller selection for measured path qualification. /// This does not change stream priority, path eligibility or packetization. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -192,6 +204,8 @@ pub struct EndpointConfig { pub transports: Transports, /// Packet-size/offload policy. Default `Adaptive`. pub packetization: Packetization, + /// Path ranking only; cannot expand permitted transport kinds or an explicit pin. + pub path_preference: PathPreference, /// Local controller factory; absent file settings retain BBRv3. pub congestion_control: CongestionControl, /// Owned-relay attachments (`noq` backend only): each entry is a @@ -223,6 +237,7 @@ impl Default for EndpointConfig { observed_address_reports: true, transports: Transports::default(), packetization: Packetization::default(), + path_preference: PathPreference::default(), congestion_control: CongestionControl::default(), #[cfg(feature = "transport-noq")] relay_endpoints: Vec::new(), diff --git a/crates/rds-net/tests/iroh_selector_refresh.rs b/crates/rds-net/tests/iroh_selector_refresh.rs new file mode 100644 index 0000000..2d6757c --- /dev/null +++ b/crates/rds-net/tests/iroh_selector_refresh.rs @@ -0,0 +1,112 @@ +//! Real actor refresh against isolated loopback endpoints; no deployed devices. +use std::sync::{ + Arc, + atomic::{AtomicU64, Ordering}, +}; +use std::time::Duration; + +use iroh::endpoint::transports::{PathSelection, PathSelectionContext, PathSelector}; + +#[derive(Debug)] +struct CountingSelector { + calls: Arc, + interval: Option, +} + +impl PathSelector for CountingSelector { + fn refresh_interval(&self) -> Option { + self.interval + } + fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection { + self.calls.fetch_add(1, Ordering::Relaxed); + let mut choice = PathSelection::none(); + if let Some(path) = ctx.paths().next() { + choice.set(&path); + } + choice + } +} + +async fn exercise(interval: Option) { + let calls = Arc::new(AtomicU64::new(0)); + let a = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal) + .clear_ip_transports() + .bind_addr("127.0.0.1:0".parse::().unwrap()) + .unwrap() + .portmapper_config(iroh::endpoint::PortmapperConfig::Disabled) + .path_selector(Arc::new(CountingSelector { + calls: calls.clone(), + interval, + })) + .alpns(vec![rds_core::ALPN.to_vec()]) + .bind() + .await + .unwrap(); + let b = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal) + .clear_ip_transports() + .bind_addr("127.0.0.1:0".parse::().unwrap()) + .unwrap() + .portmapper_config(iroh::endpoint::PortmapperConfig::Disabled) + .alpns(vec![rds_core::ALPN.to_vec()]) + .bind() + .await + .unwrap(); + let (client, server) = tokio::time::timeout(Duration::from_secs(5), async { + tokio::join!(a.connect(b.addr(), rds_core::ALPN), async { + b.accept().await.unwrap().await + }) + }) + .await + .unwrap(); + let (client, server) = (client.unwrap(), server.unwrap()); + // Let initial path events settle, then inspect the actual live actor. + tokio::time::sleep(Duration::from_millis(350)).await; + let initial = calls.load(Ordering::Relaxed); + assert!(initial > 0); + assert_eq!(client.paths().iter().count(), 1); + if interval.is_some() { + tokio::time::timeout(Duration::from_secs(2), async { + while calls.load(Ordering::Relaxed) < initial + 2 { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("opt-in refresh never called the selector again"); + } else { + tokio::time::sleep(Duration::from_millis(750)).await; + assert_eq!( + calls.load(Ordering::Relaxed), + initial, + "default selector unexpectedly became periodic" + ); + } + // Ordinary bytes still flow while refresh runs, without new connections. + let (mut send, mut recv) = client.open_bi().await.unwrap(); + send.write_all(b"refresh").await.unwrap(); + let (mut reply, mut request) = server.accept_bi().await.unwrap(); + let mut body = [0; 7]; + request.read_exact(&mut body).await.unwrap(); + assert_eq!(&body, b"refresh"); + reply.write_all(b"ok").await.unwrap(); + let mut ack = [0; 2]; + recv.read_exact(&mut ack).await.unwrap(); + assert_eq!(&ack, b"ok"); + tokio::join!(a.close(), b.close()); + let after_close = calls.load(Ordering::Relaxed); + tokio::time::sleep(Duration::from_millis(350)).await; + assert_eq!( + calls.load(Ordering::Relaxed), + after_close, + "closed endpoint retained refresh work" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn custom_refresh_runs_without_topology_change_and_stops_with_endpoint() { + exercise(Some(Duration::from_millis(250))).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn default_custom_selector_remains_topology_only() { + exercise(None).await; +} diff --git a/crates/rds-net/tests/packetization.rs b/crates/rds-net/tests/packetization.rs index fa1a5ae..3165929 100644 --- a/crates/rds-net/tests/packetization.rs +++ b/crates/rds-net/tests/packetization.rs @@ -60,6 +60,7 @@ async fn conservative_transfer(backend: Backend) { discovery: false, bind_addrs: vec!["127.0.0.1:0".parse().unwrap()], max_multipath_paths: Some(1), + path_preference: rds_net::PathPreference::Latency, observed_address_reports: false, ..Default::default() }; diff --git a/docs/architecture.md b/docs/architecture.md index 5045584..11d50ec 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -707,3 +707,15 @@ body into one buffer before one `write_all` operation. The receiver's wire bytes 64 KiB bound, partial-write semantics and error kinds are unchanged. This avoids a separate prefix-only transport admission/wakeup; it does not guarantee one network packet, atomic delivery or cancellation-safe writes. + +### Optional latency path ranking + +EndpointSettings/EndpointConfig PathPreference is lowered in the owning backend. +Iroh latency mode ranks existing candidate RTTs without transport tiers and uses +5ms switching stickiness. A bounded1s optional actor refresh makes the decision +respond to changing RTT while topology is unchanged. The exact published Iroh1.3 +source carries this narrow default-None hook under vendor/iroh, excluded from +the RDS workspace; provenance and licenses are retained. Existing/default and +explicitly pinned selectors keep their original callback behavior. Noq already +owns periodic validated-path ranking and needs no policy rewrite for this mode. +This remains a staged qualification increment, not a new default or native pass. diff --git a/docs/endpoint-configuration.md b/docs/endpoint-configuration.md index 0d733a7..76265a2 100644 --- a/docs/endpoint-configuration.md +++ b/docs/endpoint-configuration.md @@ -39,6 +39,7 @@ configuration (role/service/authority/timeout sections). | `max_multipath_paths` | Optional integer 1–32; absent preserves the backend default | | `transports` | `all` (default), `direct-only`, or `relay-only`; bounds usable path kinds | | `packetization` | `adaptive` (default), or `conservative`: 1200-byte primary QUIC UDP payloads, MTU discovery and GSO disabled | +| `path_preference` | `backend-default` (legacy policy), or `latency`: lower measured RTT across allowed direct/relay paths; explicit single-path pins still win | | `congestion_control` | `bbr3` (default), or `cubic`; explicit local controller selection for measured qualification | Congestion selection is independent of stream priority, path kinds, packetization, @@ -182,3 +183,21 @@ or dial; these service options do not extend the endpoint JSON schema. See `rds ssh` opens an [embedded SSH session](ssh.md) on one authorized TCP stream; its account/host-key/PTY options are independent of endpoint configuration. + +## Measured latency preference + +`path_preference: latency` removes Iroh's unconditional direct-primary/relay- +backup ranking and requests a1s bounded reselection interval. A5ms minimum gain +keeps the current path under small fluctuations. The owned Noq backend already +periodically ranks validated live paths by RTT with the same stickiness. +Transport bounds and max_multipath_paths1 remain authoritative; no OS/VPN route, +identity, authority, congestion-controller or wire setting changes. Defaults +retain backend policy, and old strict binaries reject the new explicit field. + +Published Iroh1.3 invokes its selector on topology changes only. The exact +published source is temporarily retained under vendor/iroh with an opt-in +refresh hook: default selectors remain topology-only; intervals are bounded to +250ms..60s, and periodic refresh does not reapply an unchanged selection. See +the patch/provenance notice. No transport engine or cryptographic behavior is +changed. A low standby RTT does not prove bulk capacity; qualify native latency, +quality and migration under actual mixed load before adoption. diff --git a/docs/reports/rds-latency-path-preference-20261006.md b/docs/reports/rds-latency-path-preference-20261006.md new file mode 100644 index 0000000..01bd72f --- /dev/null +++ b/docs/reports/rds-latency-path-preference-20261006.md @@ -0,0 +1,23 @@ +# Explicit latency path preference — 2026-10-06 + +W1/W10 path-policy increment. Iroh1.3's default selector always favors a usable +direct path over a relay regardless of relative RTT, and selection callbacks +run after topology events rather than continuous RTT changes. This can prevent +a lower-delay permitted path from serving interactive work. + +Endpoint latency preference adds kind-neutral minimum RTT and5ms stickiness, +with opt-in bounded actor refresh. Backend defaults remain; single-path pins +and transport eligibility dominate preference. Noq already periodically ranks +validated paths by RTT. No identity/authentication/wire/OS-route change. + +Exact published Iroh1.3 source and MIT/Apache-2.0 licenses are retained with the +small default-None refresh hook; intervals are clamped250ms..60s and unchanged +selection is not reapplied. The patch is a temporary substrate repair, not a +QUIC/TLS fork or completion of owned-Noq migration. No new third-party package. + +Tests specify faster relay choice while direct remains usable, stickiness/ties/ +unknown current/extreme durations, strict configuration lowering, real actor +refresh without topology changes and termination on endpoint close. Existing +real UDP-proxy packetization tests now select latency with a strict single-path +pin, ensuring that preference cannot escape an impairment route. Qualification +is pending; no deployed device configuration changed from this source yet. diff --git a/vendor/iroh/.cargo_vcs_info.json b/vendor/iroh/.cargo_vcs_info.json new file mode 100644 index 0000000..22d1374 --- /dev/null +++ b/vendor/iroh/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "0072d7d84b233f9e7185eb676f049beaf557ac03" + }, + "path_in_vcs": "iroh" +} \ No newline at end of file diff --git a/vendor/iroh/Cargo.lock b/vendor/iroh/Cargo.lock new file mode 100644 index 0000000..517c4d8 --- /dev/null +++ b/vendor/iroh/Cargo.lock @@ -0,0 +1,5161 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "data-encoding-macro" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" +dependencies = [ + "data-encoding", + "syn 3.0.6", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "serdect", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enum-assoc" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0590c4a94da3372e83493b956755a6e2266830b6e4e3b101afe66e3f39477b91" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.1", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ade96dc9003043bce7c035c85a9df5a858bfb2039c5a2e6fdf00f324f6c551" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.9", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hickory-proto" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "rand", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console", + "portable-atomic", + "tokio", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +dependencies = [ + "serde", +] + +[[package]] +name = "iroh" +version = "1.3.0" +dependencies = [ + "assert_matches", + "axum", + "backon", + "blake3", + "bytes", + "cfg_aliases", + "chrono", + "clap", + "console", + "console_error_panic_hook", + "ctor", + "ctutils", + "data-encoding", + "derive_more", + "ed25519-dalek", + "futures-util", + "getrandom 0.4.3", + "http", + "indicatif", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-tracing-test", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "parse-size", + "patchbay", + "pin-project", + "portable-atomic", + "portmapper", + "postcard", + "pretty_assertions", + "rand", + "rand_chacha", + "reqwest", + "rustc-hash", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "simple-dns", + "smallvec", + "strum", + "testdir", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber", + "url", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wasm-tracing", +] + +[[package]] +name = "iroh-base" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae9092be76c9f5429776ab3394f5a5a78dc19dc2524267e884a5b9781546c3f" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "data-encoding-macro", + "derive_more", + "ed25519-dalek", + "getrandom 0.4.3", + "n0-error", + "rand", + "serde", + "url", + "zeroize", +] + +[[package]] +name = "iroh-dns" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80276761b56e904e26ea71d0863beef0bb8d5ab6b639cbd1338e4b615209e3a1" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more", + "iroh-base", + "n0-dns-resolver", + "n0-error", + "n0-future", + "portable-atomic", + "rand", + "rustls", + "simple-dns", + "strum", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ede55536349842337f7cdc63012ec33dc637945f3259c98a0763aac364cdf09" +dependencies = [ + "http-body-util", + "hyper", + "hyper-util", + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "reqwest", + "rustls", + "rustls-platform-verifier", + "ryu", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "iroh-relay" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee116a8233f84980574bb8d11e4517503080cd2c3605bafca04ee9c35d708bd9" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "clap", + "dashmap", + "data-encoding", + "derive_more", + "getrandom 0.4.3", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "pin-project", + "postcard", + "rand", + "rcgen", + "reloadable-state", + "reqwest", + "rustls", + "rustls-cert-file-reader", + "rustls-cert-reloadable-resolver", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_bytes", + "serde_json", + "sha1", + "simdutf8", + "strum", + "time", + "tokio", + "tokio-rustls", + "tokio-rustls-acme", + "tokio-util", + "tokio-websockets", + "toml", + "tracing", + "tracing-subscriber", + "url", + "webpki-roots", + "ws_stream_wasm", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "n0-dns-resolver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535c99c9a786bee714155d2ee4fc2477405c2c43536e1bc6a51de09a0d1448f7" +dependencies = [ + "derive_more", + "ipconfig", + "jni 0.22.4", + "lru", + "n0-error", + "n0-future", + "ndk-context", + "rand", + "reqwest", + "rustc-hash", + "rustls", + "simple-dns", + "system-configuration", + "tokio", + "tokio-rustls", + "tracing", + "webpki-roots", +] + +[[package]] +name = "n0-error" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c59ea174675ce6bc196878851e5049e11b8b1f9af3ba87e4d6df8d828bb001" +dependencies = [ + "anyhow", + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4412346410d6616f3668c40e53c298e5320c7b856f7a960e5914e442601be79f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-qlog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1c2fc610d1facce51b0bf5d8245b309c91996a933defdf7742c92ac78a58cb" +dependencies = [ + "humantime", + "serde", + "serde_json", + "serde_with", + "strum", +] + +[[package]] +name = "n0-tracing-test" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "274dc19cfda091561b364e4f61b39aa959ade203232f9794884f1911022e8e59" +dependencies = [ + "n0-tracing-test-macro", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "n0-tracing-test-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab691281e87f2453a860e76dde99157f4464df18cb7cb3eb9c3847161ccc4ce1" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "n0-watcher" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc618745ad0b7414b149d0517ad8b5573b2fb4d4e2717add3d2446ce1fdd826" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "netdev" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "096c44b66a9b09b99b4dd36b1c295ff3a492039ccecaf55466bd6ddcdba59968" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.31.0", + "netlink-sys 0.8.8", + "objc2", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netdev" +version = "0.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52c2584961c68f5a6e3356797344061f818c93b8acc9cd6d7e284795e563e" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core 0.9.0", + "netlink-packet-route 0.33.0", + "netlink-sys 0.9.0", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6e2daf9a8c2e21302714706860a0e6be02e03d17294ecc66b354ea7d4059dd" + +[[package]] +name = "netlink-packet-route" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.8.2", +] + +[[package]] +name = "netlink-packet-route" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.8.2", +] + +[[package]] +name = "netlink-packet-route" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d48ffc8b73a506e1ff0878528c72668f2bfbc3d875b6be890a96be5d0d5576" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.9.0", + "zerocopy", +] + +[[package]] +name = "netlink-proto" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93af8261786086024cd5e96e0a991dd65ced07bbf7c233a487bbc96b971d5539" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "log", + "netlink-packet-core 0.8.2", + "netlink-sys 0.8.8", + "thiserror 2.0.20", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99d38e00b420df49e940fe0826e667c3dad82ac68eb4c2bbf754c1c14a83a10" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "netwatch" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39da9cad5f23aa43401f09497d3b280e51b5feba115d9ffbf38ca35d6ff95e96" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "ipnet", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev 0.45.1", + "netdev 0.46.3", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.31.0", + "netlink-proto", + "netlink-sys 0.8.8", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows", + "windows-result", + "wmi", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noq" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b78be567e796cfa74bb9bdc4117790af2d505eb887019cf8803244353eb09d89" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c1e5b6fe668491eca022f745a0a9402585626c73a7b839b3424ace15d6a9c8f" +dependencies = [ + "aes-gcm", + "aws-lc-rs", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.4.3", + "identity-hash", + "lru-slab", + "n0-qlog", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "sorted-index-buffer", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc50afea61aff926e150a36c31f06094608848e114a3fe667d76ec233163b1c" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "papaya" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2442474a9404698c42509b8967f437249dbc7b50493e83020333d3943ec0ae" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "patchbay" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07db6594ac62885d28def27f2385d526f16ba4d2edd096d8806072fa81b757f2" +dependencies = [ + "anyhow", + "chrono", + "derive_more", + "futures", + "hickory-proto", + "ipnet", + "iroh-metrics", + "libc", + "nix 0.31.3", + "rtnetlink", + "serde", + "serde_json", + "strum", + "tokio", + "tokio-util", + "toml", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +dependencies = [ + "serde", +] + +[[package]] +name = "portmapper" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca97242f016e090a25330613bc2382aab65eb22f9149dbe84d8f8e711d49a530" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "n0-future", + "netwatch", + "num_enum", + "rand", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "pem 4.0.0", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reloadable-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc20ac1418988b60072d783c9f68e28a173fb63493c127952f6face3b40c6e0" + +[[package]] +name = "reloadable-state" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3853ef78d45b50f8b989896304a85239539d39b7f866a000e8846b9b72d74ce8" +dependencies = [ + "arc-swap", + "reloadable-core", + "tokio", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rtnetlink" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19f84f710fa2f337617f9bc0400260a94224bde7bae28fd8879f3771ca5784" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.30.0", + "netlink-proto", + "netlink-sys 0.8.8", + "nix 0.30.1", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-cert-file-reader" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb47c2a50fdfdaf95b0ac8b12620fc327da1fd4adbb30d0c56d866b005873ff" +dependencies = [ + "rustls-cert-read", + "rustls-pki-types", + "thiserror 2.0.20", + "tokio", +] + +[[package]] +name = "rustls-cert-read" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd46e8c5ae4de3345c4786a83f99ec7aff287209b9e26fa883c473aeb28f19d5" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-cert-reloadable-resolver" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1baa8a3a1f05eaa9fc55aed4342867f70e5c170ea3bfed1b38c51a4857c0c8" +dependencies = [ + "futures-util", + "reloadable-state", + "rustls", + "rustls-cert-read", + "thiserror 2.0.20", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "serde_core", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6f884fa9a8d48101774bfbd3aeb81e968dd22cffd19a372da69f183db22c1a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "testdir" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d53c48916d4a8bb476f45e3699d9d904477dcd3569117d446f1b870d1e5a576" +dependencies = [ + "anyhow", + "backtrace", + "cargo-platform", + "cargo_metadata", + "fslock", + "sysinfo", + "whoami", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-rustls-acme" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1af8573b15fdad8d66da116198cd8fd8d87ff62a67c1c6c3df7f62da1170793f" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "futures", + "log", + "num-bigint", + "pem 3.0.6", + "proc-macro2", + "rcgen", + "reqwest", + "ring", + "rustls", + "serde", + "serde_json", + "thiserror 2.0.20", + "time", + "tokio", + "tokio-rustls", + "webpki-roots", + "x509-parser", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" +dependencies = [ + "aws-lc-rs", + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.3", + "http", + "httparse", + "rand", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45863ef0bef521c12124eb39d9a38513c47db75f22e503c06beacd40afeb35db" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c89dcab8b516b6b603baca9d550b7282d68fcc7f367e3956cff7ebf406a3f12" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af" + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-tracing" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ab253baf6d3772bbdb37a0966b67d37ab80657ccd1a084b4d7b3de3232375d" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.20", + "windows", + "windows-core", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.20", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/vendor/iroh/Cargo.toml b/vendor/iroh/Cargo.toml new file mode 100644 index 0000000..614ac62 --- /dev/null +++ b/vendor/iroh/Cargo.toml @@ -0,0 +1,539 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.91" +name = "iroh" +version = "1.3.0" +authors = [ + "dignifiedquire ", + "n0 team", +] +build = "build.rs" +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "p2p quic connections dialed by public key" +readme = "README.md" +keywords = [ + "quic", + "networking", + "holepunching", + "p2p", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/n0-computer/iroh" +resolver = "2" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "iroh_docsrs", +] + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "iroh_base::*", + "iroh_dns::*", + "iroh_relay::*", + "iroh_metrics::*", + "n0_error::*", + "n0_watcher::*", + "noq::*", + "noq_proto::*", + "noq_udp::*", + "bytes::*", + "http::*", + "serde_core::*", + "tokio::*", + "url::*", + "rustls::*", + "futures_core::stream::Stream", + "futures_lite::stream::Boxed", +] + +[features] +default = [ + "metrics", + "fast-apple-datapath", + "portmapper", + "tls-ring", +] +fast-apple-datapath = ["noq/fast-apple-datapath"] +metrics = [ + "iroh-metrics/metrics", + "iroh-relay/metrics", +] +platform-verifier = ["iroh-relay/platform-verifier"] +portmapper = ["dep:portmapper"] +qlog = ["noq/qlog"] +test-utils = [ + "iroh-relay/test-utils", + "iroh-relay/server", + "dep:axum", + "dep:simple-dns", +] +tls-aws-lc-rs = [ + "noq/aws-lc-rs", + "iroh-relay/tls-aws-lc-rs", + "iroh-dns/tls-aws-lc-rs", +] +tls-ring = [ + "noq/ring", + "iroh-relay/tls-ring", + "iroh-dns/tls-ring", +] +unstable-custom-transports = [] +unstable-net-report = [] + +[lib] +name = "iroh" +crate-type = [ + "lib", + "cdylib", +] +path = "src/lib.rs" + +[[example]] +name = "0rtt" +path = "examples/0rtt.rs" +required-features = [] + +[[example]] +name = "auth-hook" +path = "examples/auth-hook.rs" + +[[example]] +name = "connect" +path = "examples/connect.rs" +required-features = [] + +[[example]] +name = "connect-unreliable" +path = "examples/connect-unreliable.rs" +required-features = [] + +[[example]] +name = "custom-transport" +path = "examples/custom-transport.rs" +required-features = [ + "test-utils", + "unstable-custom-transports", +] + +[[example]] +name = "echo" +path = "examples/echo.rs" +required-features = [] + +[[example]] +name = "echo-no-router" +path = "examples/echo-no-router.rs" +required-features = [] + +[[example]] +name = "home-relay-status" +path = "examples/home-relay-status.rs" +required-features = [] + +[[example]] +name = "incoming-filter" +path = "examples/incoming-filter.rs" +required-features = [] + +[[example]] +name = "listen" +path = "examples/listen.rs" +required-features = [] + +[[example]] +name = "listen-unreliable" +path = "examples/listen-unreliable.rs" +required-features = [] + +[[example]] +name = "monitor-connections" +path = "examples/monitor-connections.rs" + +[[example]] +name = "pq-only-key-exchange" +path = "examples/pq-only-key-exchange.rs" +required-features = ["tls-aws-lc-rs"] + +[[example]] +name = "prefer-pq-key-exchange" +path = "examples/prefer-pq-key-exchange.rs" +required-features = ["tls-aws-lc-rs"] + +[[example]] +name = "remote-info" +path = "examples/remote-info.rs" + +[[example]] +name = "screening-connection" +path = "examples/screening-connection.rs" + +[[example]] +name = "search" +path = "examples/search.rs" +required-features = [] + +[[example]] +name = "transfer" +path = "examples/transfer.rs" +required-features = [] + +[[test]] +name = "integration" +path = "tests/integration.rs" + +[[test]] +name = "patchbay" +path = "tests/patchbay.rs" + +[dependencies.axum] +version = "0.8" +optional = true + +[dependencies.backon] +version = "1.4" + +[dependencies.blake3] +version = "1.8.3" +default-features = false + +[dependencies.bytes] +version = "1.11" + +[dependencies.ctutils] +version = "0.4.0" +default-features = false + +[dependencies.data-encoding] +version = "2.2" + +[dependencies.derive_more] +version = "2.0.1" +features = [ + "debug", + "display", + "from", + "try_into", + "deref", + "from_str", + "into_iterator", +] + +[dependencies.ed25519-dalek] +version = ">=3.0.0-rc.0,<4.0.0" +features = [ + "serde", + "rand_core", + "zeroize", + "pkcs8", + "pem", +] + +[dependencies.futures-util] +version = "0.3" + +[dependencies.http] +version = "1" + +[dependencies.ipnet] +version = "2" + +[dependencies.iroh-base] +version = "1.3.0" +features = [ + "key", + "relay", +] +default-features = false + +[dependencies.iroh-dns] +version = "1.3.0" + +[dependencies.iroh-metrics] +version = "1.0.2" +default-features = false + +[dependencies.iroh-relay] +version = "1.3.0" +default-features = false + +[dependencies.n0-error] +version = "1.0.0" + +[dependencies.n0-future] +version = "0.3" + +[dependencies.n0-watcher] +version = "1.0.0" + +[dependencies.netwatch] +version = "0.19.3" + +[dependencies.noq] +version = "1.3.0" +features = ["rustls"] +default-features = false + +[dependencies.noq-proto] +version = "1.3.0" +default-features = false + +[dependencies.noq-udp] +version = "1.3.0" +default-features = false + +[dependencies.papaya] +version = "0.2.3" +default-features = false + +[dependencies.pin-project] +version = "1" + +[dependencies.portable-atomic] +version = "1" + +[dependencies.rand] +version = "0.10" + +[dependencies.reqwest] +version = "0.13" +features = [ + "rustls-no-provider", + "stream", +] +default-features = false + +[dependencies.rustc-hash] +version = "2" + +[dependencies.rustls] +version = "0.23.33" +default-features = false + +[dependencies.serde] +version = "1.0.219" +features = [ + "derive", + "rc", +] + +[dependencies.simple-dns] +version = "0.12" +optional = true + +[dependencies.smallvec] +version = "1.11.1" + +[dependencies.strum] +version = "0.28" +features = ["derive"] + +[dependencies.tokio] +version = "1.44.1" +features = [ + "io-util", + "macros", + "sync", + "rt", +] + +[dependencies.tokio-stream] +version = "0.1.15" +features = ["sync"] + +[dependencies.tokio-util] +version = "0.7" +features = [ + "io-util", + "io", + "rt", +] + +[dependencies.tracing] +version = "0.1" + +[dependencies.url] +version = "2.5" +features = ["serde"] + +[dependencies.webpki_types] +version = "1.12" +package = "rustls-pki-types" + +[dev-dependencies.chrono] +version = "0.4.43" + +[dev-dependencies.console_error_panic_hook] +version = "0.1" + +[dev-dependencies.n0-error] +version = "1.0.0" +features = ["anyhow"] + +[dev-dependencies.postcard] +version = "1.1.1" +features = ["use-std"] + +[dev-dependencies.rand_chacha] +version = "0.10" + +[dev-dependencies.simple-dns] +version = "0.12" + +[dev-dependencies.tracing-subscriber] +version = "0.3" +features = ["env-filter"] + +[build-dependencies.cfg_aliases] +version = "0.2.2" + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.getrandom] +version = "0.4" +features = ["wasm_js"] + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.time] +version = "0.3" +features = ["wasm-bindgen"] + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.wasm-bindgen-futures] +version = "0.4" + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dev-dependencies.wasm-bindgen-test] +version = "0.3.62" + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dev-dependencies.wasm-tracing] +version = "2.1.0" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies.noq] +version = "1.3.0" +features = [ + "runtime-tokio", + "rustls", +] +default-features = false + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies.portmapper] +version = "0.19.3" +optional = true +default-features = false + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies.tokio] +version = "1" +features = [ + "io-util", + "macros", + "sync", + "rt", + "net", + "fs", + "io-std", +] + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.assert_matches] +version = "1.5.0" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.axum] +version = "0.8" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.clap] +version = "4" +features = ["derive"] + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.console] +version = "0.16" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.indicatif] +version = "0.18" +features = ["tokio"] + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.iroh-base] +version = "1.3.0" +features = [ + "key", + "relay", +] +default-features = false + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.iroh-relay] +version = "1.3.0" +features = [ + "test-utils", + "server", +] +default-features = false + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.n0-tracing-test] +version = "0.3" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.parse-size] +version = "1.1.0" +features = ["std"] + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.pretty_assertions] +version = "1.4" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.rand_chacha] +version = "0.10" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.serde_json] +version = "1" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.tokio] +version = "1" +features = [ + "io-util", + "sync", + "rt", + "rt-multi-thread", + "net", + "fs", + "macros", + "time", + "test-util", +] + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies.tracing-subscriber] +version = "0.3" +features = ["env-filter"] + +[target.'cfg(target_os = "linux")'.dev-dependencies.ctor] +version = "1.0" + +[target.'cfg(target_os = "linux")'.dev-dependencies.patchbay] +version = "0.7" +features = ["iroh-metrics"] + +[target.'cfg(target_os = "linux")'.dev-dependencies.testdir] +version = "0.10" + +[lints.clippy] +unused-async = "warn" + +[lints.rust] +missing_debug_implementations = "warn" + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = [ + "cfg(iroh_docsrs)", + "cfg(iroh_loom)", + "cfg(skip_patchbay)", +] diff --git a/vendor/iroh/Cargo.toml.orig b/vendor/iroh/Cargo.toml.orig new file mode 100644 index 0000000..0866779 --- /dev/null +++ b/vendor/iroh/Cargo.toml.orig @@ -0,0 +1,256 @@ +[package] +name = "iroh" +version = "1.3.0" +edition = "2024" +readme = "README.md" +description = "p2p quic connections dialed by public key" +license = "MIT OR Apache-2.0" +authors = ["dignifiedquire ", "n0 team"] +repository = "https://github.com/n0-computer/iroh" +keywords = ["quic", "networking", "holepunching", "p2p"] + +# Sadly this also needs to be updated in .github/workflows/ci.yml +rust-version = "1.91" + +[lib] +# We need "cdylib" to actually generate .wasm files when we run with --target=wasm32-unknown-unknown. +# It would be nice if we could make this target-dependent, but we can't (yet): https://github.com/rust-lang/cargo/issues/12260 +crate-type = ["lib", "cdylib"] + +[lints] +workspace = true + +[dependencies] +backon = { version = "1.4" } +blake3 = { version = "1.8.3", default-features = false } +bytes = "1.11" +ctutils = { version = "0.4.0", default-features = false } +data-encoding = "2.2" + +derive_more = { version = "2.0.1", features = ["debug", "display", "from", "try_into", "deref", "from_str", "into_iterator"] } +ed25519-dalek = { version = ">=3.0.0-rc.0,<4.0.0", features = ["serde", "rand_core", "zeroize", "pkcs8", "pem"] } +http = "1" +ipnet = "2" +iroh-base = { version = "1.3.0", default-features = false, features = ["key", "relay"], path = "../iroh-base" } +iroh-dns = { version = "1.3.0", path = "../iroh-dns" } +iroh-relay = { version = "1.3.0", path = "../iroh-relay", default-features = false } +n0-future = "0.3" +n0-error = "1.0.0" +n0-watcher = "1.0.0" +netwatch = "0.19.3" +papaya = { version = "0.2.3", default-features = false } +pin-project = "1" +portable-atomic = "1" +noq = { version = "1.3.0", default-features = false, features = ["rustls"] } +noq-proto = { version = "1.3.0", default-features = false } +noq-udp = { version = "1.3.0", default-features = false } +rand = "0.10" +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "stream"] } +rustc-hash = "2" +rustls = { version = "0.23.33", default-features = false } +serde = { version = "1.0.219", features = ["derive", "rc"] } +simple-dns = { version = "0.12", optional = true } +smallvec = "1.11.1" +strum = { version = "0.28", features = ["derive"] } +tokio = { version = "1.44.1", features = [ + "io-util", + "macros", + "sync", + "rt", +] } +tokio-stream = { version = "0.1.15", features = ["sync"] } +tokio-util = { version = "0.7", features = ["io-util", "io", "rt"] } +tracing = "0.1" +url = { version = "2.5", features = ["serde"] } +webpki_types = { package = "rustls-pki-types", version = "1.12" } + +# metrics +iroh-metrics = { version = "1.0.2", default-features = false } + +futures-util = "0.3" + +# test_utils +axum = { version = "0.8", optional = true } + +# non-wasm-in-browser dependencies +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +portmapper = { version = "0.19.3", optional = true, default-features = false } +noq = { version = "1.3.0", default-features = false, features = ["runtime-tokio", "rustls"] } +tokio = { version = "1", features = [ + "io-util", + "macros", + "sync", + "rt", + "net", + "fs", + "io-std", +] } + +# wasm-in-browser dependencies +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +wasm-bindgen-futures = "0.4" +# we don't use time directly, but need to enable it because x509_parser uses these in browsers and we need to enable some feature flags for that to work +time = { version = "0.3", features = ["wasm-bindgen"] } +getrandom = { version = "0.4", features = ["wasm_js"] } + +# target-common test/dev dependencies +[dev-dependencies] +console_error_panic_hook = "0.1" +n0-error = { version = "1.0.0", features = ["anyhow"] } +postcard = { version = "1.1.1", features = ["use-std"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +rand_chacha = "0.10" +chrono = "0.4.43" +simple-dns = "0.12" + +# *non*-wasm-in-browser test/dev dependencies +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies] +assert_matches = "1.5.0" +axum = { version = "0.8" } +pretty_assertions = "1.4" +rand_chacha = "0.10" +tokio = { version = "1", features = [ + "io-util", + "sync", + "rt", + "rt-multi-thread", + "net", + "fs", + "macros", + "time", + "test-util", +] } +serde_json = "1" +iroh-relay = { version = "1.3.0", path = "../iroh-relay", default-features = false, features = ["test-utils", "server"] } +n0-tracing-test = "0.3" +clap = { version = "4", features = ["derive"] } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", +] } +indicatif = { version = "0.18", features = ["tokio"] } +parse-size = { version = "1.1.0", features = ['std'] } +iroh-base = { version = "1.3.0", default-features = false, features = ["key", "relay"], path = "../iroh-base" } +console = { version = "0.16" } + +# wasm-in-browser test/dev dependencies +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dev-dependencies] +wasm-tracing = "2.1.0" +wasm-bindgen-test = "0.3.62" + +# patchbay netsim test dependencies (linux only) +[target.'cfg(target_os = "linux")'.dev-dependencies] +ctor = "1.0" +patchbay = { version = "0.7", features = ["iroh-metrics"] } +testdir = "0.10" + +[build-dependencies] +cfg_aliases = { version = "0.2.2" } + +[features] +default = ["metrics", "fast-apple-datapath", "portmapper", "tls-ring"] +portmapper = ["dep:portmapper"] +metrics = ["iroh-metrics/metrics", "iroh-relay/metrics"] +test-utils = ["iroh-relay/test-utils", "iroh-relay/server", "dep:axum", "dep:simple-dns"] +# Enables fetching TLS trust anchors from the operating system +platform-verifier = ["iroh-relay/platform-verifier"] +qlog = ["noq/qlog"] +# Use private Apple APIs to send multiple packets in a single syscall. +fast-apple-datapath = ["noq/fast-apple-datapath"] +# Use ring as the crypto backend. +tls-ring = ["noq/ring", "iroh-relay/tls-ring", "iroh-dns/tls-ring"] +# Use aws-lc-rs as the crypto backend, unless `ring` is also enabled. +tls-aws-lc-rs = ["noq/aws-lc-rs", "iroh-relay/tls-aws-lc-rs", "iroh-dns/tls-aws-lc-rs"] +# Unstable: Custom transport API (may change without notice) +unstable-custom-transports = [] +# Unstable: API to access an endpoint's NetReport (may change without notice) +unstable-net-report = [] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "iroh_docsrs"] + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + # workspace crates + "iroh_base::*", + "iroh_dns::*", + "iroh_relay::*", + # crates owned by us that will move to 1.0 as well + "iroh_metrics::*", + "n0_error::*", + "n0_watcher::*", + "noq::*", + "noq_proto::*", + "noq_udp::*", + # 1.0 crates that we deem fine to be part of the public API + "bytes::*", + "http::*", + "serde_core::*", + "tokio::*", + "url::*", + # non-1.0 crates that we decided to accept in the public API + "rustls::*", + "futures_core::stream::Stream", + # only type alias + "futures_lite::stream::Boxed", +] + +[[test]] +name = "integration" +path = "tests/integration.rs" + +[[example]] +name = "listen" +required-features = [] + +[[example]] +name = "connect" +required-features = [] + +[[example]] +name = "custom-transport" +required-features = ["test-utils", "unstable-custom-transports"] + +[[example]] +name = "listen-unreliable" +required-features = [] + +[[example]] +name = "connect-unreliable" +required-features = [] + +[[example]] +name = "search" +required-features = [] + +[[example]] +name = "echo" +required-features = [] + +[[example]] +name = "echo-no-router" +required-features = [] + +[[example]] +name = "transfer" +required-features = [] + +[[example]] +name = "0rtt" +required-features = [] + +[[example]] +name = "incoming-filter" +required-features = [] + +[[example]] +name = "pq-only-key-exchange" +required-features = ["tls-aws-lc-rs"] + +[[example]] +name = "prefer-pq-key-exchange" +required-features = ["tls-aws-lc-rs"] + +[[example]] +name = "home-relay-status" +required-features = [] diff --git a/vendor/iroh/DEVELOPMENT.md b/vendor/iroh/DEVELOPMENT.md new file mode 100644 index 0000000..77d030c --- /dev/null +++ b/vendor/iroh/DEVELOPMENT.md @@ -0,0 +1,49 @@ +# Developing iroh + +## Structured events + +The library uses [tracing] both for logging and for *structured events*. +Events differ from normal logging by convention: + +- The [target] is prefixed with `iroh::_events::`, with `::`-separated names. +- There is **no message**; the unique target indicates the meaning. +- The [fields] carry exclusively structured data. +- The [Level] is always `DEBUG`. + +This lets automated tooling process events through custom subscribers while +still producing distinct output under the default tracing formatters, and makes +them unlikely to conflict with real modules. + +An application can subscribe to the `iroh::_events` target separately. With the +default file logging it is also easy to grep for all events: + +```sh +rg 'events::[a-z_\-:]+' path/to/iroh/logs/iroh.YYYY-MM-DD-NN.log +``` + +When adding events, aim for a high signal-to-noise ratio and design them to be +extracted automatically. To keep them distinct from normal logging, write them +with the `event!()` macro: + +```rust,ignore +event!( + target: "iroh::_events::subject", + Level::DEBUG, + field = value, +); +``` + +## Building documentation + +Building the documentation is only supported with `--all-features`. To also +document the cargo features required for certain APIs, pass the `iroh_docsrs` +cfg to rustdoc, which requires nightly Rust: + +```sh +RUSTDOCFLAGS="--cfg iroh_docsrs" cargo +nightly doc --workspace --no-deps --all-features +``` + +[target]: https://docs.rs/tracing/latest/tracing/struct.Metadata.html#method.target +[fields]: https://docs.rs/tracing/latest/tracing/#recording-fields +[Level]: https://docs.rs/tracing/latest/tracing/struct.Level.html +[tracing]: https://docs.rs/tracing diff --git a/vendor/iroh/LICENSE-BSD3 b/vendor/iroh/LICENSE-BSD3 new file mode 100644 index 0000000..78f54c7 --- /dev/null +++ b/vendor/iroh/LICENSE-BSD3 @@ -0,0 +1,33 @@ +Parts of the code has been derived from tailscale, which is under the following license. +Specifically the following files are most relevant + +- ./src/socket** + +BSD 3-Clause License + +Copyright (c) 2020 Tailscale Inc & AUTHORS. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/iroh/RDS-PATCH.md b/vendor/iroh/RDS-PATCH.md new file mode 100644 index 0000000..8f31d2b --- /dev/null +++ b/vendor/iroh/RDS-PATCH.md @@ -0,0 +1,14 @@ +# Iroh1.3 periodic custom selection patch + +Upstream: published crates.io `iroh`1.3.0, checksum +885787b892b5e2507c701f132ecbd45d2bad4bbb75157a19427087c16dadb833. +Original MIT/Apache-2.0 notices and source retained. No QUIC/TLS/relay wire change. + +The published selector only runs after connection/path topology events. Add one +optional default-None selector refresh interval, bounded250ms..60s. The RDS +latency selector requests1s; ordinary/pinned selectors preserve original events. +Unchanged selections are not reapplied during refresh. The actor already owns +only weak connection references; refresh does not add connection ownership. +No default behavior change for other selectors. This temporary source patch +keeps the working substrate while the owned Noq backend retains its existing +periodic lowest-RTT policy. Upstream qualification and convergence remain work. diff --git a/vendor/iroh/README.md b/vendor/iroh/README.md new file mode 100644 index 0000000..251985f --- /dev/null +++ b/vendor/iroh/README.md @@ -0,0 +1,183 @@ +

+ +iroh + +

+ +

+less net work for networks +

+ +[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.rs/iroh/) +[![Crates.io](https://img.shields.io/crates/v/iroh.svg?style=flat-square)](https://crates.io/crates/iroh) +[![Chat](https://img.shields.io/discord/1161119546170687619?logo=discord&style=flat-square)](https://discord.com/invite/DpmJgtU7cW) +[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@n0computer) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE-MIT) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE-APACHE) + + +
+ +Iroh is a Rust library to establish direct connections between endpoints. +It gives you an API for dialing by public key. You say "connect to that +endpoint", and iroh finds and maintains the best connection for you. + +Under the hood iroh establishes peer-to-peer [QUIC] connections between +endpoints. The fastest route is a direct connection, so iroh tries to +[hole-punch] one whenever it can. If that fails it falls back to using +relay servers. + +Because iroh is built on [QUIC], all connections are end-to-end encrypted and may +carry any number of concurrent streams. Dialing by public key also makes them mutually +authenticated, because each endpoint's public key is its TLS identity. + +## Overview + +An iroh endpoint is created and controlled by the [`Endpoint`]. Each endpoint +has a unique [`SecretKey`], whose public key is the endpoint's identity, the +[`EndpointId`]. Connections are authenticated against this key, which means an +[`EndpointId`] can't be impersonated. + +A connection is usually established with the help of a *relay server*. When an +endpoint is created it connects to the closest relay and designates it as its +*home relay*. Other endpoints reach it first through this relay, then both +sides use QUIC NAT traversal to establish a direct connection. In the rare +cases where a direct connection is not possible, traffic keeps flowing over the +relay. + +Relay servers only forward encrypted packets addressed to Endpoint IDs, they +cannot read any traffic between endpoints. + +Endpoints can also connect directly without a relay, as long as the accepting +endpoint is directly reachable at one of its addresses. + +To discover addressing information for an endpoint, iroh uses +[address lookup services]. With address lookup, you can connect to other +endpoints with only their [`EndpointId`]. Addressing information will then +be resolved on-demand. + +The [`N0` preset] installs the DNS/Pkarr address lookup service, which uses +servers hosted by [n0] to provide global lookup for endpoints. + +## Example + +This is an echo protocol: the accepting side copies back whatever it receives. +The full, commented version is in [`echo.rs`](examples/echo.rs). + +```rust +use iroh::{ + Endpoint, + endpoint::{Connection, presets}, + protocol::{AcceptError, ProtocolHandler, Router}, +}; + +/// Each protocol is identified by its ALPN, exchanged during the handshake. +const ALPN: &[u8] = b"iroh-example/echo/0"; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // The accepting side: bind an endpoint and route the ALPN to a handler. + let endpoint = Endpoint::bind(presets::N0).await?; + let router = Router::builder(endpoint.clone()).accept(ALPN, Echo).spawn(); + endpoint.online().await; + // Get the endpoint's address so that we can connect to it. + let addr = endpoint.addr(); + + // The connecting side: dial the accepting endpoint by its address. + { + let other_endpoint = Endpoint::bind(presets::N0).await?; + + let conn = other_endpoint.connect(addr, ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await?; + send.write_all(b"Hello, world!").await?; + send.finish()?; + let response = recv.read_to_end(1000).await?; + assert_eq!(&response, b"Hello, world!"); + conn.close(0u32.into(), b"bye!"); + + other_endpoint.close().await; + } + + router.shutdown().await?; + Ok(()) +} + +#[derive(Debug, Clone)] +struct Echo; + +impl ProtocolHandler for Echo { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let (mut send, mut recv) = connection.accept_bi().await?; + // Echo bytes back until the sender signals the end of data. + tokio::io::copy(&mut recv, &mut send).await?; + send.finish()?; + // Wait until the other endpoint closes the connection. + connection.closed().await; + Ok(()) + } +} +``` + +More examples live in [`iroh/examples`](examples). Run them with +`cargo run --example NAME`. Details for each are in the file itself. + +## Compose protocols + +Instead of writing your own, you can build on protocols that already exist on +top of iroh: + +- [iroh-blobs] for [BLAKE3]-based content-addressed blob transfer, scaling from + kilobytes to terabytes. +- [iroh-gossip] for publish-subscribe overlay networks that scale down to what + an average phone can handle. +- and many more. + +To use iroh from other languages, see [iroh-ffi]. + +## Development + +For notes on iroh's structured events and how to build the documentation, see +[DEVELOPMENT.md](DEVELOPMENT.md). + +# License + +This project is licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or + https://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +See [CONTRIBUTING.md](https://github.com/n0-computer/iroh/blob/main/CONTRIBUTING.md) +for how to get involved. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + +[QUIC]: https://en.wikipedia.org/wiki/QUIC +[hole-punch]: https://en.wikipedia.org/wiki/Hole_punching_(networking) +[BLAKE3]: https://github.com/BLAKE3-team/BLAKE3 +[`Endpoint`]: https://docs.rs/iroh/latest/iroh/struct.Endpoint.html +[`SecretKey`]: https://docs.rs/iroh/latest/iroh/struct.SecretKey.html +[`EndpointId`]: https://docs.rs/iroh/latest/iroh/struct.EndpointId.html +[address lookup services]: https://docs.rs/iroh/latest/iroh/address_lookup/index.html +[`N0` preset]: https://docs.rs/iroh/latest/iroh/endpoint/presets/struct.N0.html +[iroh-blobs]: https://github.com/n0-computer/iroh-blobs +[iroh-gossip]: https://github.com/n0-computer/iroh-gossip +[iroh-ffi]: https://github.com/n0-computer/iroh-ffi +[n0]: https://n0.computer diff --git a/vendor/iroh/build.rs b/vendor/iroh/build.rs new file mode 100644 index 0000000..3c37ab1 --- /dev/null +++ b/vendor/iroh/build.rs @@ -0,0 +1,10 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + // Setup cfg aliases + cfg_aliases! { + // Convenience aliases + wasm_browser: { all(target_family = "wasm", target_os = "unknown") }, + with_crypto_provider: { any(feature = "tls-ring", feature = "tls-aws-lc-rs") } + } +} diff --git a/vendor/iroh/docs/local_relays.md b/vendor/iroh/docs/local_relays.md new file mode 100644 index 0000000..8373790 --- /dev/null +++ b/vendor/iroh/docs/local_relays.md @@ -0,0 +1,36 @@ +# Using a local iroh-relay + +It's easy to set up a iroh-relay that runs locally on your machine. + +Using cargo: + +```shell +$ cargo run --bin iroh-relay --features="iroh-relay" -- --dev +``` + +This will bind the iroh-relay to `[::]3340` and run it over HTTP. + +To connect to this iroh-relay when doing your normal iroh commands, adjust the iroh configuration file to read: + +```toml +# iroh.config.toml: +[[relays]] +url = "http://localhost:3340" +``` + +If you want to give a specific port for the iroh-relay to bind to, you can create a iroh-relay config file and pass that file in using the `--config_path` flag. You need to retain a `secret_key`, so it is recommended to run `iroh-relay --config-path [PATH]` once to generate a secret key and save it to the config file before doing further edits to the file. + +To change the port you want to listen on, change the port in the `addr` field: + +``` +# iroh-relay.toml + +secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" +addr = "[::]:12345" +hostname = "my.relay.network" +enable_relay = true +``` + +Check [the iroh-relay file's](../src/bin/iroh-relay.rs) `Config` struct for documentation on each configuration field. + +If you change the local iroh-relay server's configuration, however, be sure to adjust the associated fields in your iroh config as well. diff --git a/vendor/iroh/docs/relays.md b/vendor/iroh/docs/relays.md new file mode 100644 index 0000000..cc448d4 --- /dev/null +++ b/vendor/iroh/docs/relays.md @@ -0,0 +1,9 @@ +# Relays + +When an Iroh endpoint starts up, it does a latency test to see which known relay endpoint it is “closest to”. That relay server is considered the Iroh endpoint's home relay server. + +An endpoint may be connected to multiple relay servers, but it will advertise its home relay endpoint as the one best used to hole-punch or relay packets through. + +You do not need to know an endpoint's relay server in order to connect to them directly, if there are no firewalls or NATs between the two endpoints trying to connect. However, to have any hole punching, you must know at least one relay server to which that endpoint is connected. + +We currently run 3 relays. diff --git a/vendor/iroh/examples/0rtt.rs b/vendor/iroh/examples/0rtt.rs new file mode 100644 index 0000000..655f861 --- /dev/null +++ b/vendor/iroh/examples/0rtt.rs @@ -0,0 +1,181 @@ +use std::{env, str::FromStr, time::Instant}; + +use clap::Parser; +use data_encoding::HEXLOWER; +use iroh::{ + EndpointId, SecretKey, + endpoint::{RecvStream, SendStream, ZeroRttStatus, presets}, +}; +use n0_error::{Result, StackResultExt, StdResultExt}; +use n0_future::StreamExt; +use tracing::{info, trace}; + +const PINGPONG_ALPN: &[u8] = b"0rtt-pingpong"; + +#[derive(Parser)] +struct Args { + /// The endpoint id to connect to. If not set, the program will start a server. + endpoint_id: Option, + /// Number of rounds to run. + #[clap(long, default_value = "100")] + rounds: u64, + /// Run without 0-RTT for comparison. + #[clap(long)] + disable_0rtt: bool, +} + +/// Gets a secret key from the IROH_SECRET environment variable or generates a new random one. +/// If the environment variable is set, it must be a valid string representation of a secret key. +pub fn get_or_generate_secret_key() -> Result { + if let Ok(secret) = env::var("IROH_SECRET") { + // Parse the secret key from string + SecretKey::from_str(&secret).context("Invalid secret key format") + } else { + // Generate a new random key + let secret_key = SecretKey::generate(); + println!( + "Generated new secret key: {}", + HEXLOWER.encode(&secret_key.to_bytes()) + ); + println!("To reuse this key, set the IROH_SECRET environment variable to this value"); + Ok(secret_key) + } +} + +/// Do a simple ping-pong with the given connection. +async fn pingpong(send: SendStream, recv: RecvStream, x: u64) -> Result<()> { + ping(send, x).await?; + pong(recv, x).await +} + +async fn ping(mut send: SendStream, x: u64) -> Result<()> { + let data = x.to_be_bytes(); + send.write_all(&data).await.anyerr()?; + send.finish().anyerr() +} + +async fn pong(mut recv: RecvStream, x: u64) -> Result<()> { + let data = x.to_be_bytes(); + let echo = recv.read_to_end(8).await.anyerr()?; + assert!(echo == data); + Ok(()) +} + +async fn connect(args: Args) -> Result<()> { + let remote_id = args.endpoint_id.unwrap(); + let endpoint = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .keylog(true) + .bind() + .await?; + // ensure we have resolved the remote_id before connecting + // so we get a more accurate connection timing + let mut address_lookup_stream = endpoint.address_lookup()?.resolve(remote_id); + let _item = address_lookup_stream + .next() + .await + .context("failed to lookup remote")?; + + let t0 = Instant::now(); + for i in 0..args.rounds { + let t0 = Instant::now(); + let connecting = endpoint + .connect_with_opts(remote_id, PINGPONG_ALPN, Default::default()) + .await?; + let connection = if args.disable_0rtt { + let connection = connecting.await.anyerr()?; + trace!("connecting without 0-RTT"); + let (send, recv) = connection.open_bi().await.anyerr()?; + pingpong(send, recv, i).await?; + connection + } else { + match connecting.into_0rtt() { + Ok(zrtt_connection) => { + trace!("0-RTT possible from our side"); + let (send, recv) = zrtt_connection.open_bi().await.anyerr()?; + // before we get the full handshake, attempt to send 0-RTT data + let zrtt_task = tokio::spawn(ping(send, i)); + match zrtt_connection.handshake_completed().await? { + ZeroRttStatus::Accepted(conn) => { + let _ = zrtt_task.await.anyerr()?; + pong(recv, i).await?; + conn + } + ZeroRttStatus::Rejected(conn) => { + zrtt_task.abort(); + let (send, recv) = conn.open_bi().await.anyerr()?; + pingpong(send, recv, i).await?; + conn + } + } + } + Err(connecting) => { + trace!("0-RTT not possible from our side"); + let conn = connecting.await.anyerr()?; + let (send, recv) = conn.open_bi().await.anyerr()?; + pingpong(send, recv, i).await?; + conn + } + } + }; + connection.close(0u8.into(), b""); + let elapsed = t0.elapsed(); + println!("round {i}: {} us", elapsed.as_micros()); + } + let elapsed = t0.elapsed(); + println!("total time: {} us", elapsed.as_micros()); + println!( + "time per round: {} us", + elapsed.as_micros() / (args.rounds as u128) + ); + Ok(()) +} + +async fn accept(_args: Args) -> Result<()> { + let secret_key = get_or_generate_secret_key()?; + let endpoint = iroh::Endpoint::builder(presets::N0) + .alpns(vec![PINGPONG_ALPN.to_vec()]) + .secret_key(secret_key) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await?; + println!("endpoint id: {}", endpoint.id()); + + let accept = async move { + while let Some(incoming) = endpoint.accept().await { + tokio::spawn(async move { + let accepting = incoming.accept().anyerr()?; + let connection = accepting.into_0rtt(); + let (mut send, mut recv) = connection.accept_bi().await.anyerr()?; + trace!("recv.is_0rtt: {}", recv.is_0rtt()); + let data = recv.read_to_end(8).await.anyerr()?; + trace!("recv: {}", data.len()); + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + connection.closed().await; + n0_error::Ok(()) + }); + } + }; + tokio::select! { + _ = accept => { + info!("accept finished, shutting down"); + }, + _ = tokio::signal::ctrl_c()=> { + info!("Ctrl-C received, shutting down"); + } + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let args = Args::parse(); + if args.endpoint_id.is_some() { + connect(args).await?; + } else { + accept(args).await?; + }; + Ok(()) +} diff --git a/vendor/iroh/examples/auth-hook.rs b/vendor/iroh/examples/auth-hook.rs new file mode 100644 index 0000000..83541c2 --- /dev/null +++ b/vendor/iroh/examples/auth-hook.rs @@ -0,0 +1,355 @@ +//! Implementation of authentication using iroh hooks +//! +//! This implements an auth protocol that works with iroh hooks. +//! It allows to put authentication in front of iroh protocols. The protocols don't need any special support. +//! Authentication is handled prior to establishing the connections, over a separate connection. + +use iroh::{Endpoint, EndpointAddr, endpoint::presets, protocol::Router}; +use n0_error::{Result, StdResultExt}; + +use crate::echo::Echo; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let server_router = accept_side(b"secret!!").await?; + server_router.endpoint().online().await; + let server_addr = server_router.endpoint().addr(); + + println!("-- no --"); + let res = connect_side_no_auth(server_addr.clone()).await; + println!("echo without auth: {:#}", res.unwrap_err()); + + println!("-- wrong --"); + let res = connect_side(server_addr.clone(), b"dunno").await; + println!("echo with wrong auth: {:#}", res.unwrap_err()); + + println!("-- correct --"); + let res = connect_side(server_addr.clone(), b"secret!!").await; + println!("echo with correct auth: {res:?}"); + + server_router.shutdown().await.anyerr()?; + + Ok(()) +} + +async fn connect_side(remote_addr: EndpointAddr, token: &[u8]) -> Result<()> { + let (auth_hook, auth_task) = auth::outgoing(token.to_vec()); + let endpoint = Endpoint::builder(presets::N0) + .hooks(auth_hook) + .bind() + .await?; + let _guard = auth_task.spawn(endpoint.clone()); + Echo::connect(&endpoint, remote_addr, b"hello there!").await +} + +async fn connect_side_no_auth(remote_addr: EndpointAddr) -> Result<()> { + let endpoint = Endpoint::bind(presets::N0).await?; + Echo::connect(&endpoint, remote_addr, b"hello there!").await +} + +async fn accept_side(token: &[u8]) -> Result { + let (auth_hook, auth_protocol) = auth::incoming(token.to_vec()); + let endpoint = Endpoint::builder(presets::N0) + .hooks(auth_hook) + .bind() + .await?; + + let router = Router::builder(endpoint) + .accept(auth::ALPN, auth_protocol) + .accept(echo::ALPN, Echo) + .spawn(); + + Ok(router) +} + +mod echo { + //! A bare-bones protocol with no knowledge of auth whatsoever. + + use iroh::{ + Endpoint, EndpointAddr, + endpoint::Connection, + protocol::{AcceptError, ProtocolHandler}, + }; + use n0_error::{Result, StdResultExt, anyerr}; + + #[derive(Debug, Clone)] + pub struct Echo; + + pub const ALPN: &[u8] = b"iroh-example/echo/0"; + + impl Echo { + pub async fn connect( + endpoint: &Endpoint, + remote: impl Into, + message: &[u8], + ) -> Result<()> { + let conn = endpoint.connect(remote, ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(message).await.anyerr()?; + send.finish().anyerr()?; + let response = recv.read_to_end(1000).await.anyerr()?; + conn.close(0u32.into(), b"bye!"); + if response == message { + Ok(()) + } else { + Err(anyerr!("Received invalid response")) + } + } + } + + impl ProtocolHandler for Echo { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let (mut send, mut recv) = connection.accept_bi().await?; + tokio::io::copy(&mut recv, &mut send).await?; + send.finish()?; + connection.closed().await; + Ok(()) + } + } +} + +mod auth { + //! Authentication hook + + use std::{ + collections::{HashMap, HashSet, hash_map}, + sync::{Arc, Mutex}, + }; + + use iroh::{ + Endpoint, EndpointAddr, EndpointId, + endpoint::{ + AfterHandshakeOutcome, BeforeConnectOutcome, Connection, ConnectionError, EndpointHooks, + }, + protocol::{AcceptError, ProtocolHandler}, + }; + use n0_error::{AnyError, Result, StackResultExt, StdResultExt, anyerr}; + use n0_future::task::AbortOnDropHandle; + use tokio::{ + sync::{mpsc, oneshot}, + task::JoinSet, + }; + use tracing::debug; + + pub const ALPN: &[u8] = b"iroh-example/auth/0"; + + const CLOSE_ACCEPTED: u32 = 1; + const CLOSE_DENIED: u32 = 403; + + /// Outgoing side: Use this if you want to pre-auth outgoing connections. + pub fn outgoing(token: Vec) -> (OutgoingAuthHook, OutgoingAuthTask) { + let (tx, rx) = mpsc::channel(16); + let hook = OutgoingAuthHook { tx }; + let connector = OutgoingAuthTask { + token, + rx, + allowed_remotes: Default::default(), + pending_remotes: Default::default(), + tasks: JoinSet::new(), + }; + (hook, connector) + } + + type AuthResult = Result<(), Arc>; + + /// Hook to mount on the endpoint builder. + #[derive(Debug)] + pub struct OutgoingAuthHook { + tx: mpsc::Sender<(EndpointId, oneshot::Sender)>, + } + + impl OutgoingAuthHook { + async fn authenticate(&self, remote_id: EndpointId) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.tx + .send((remote_id, tx)) + .await + .std_context("authenticator stopped")?; + rx.await + .std_context("authenticator stopped")? + .context("failed to authenticate") + } + } + + impl EndpointHooks for OutgoingAuthHook { + async fn before_connect<'a>( + &'a self, + remote_addr: &'a EndpointAddr, + alpn: &'a [u8], + ) -> BeforeConnectOutcome { + // Don't intercept auth request themsevles + if alpn == ALPN { + BeforeConnectOutcome::Accept + } else { + match self.authenticate(remote_addr.id).await { + Ok(()) => BeforeConnectOutcome::Accept, + Err(err) => { + debug!("authentication denied: {err:#}"); + BeforeConnectOutcome::Reject + } + } + } + } + } + + /// Connector task that initiates pre-auth request. Call [`Self::spawn`] once the endpoint is built. + pub struct OutgoingAuthTask { + token: Vec, + rx: mpsc::Receiver<(EndpointId, oneshot::Sender)>, + allowed_remotes: HashSet, + pending_remotes: HashMap>>, + tasks: JoinSet<(EndpointId, Result<()>)>, + } + + impl OutgoingAuthTask { + pub fn spawn(self, endpoint: Endpoint) -> AbortOnDropHandle<()> { + AbortOnDropHandle::new(tokio::spawn(self.run(endpoint))) + } + + async fn run(mut self, endpoint: Endpoint) { + loop { + tokio::select! { + msg = self.rx.recv() => { + let Some((remote_id, tx)) = msg else { + break; + }; + self.handle_msg(&endpoint, remote_id, tx); + } + Some(res) = self.tasks.join_next(), if !self.tasks.is_empty() => { + let (remote_id, res) = res.expect("connect task panicked"); + let res = res.map_err(Arc::new); + self.handle_task(remote_id, res); + } + } + } + } + + fn handle_msg( + &mut self, + endpoint: &Endpoint, + remote_id: EndpointId, + tx: oneshot::Sender>>, + ) { + if self.allowed_remotes.contains(&remote_id) { + tx.send(Ok(())).ok(); + } else { + match self.pending_remotes.entry(remote_id) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(tx); + } + hash_map::Entry::Vacant(entry) => { + let endpoint = endpoint.clone(); + let token = self.token.clone(); + self.tasks.spawn(async move { + let res = Self::connect(endpoint, remote_id, token).await; + (remote_id, res) + }); + entry.insert(vec![tx]); + } + } + } + } + + fn handle_task(&mut self, remote_id: EndpointId, res: Result<(), Arc>) { + if res.is_ok() { + self.allowed_remotes.insert(remote_id); + } + let senders = self.pending_remotes.remove(&remote_id); + for tx in senders.into_iter().flatten() { + tx.send(res.clone()).ok(); + } + } + + async fn connect(endpoint: Endpoint, remote_id: EndpointId, token: Vec) -> Result<()> { + let conn = endpoint.connect(remote_id, ALPN).await?; + let mut stream = conn.open_uni().await.anyerr()?; + stream.write_all(&token).await.anyerr()?; + stream.finish().anyerr()?; + let reason = conn.closed().await; + if let ConnectionError::ApplicationClosed(code) = &reason + && code.error_code.into_inner() as u32 == CLOSE_ACCEPTED + { + Ok(()) + } else if let ConnectionError::ApplicationClosed(code) = &reason + && code.error_code.into_inner() as u32 == CLOSE_DENIED + { + Err(anyerr!("authentication denied by remote")) + } else { + Err(AnyError::from_std(reason)) + } + } + } + + /// Incoming side: Use this if you want to only accept connections from peers with successful pre-auth requests. + pub fn incoming(token: Vec) -> (IncomingAuthHook, AuthProtocol) { + let allowed_remotes: Arc>> = Default::default(); + let hook = IncomingAuthHook { + allowed_remotes: allowed_remotes.clone(), + }; + let protocol = AuthProtocol { + allowed_remotes, + token, + }; + (hook, protocol) + } + + /// Accept-side auth hook: Mount this onto the endpoint. + /// + /// This will reject incoming connections if the remote did not successfully authenticate before. + #[derive(Debug)] + pub struct IncomingAuthHook { + allowed_remotes: Arc>>, + } + + impl EndpointHooks for IncomingAuthHook { + async fn after_handshake<'a>( + &'a self, + conn: &'a iroh::endpoint::Connection, + ) -> AfterHandshakeOutcome { + if conn.alpn() == ALPN + || self + .allowed_remotes + .lock() + .expect("poisoned") + .contains(&conn.remote_id()) + { + AfterHandshakeOutcome::Accept + } else { + AfterHandshakeOutcome::Reject { + error_code: 403u32.into(), + reason: b"not authenticated".to_vec(), + } + } + } + } + + /// Accept-side auth protocol. Mount this on the router to accept authentication requests. + #[derive(Debug, Clone)] + pub struct AuthProtocol { + token: Vec, + allowed_remotes: Arc>>, + } + + impl ProtocolHandler for AuthProtocol { + /// The `accept` method is called for each incoming connection for our ALPN. + /// + /// The returned future runs on a newly spawned tokio task, so it can run as long as + /// the connection lasts. + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let mut stream = connection.accept_uni().await?; + let token = stream.read_to_end(256).await.anyerr()?; + let remote_id = connection.remote_id(); + if token == self.token { + self.allowed_remotes + .lock() + .expect("poisoned") + .insert(remote_id); + connection.close(CLOSE_ACCEPTED.into(), b"accepted"); + } else { + connection.close(CLOSE_DENIED.into(), b"rejected"); + } + Ok(()) + } + } +} diff --git a/vendor/iroh/examples/connect-unreliable.rs b/vendor/iroh/examples/connect-unreliable.rs new file mode 100644 index 0000000..493755c --- /dev/null +++ b/vendor/iroh/examples/connect-unreliable.rs @@ -0,0 +1,95 @@ +//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect to a remote endpoint and pass bytes using unreliable datagrams. +//! +//! We use the endpoint ID (the PublicKey of the remote endpoint), the direct UDP addresses, and the relay url to achieve a connection. +//! +//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection. +//! +//! Run the `listen-unreliable` example first (`iroh/examples/listen-unreliable.rs`), which will give you instructions on how to run this example to watch two endpoints connect and exchange bytes. +use std::net::SocketAddr; + +use clap::Parser; +use iroh::{Endpoint, EndpointAddr, RelayMode, RelayUrl, SecretKey, endpoint::presets}; +use iroh_base::TransportAddr; +use n0_error::{Result, StdResultExt}; +use tracing::info; + +// An example ALPN that we are using to communicate over the `Endpoint` +const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0"; + +#[derive(Debug, Parser)] +struct Cli { + /// The id of the remote endpoint. + #[clap(long)] + endpoint_id: iroh::EndpointId, + /// The list of direct UDP addresses for the remote endpoint. + #[clap(long, value_parser, num_args = 1.., value_delimiter = ' ')] + addrs: Vec, + /// The url of the relay server the remote endpoint can also be reached at. + #[clap(long)] + relay_url: RelayUrl, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + println!("\nconnect (unreliable) example!\n"); + let args = Cli::parse(); + let secret_key = SecretKey::from_bytes(&rand::random()); + println!("public key: {}", secret_key.public()); + + // Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay protocol and relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers. + let endpoint = Endpoint::builder(presets::N0) + // The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase. + .secret_key(secret_key) + // Set the ALPN protocols this endpoint will accept on incoming connections + .alpns(vec![EXAMPLE_ALPN.to_vec()]) + // `RelayMode::Default` means that we will use the default relay servers to holepunch and relay. + // Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls. + // Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS + // If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code + .relay_mode(RelayMode::Default) + // You can choose an address to bind to, but passing in `None` will bind the socket to a random available port + .bind() + .await?; + + // wait for the endpoint to be online + endpoint.online().await; + + let endpoint_addr = endpoint.addr(); + let me = endpoint_addr.id; + println!("endpoint id: {me}"); + println!("endpoint listening addresses:"); + endpoint_addr + .ip_addrs() + .for_each(|addr| println!("\t{addr}")); + let relay_url = endpoint_addr + .relay_urls() + .next() + .expect("Should have a relay URL, assuming a default endpoint setup."); + println!("endpoint relay server url: {relay_url}\n"); + // Build a `EndpointAddr` from the endpoint_id, relay url, and UDP addresses. + let addrs = args + .addrs + .into_iter() + .map(TransportAddr::Ip) + .chain(std::iter::once(TransportAddr::Relay(args.relay_url))); + + let addr = EndpointAddr::from_parts(args.endpoint_id, addrs); + + // Attempt to connect, over the given ALPN. + // Returns a QUIC connection. + let conn = endpoint.connect(addr, EXAMPLE_ALPN).await?; + info!("connected"); + + // Send a datagram over the connection. + let message = format!("{me} is saying 'hello!'"); + conn.send_datagram(message.as_bytes().to_vec().into()) + .anyerr()?; + + // Read a datagram over the connection. + let message = conn.read_datagram().await.anyerr()?; + let message = String::from_utf8(message.into()).anyerr()?; + println!("received: {message}"); + + Ok(()) +} diff --git a/vendor/iroh/examples/connect.rs b/vendor/iroh/examples/connect.rs new file mode 100644 index 0000000..6f3f2c7 --- /dev/null +++ b/vendor/iroh/examples/connect.rs @@ -0,0 +1,101 @@ +//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect to a remote endpoint. +//! +//! We use the endpoint ID (the PublicKey of the remote endpoint), the direct UDP addresses, and the relay url to achieve a connection. +//! +//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection. +//! +//! Run the `listen` example first (`iroh/examples/listen.rs`), which will give you instructions on how to run this example to watch two endpoints connect and exchange bytes. +use std::net::SocketAddr; + +use clap::Parser; +use iroh::{ + Endpoint, EndpointAddr, RelayMode, RelayUrl, SecretKey, TransportAddr, endpoint::presets, +}; +use n0_error::{Result, StdResultExt}; +use tracing::info; + +// An example ALPN that we are using to communicate over the `Endpoint` +const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0"; + +#[derive(Debug, Parser)] +struct Cli { + /// The id of the remote endpoint. + #[clap(long)] + endpoint_id: iroh::EndpointId, + /// The list of direct UDP addresses for the remote endpoint. + #[clap(long, value_parser, num_args = 1.., value_delimiter = ' ')] + addrs: Vec, + /// The url of the relay server the remote endpoint can also be reached at. + #[clap(long)] + relay_url: RelayUrl, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + println!("\nconnect example!\n"); + let args = Cli::parse(); + let secret_key = SecretKey::generate(); + println!("public key: {}", secret_key.public()); + + // Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay protocol and relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers. + let endpoint = Endpoint::builder(presets::N0) + // The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase. + .secret_key(secret_key) + // Set the ALPN protocols this endpoint will accept on incoming connections + .alpns(vec![EXAMPLE_ALPN.to_vec()]) + // `RelayMode::Default` means that we will use the default relay servers to holepunch and relay. + // Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls. + // Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS + // If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code + .relay_mode(RelayMode::Default) + // You can choose an address to bind to, but passing in `None` will bind the socket to a random available port + .bind() + .await?; + + // wait for the endpoint to be online + endpoint.online().await; + + let endpoint_addr = endpoint.addr(); + let me = endpoint.id(); + println!("endpoint id: {me}"); + println!("endpoint listening addresses:"); + for addr in endpoint_addr.ip_addrs() { + println!("\t{addr}") + } + + let relay_url = endpoint_addr + .relay_urls() + .next() + .expect("should be connected to a relay server"); + println!("endpoint relay server url: {relay_url}\n"); + // Build a `EndpointAddr` from the endpoint_id, relay url, and UDP addresses. + let addrs = args + .addrs + .into_iter() + .map(TransportAddr::Ip) + .chain(std::iter::once(TransportAddr::Relay(args.relay_url))); + let addr = EndpointAddr::from_parts(args.endpoint_id, addrs); + + // Attempt to connect, over the given ALPN. + // Returns a Noq connection. + let conn = endpoint.connect(addr, EXAMPLE_ALPN).await?; + info!("connected"); + + // Use the Noq API to send and recv content. + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + + let message = format!("{me} is saying 'hello!'"); + send.write_all(message.as_bytes()).await.anyerr()?; + + // Call `finish` to close the send side of the connection gracefully. + send.finish().anyerr()?; + let message = recv.read_to_end(100).await.anyerr()?; + let message = String::from_utf8(message).anyerr()?; + println!("received: {message}"); + + // We received the last message: close all connections and allow for the close + // message to be sent. + endpoint.close().await; + Ok(()) +} diff --git a/vendor/iroh/examples/custom-transport.rs b/vendor/iroh/examples/custom-transport.rs new file mode 100644 index 0000000..d3210bc --- /dev/null +++ b/vendor/iroh/examples/custom-transport.rs @@ -0,0 +1,205 @@ +use std::{sync::Arc, time::Duration}; + +use clap::Parser; +use iroh::{ + Endpoint, SecretKey, TransportAddr, + endpoint::{ + Builder, Connection, presets, + transports::{Addr, PathSelection, PathSelectionContext, PathSelector}, + }, + protocol::{AcceptError, ProtocolHandler, Router}, + test_utils::test_transport::{TEST_TRANSPORT_ID, TestNetwork, TestTransport}, +}; +use n0_error::{Result, StdResultExt}; + +/// Each protocol is identified by its ALPN string. +/// +/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake, +/// and the connection is aborted unless both endpoints pass the same bytestring. +const ALPN: &[u8] = b"iroh-example/echo/0"; + +/// Example demonstrating custom transport usage. +#[derive(Parser, Debug, Clone)] +struct Args { + /// Keep IP transports enabled (in addition to custom transport) + #[arg(long)] + keep_ip: bool, + + /// Keep relay transports enabled (in addition to custom transport) + #[arg(long)] + keep_relay: bool, + + /// Delay in seconds to wait after connecting before re-checking the selected transport + #[arg(long, default_value = "0")] + delay: u64, +} + +/// A [`PathSelector`] that prefers the test custom transport whenever a candidate path on +/// it exists, falling back to the lowest-RTT candidate otherwise. +#[derive(Debug)] +struct PreferTestTransport; + +impl PathSelector for PreferTestTransport { + fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection { + tracing::debug!("dumping path RTTs"); + for p in ctx.paths() { + let network_path = p.network_path(); + let rtt = p.stats().map(|s| s.rtt); + tracing::debug!(%network_path, ?rtt); + } + let mut selection = PathSelection::none(); + // First preference: any path on our test custom transport. + if let Some(p) = ctx.paths().find( + |p| matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID), + ) { + selection.set(&p); + return selection; + } + // Otherwise: lowest RTT wins. Paths whose stats can't be read (closed + // concurrently with selection) are skipped entirely. + if let Some(p) = ctx + .paths() + .filter_map(|p| p.stats().map(|s| (p, s.rtt))) + .min_by_key(|(_, rtt)| *rtt) + .map(|(p, _)| p) + { + selection.set(&p); + } + selection + } +} + +impl Args { + /// Configure an endpoint builder with the custom transport and optional IP/relay transports. + fn configure(&self, secret_key: SecretKey, transport: Arc) -> Builder { + let mut builder = Endpoint::builder(presets::N0) + .secret_key(secret_key) + .preset(transport) + // Always prefer the custom transport when it has a working path. + .path_selector(Arc::new(PreferTestTransport)); + if !self.keep_ip { + builder = builder.clear_ip_transports(); + } + if !self.keep_relay { + builder = builder.clear_relay_transports(); + } + builder + } +} + +#[derive(Debug, Clone)] +struct Echo; + +impl ProtocolHandler for Echo { + /// The `accept` method is called for each incoming connection for our ALPN. + /// + /// The returned future runs on a newly spawned tokio task, so it can run as long as + /// the connection lasts. + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + // We can get the remote's endpoint id from the connection. + let endpoint_id = connection.remote_id(); + println!("accepted connection from {endpoint_id}"); + + // Our protocol is a simple request-response protocol, so we expect the + // connecting peer to open a single bi-directional stream. + let (mut send, mut recv) = connection.accept_bi().await?; + + // Echo any bytes received back directly. + // This will keep copying until the sender signals the end of data on the stream. + let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; + println!("Copied over {bytes_sent} byte(s)"); + + // By calling `finish` on the send stream we signal that we will not send anything + // further, which makes the receive stream on the other end terminate. + send.finish()?; + + // Wait until the remote closes the connection, which it does once it + // received the response. + connection.closed().await; + + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let args = Args::parse(); + + println!( + "Config: keep_ip={}, keep_relay={}, delay={}s", + args.keep_ip, args.keep_relay, args.delay + ); + + let network = TestNetwork::new(); + let s1 = SecretKey::from([0u8; 32]); + let s2 = SecretKey::from([1u8; 32]); + + // Create transports and configure builders with transport + address lookup + let t1 = network.create_transport(s1.public())?; + let ep1 = args.configure(s1.clone(), t1).bind().await?; + + let t2 = network.create_transport(s2.public())?; + let ep2 = args.configure(s2.clone(), t2).bind().await?; + println!("ep2 addr: {:?}", ep2.addr()); + let server = Router::builder(ep2).accept(ALPN, Echo).spawn(); + + // Connect using just the endpoint ID - discovery will resolve addresses + // Note: The test network's discovery is very fast (in-memory), so the custom + // transport address is available immediately and wins before IP discovery runs. + println!("Connecting to: {:?}", s2.public()); + let conn = ep1.connect(s2.public(), ALPN).await?; + + // Helper to print paths and verify test transport is selected + let verify_test_transport = |label: &str| { + let paths = conn.paths(); + println!("Paths {}:", label); + for path in paths.iter() { + println!( + " {} selected={} rtt={:?}", + path.remote_addr(), + path.is_selected(), + path.rtt() + ); + } + let selected_path = paths.iter().find(|p| p.is_selected()); + let is_test_transport = selected_path.as_ref().is_some_and(|p| { + matches!(p.remote_addr(), TransportAddr::Custom(addr) if addr.id() == TEST_TRANSPORT_ID) + }); + assert!( + is_test_transport, + "Expected test transport (id={}) to be selected {}, got: {:?}", + TEST_TRANSPORT_ID, + label, + selected_path.as_ref().map(|p| p.remote_addr()) + ); + println!( + "Verified: test transport (id={}) is selected {}", + TEST_TRANSPORT_ID, label + ); + }; + + // Verify test transport is selected immediately after connecting + verify_test_transport("immediately after connecting"); + + // If a delay is specified, wait and then re-check to see if the transport is still selected + // after other discovery mechanisms have had time to run. + if args.delay > 0 { + println!( + "Waiting {}s to let other discovery mechanisms run...", + args.delay + ); + tokio::time::sleep(Duration::from_secs(args.delay)).await; + verify_test_transport(&format!("after {}s delay", args.delay)); + } + + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"Hello custom transport!").await.anyerr()?; + send.finish().anyerr()?; + let response = recv.read_to_end(1000).await.anyerr()?; + assert_eq!(&response, b"Hello custom transport!"); + conn.close(0u32.into(), b"bye!"); + server.shutdown().await.anyerr()?; + drop(server); + Ok(()) +} diff --git a/vendor/iroh/examples/echo-no-router.rs b/vendor/iroh/examples/echo-no-router.rs new file mode 100644 index 0000000..c5c9796 --- /dev/null +++ b/vendor/iroh/examples/echo-no-router.rs @@ -0,0 +1,121 @@ +//! Very basic example showing how to implement a basic echo protocol, +//! without using the `Router` API. (For the router version, check out the echo.rs example.) +//! +//! The echo protocol echos any data sent to it in the first stream. +//! +//! ## Running the Example +//! +//! cargo run --example echo-no-router + +use iroh::{Endpoint, EndpointAddr, endpoint::presets}; +use n0_error::{AnyError as Error, Result, StdResultExt}; + +/// Each protocol is identified by its ALPN string. +/// +/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake, +/// and the connection is aborted unless both endpoints pass the same bytestring. +const ALPN: &[u8] = b"iroh-example/echo/0"; + +#[tokio::main] +async fn main() -> Result<()> { + let endpoint = start_accept_side().await?; + + // wait for the endpoint to be online + endpoint.online().await; + + connect_side(endpoint.addr()).await?; + + // This makes sure the endpoint is closed properly and connections close gracefully + // and will indirectly close the tasks spawned by `start_accept_side`. + endpoint.close().await; + + Ok(()) +} + +async fn connect_side(addr: EndpointAddr) -> Result<()> { + let endpoint = Endpoint::bind(presets::N0).await?; + + // Open a connection to the accepting endpoint + let conn = endpoint.connect(addr, ALPN).await?; + + // Open a bidirectional QUIC stream + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + + // Send some data to be echoed + send.write_all(b"Hello, world!").await.anyerr()?; + + // Signal the end of data for this particular stream + send.finish().anyerr()?; + + // Receive the echo, but limit reading up to maximum 1000 bytes + let response = recv.read_to_end(1000).await.anyerr()?; + assert_eq!(&response, b"Hello, world!"); + + // Explicitly close the whole connection. + conn.close(0u32.into(), b"bye!"); + + // The above call only queues a close message to be sent (see how it's not async!). + // We need to actually call this to make sure this message is sent out. + endpoint.close().await; + // If we don't call this, but continue using the endpoint, then the queued + // close call will eventually be picked up and sent. + // But always try to wait for endpoint.close().await to go through before dropping + // the endpoint to ensure any queued messages are sent through and connections are + // closed gracefully. + + Ok(()) +} + +async fn start_accept_side() -> Result { + let endpoint = Endpoint::builder(presets::N0) + // The accept side needs to opt-in to the protocols it accepts, + // as any connection attempts that can't be found with a matching ALPN + // will be rejected. + .alpns(vec![ALPN.to_vec()]) + .bind() + .await?; + + // spawn a task so that `start_accept_side` returns immediately and we can continue in main(). + tokio::spawn({ + let endpoint = endpoint.clone(); + async move { + // This task won't leak, because we call `endpoint.close()` in `main()`, + // which causes `endpoint.accept().await` to return `None`. + // In a more serious environment, we recommend avoiding `tokio::spawn` and use either a `TaskTracker` or + // `JoinSet` instead to make sure you're not accidentally leaking tasks. + while let Some(incoming) = endpoint.accept().await { + // spawn a task for each incoming connection, so we can serve multiple connections asynchronously + tokio::spawn(async move { + let connection = incoming.await.anyerr()?; + + // We can get the remote's endpoint id from the connection. + let endpoint_id = connection.remote_id(); + println!("accepted connection from {endpoint_id}"); + + // Our protocol is a simple request-response protocol, so we expect the + // connecting peer to open a single bi-directional stream. + let (mut send, mut recv) = connection.accept_bi().await.anyerr()?; + + // Echo any bytes received back directly. + // This will keep copying until the sender signals the end of data on the stream. + let bytes_sent = tokio::io::copy(&mut recv, &mut send).await.anyerr()?; + println!("Copied over {bytes_sent} byte(s)"); + + // By calling `finish` on the send stream we signal that we will not send anything + // further, which makes the receive stream on the other end terminate. + send.finish().anyerr()?; + + // Wait until the remote closes the connection, which it does once it + // received the response. + connection.closed().await; + + Ok::<_, Error>(()) + }); + } + + Ok::<_, Error>(()) + } + }); + + Ok(endpoint) +} diff --git a/vendor/iroh/examples/echo.rs b/vendor/iroh/examples/echo.rs new file mode 100644 index 0000000..dd6b2cb --- /dev/null +++ b/vendor/iroh/examples/echo.rs @@ -0,0 +1,112 @@ +//! Very basic example to showcase how to use iroh's APIs. +//! +//! This example implements a simple protocol that echos any data sent to it in the first stream. +//! +//! ## Usage +//! +//! cargo run --example echo + +use iroh::{ + Endpoint, EndpointAddr, + endpoint::{Connection, presets}, + protocol::{AcceptError, ProtocolHandler, Router}, +}; +use n0_error::{Result, StdResultExt}; + +/// Each protocol is identified by its ALPN string. +/// +/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake, +/// and the connection is aborted unless both endpoints pass the same bytestring. +const ALPN: &[u8] = b"iroh-example/echo/0"; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let router = start_accept_side().await?; + + // wait for the endpoint to be online + router.endpoint().online().await; + + connect_side(router.endpoint().addr()).await?; + + // This makes sure the endpoint in the router is closed properly and connections close gracefully + router.shutdown().await.anyerr()?; + + Ok(()) +} + +async fn connect_side(addr: EndpointAddr) -> Result<()> { + let endpoint = Endpoint::bind(presets::N0).await?; + + // Open a connection to the accepting endpoint + let conn = endpoint.connect(addr, ALPN).await?; + + // Open a bidirectional QUIC stream + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + + // Send some data to be echoed + send.write_all(b"Hello, world!").await.anyerr()?; + + // Signal the end of data for this particular stream + send.finish().anyerr()?; + + // Receive the echo, but limit reading up to maximum 1000 bytes + let response = recv.read_to_end(1000).await.anyerr()?; + assert_eq!(&response, b"Hello, world!"); + + // Explicitly close the whole connection. + conn.close(0u32.into(), b"bye!"); + + // The above call only queues a close message to be sent (see how it's not async!). + // We need to actually call this to make sure this message is sent out. + endpoint.close().await; + // If we don't call this, but continue using the endpoint, we then the queued + // close call will eventually be picked up and sent. + // But always try to wait for endpoint.close().await to go through before dropping + // the endpoint to ensure any queued messages are sent through and connections are + // closed gracefully. + Ok(()) +} + +async fn start_accept_side() -> Result { + let endpoint = Endpoint::bind(presets::N0).await?; + + // Build our protocol handler and add our protocol, identified by its ALPN, and spawn the endpoint. + let router = Router::builder(endpoint).accept(ALPN, Echo).spawn(); + + Ok(router) +} + +#[derive(Debug, Clone)] +struct Echo; + +impl ProtocolHandler for Echo { + /// The `accept` method is called for each incoming connection for our ALPN. + /// + /// The returned future runs on a newly spawned tokio task, so it can run as long as + /// the connection lasts. + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + // We can get the remote's endpoint id from the connection. + let endpoint_id = connection.remote_id(); + println!("accepted connection from {endpoint_id}"); + + // Our protocol is a simple request-response protocol, so we expect the + // connecting peer to open a single bi-directional stream. + let (mut send, mut recv) = connection.accept_bi().await?; + + // Echo any bytes received back directly. + // This will keep copying until the sender signals the end of data on the stream. + let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; + println!("Copied over {bytes_sent} byte(s)"); + + // By calling `finish` on the send stream we signal that we will not send anything + // further, which makes the receive stream on the other end terminate. + send.finish()?; + + // Wait until the remote closes the connection, which it does once it + // received the response. + connection.closed().await; + + Ok(()) + } +} diff --git a/vendor/iroh/examples/home-relay-status.rs b/vendor/iroh/examples/home-relay-status.rs new file mode 100644 index 0000000..849ba7d --- /dev/null +++ b/vendor/iroh/examples/home-relay-status.rs @@ -0,0 +1,89 @@ +//! Tests connection to a home relay server and reports the status. +//! +//! Also shows how to specifically test if a connection fails due to authentication. + +use clap::Parser; +use iroh::{Endpoint, RelayMap, RelayUrl, Watcher, endpoint::presets}; +use n0_future::StreamExt; + +#[derive(clap::Parser, Debug)] +struct Args { + /// Pass one or more relay URLs. + /// + /// If unset will use the public default relays. + relays: Vec, +} + +#[tokio::main] +async fn main() -> n0_error::Result<()> { + tracing_subscriber::fmt::init(); + let args = Args::parse(); + let relay_map = if args.relays.is_empty() { + iroh::defaults::prod::default_relay_map() + } else { + RelayMap::from_iter(args.relays) + }; + + let endpoint = Endpoint::builder(presets::Minimal) + .relay_mode(iroh::RelayMode::Custom(relay_map)) + .bind() + .await?; + + println!("endpoint bound"); + + // Spawn a task to report the home relay status. + // + // You could pass a channel here if you wanted to abort something in case the + // home relay connection fails. + tokio::spawn(report_home_relay_status(endpoint.clone())); + + // Wait for the endpoint to be online, i.e. connected to a home relay. + // + // Internally, this works like simplified version of the loop in `report_home_relay_status`, + // only reporting about whether a connection was established and not exposing any + // details in case of failure. + endpoint.online().await; + println!( + "Endpoint is online. Home relay: {}", + endpoint.addr().relay_urls().next().expect("has relay") + ); + + tokio::signal::ctrl_c().await.ok(); + + endpoint.close().await; + + Ok(()) +} + +async fn report_home_relay_status(endpoint: Endpoint) { + let mut home_relay_status = endpoint.home_relay_status().stream(); + // The stream moves to the next item whenever the home relay status changes in any way. + while let Some(status_list) = home_relay_status.next().await { + // The stream's item is a list of status entries, one per home relay. Today the list + // never holds more than one entry, because iroh only connects to a single home + // relay. It is a list so that iroh could support multiple home relays without an API + // change, so we may as well iterate it. + for status in status_list { + let url = status.url(); + if status.is_connected() { + // We are connected! + // When we reach this line, `Endpoint::online` also resolves. + println!("Relay {url}: Connected"); + } else if let Some(reason) = status.auth_denied_reason() { + // The relay server denied our authentication. Retrying will not help: we + // would present the same credentials again. Report this prominently in + // our app instead of waiting to come online. + println!("Relay {url}: Authentication denied ({reason})"); + } else if let Some(error) = status.last_error() { + // The connection failed for another reason, for example a network issue. + // iroh keeps retrying with a backoff, so let's just print it. + println!("Relay {url}: Connection failed ({error:#})"); + } else { + // We are not connected, and there is no error to report: either no + // connection has been attempted yet, or one is in progress. You should + // usually just loop over this and wait for the next update. + println!("Relay {url}: Disconnected") + } + } + } +} diff --git a/vendor/iroh/examples/incoming-filter.rs b/vendor/iroh/examples/incoming-filter.rs new file mode 100644 index 0000000..762ffa5 --- /dev/null +++ b/vendor/iroh/examples/incoming-filter.rs @@ -0,0 +1,76 @@ +//! Example demonstrating the [`IncomingFilter`] hook. +//! +//! This example requires all direct (UDP) connections to pass QUIC address +//! validation via a retry token before being accepted. +//! +//! QUIC address validation ensures that a client truly owns its claimed IP +//! address before proceeding with the more expensive part of the handshake, +//! preventing denial-of-service attacks via spoofed source IPs. +//! +//! Relay connections are +//! accepted without validation since the relay already vouches for the source. +//! +//! ## Usage +//! +//! ```sh +//! cargo run --example incoming-filter +//! ``` +//! +//! To test, connect from another process using: +//! ```sh +//! cargo run --example connect -- +//! ``` +use std::sync::Arc; + +use iroh::{ + Endpoint, + endpoint::{Connection, Incoming, IncomingAddr, presets}, + protocol::{AcceptError, IncomingFilterOutcome, ProtocolHandler, Router}, +}; +use n0_error::{Result, StdResultExt}; + +const ALPN: &[u8] = b"iroh-example/incoming-filter/0"; + +#[derive(Debug, Clone)] +struct Echo; + +impl ProtocolHandler for Echo { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let (mut send, mut recv) = connection.accept_bi().await?; + tokio::io::copy(&mut recv, &mut send).await?; + send.finish()?; + Ok(()) + } +} + +/// Require address validation for direct connections. +/// +/// If the address is not yet validated, return `Retry` so the client has to +/// prove it owns the source address before we do any further work. Validated +/// connections and relay connections are accepted. +fn filter(incoming: &Incoming) -> IncomingFilterOutcome { + match incoming.remote_addr() { + IncomingAddr::Ip(_) if !incoming.remote_addr_validated() => IncomingFilterOutcome::Retry, + _ => IncomingFilterOutcome::Accept, + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let endpoint = Endpoint::bind(presets::N0).await?; + + endpoint.online().await; + let addr = endpoint.addr(); + println!("Node ID: {}", endpoint.id()); + println!("Listening on: {addr:?}"); + println!("All direct connections require address validation via retry.\n"); + + let router = Router::builder(endpoint) + .incoming_filter(Arc::new(filter)) + .accept(ALPN, Echo) + .spawn(); + + tokio::signal::ctrl_c().await.anyerr()?; + router.shutdown().await.anyerr()?; + Ok(()) +} diff --git a/vendor/iroh/examples/listen-unreliable.rs b/vendor/iroh/examples/listen-unreliable.rs new file mode 100644 index 0000000..3e51868 --- /dev/null +++ b/vendor/iroh/examples/listen-unreliable.rs @@ -0,0 +1,99 @@ +//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect two devices and pass bytes using unreliable datagrams. +//! +//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection. +//! run this example from the project root: +//! $ cargo run --example listen-unreliable +use iroh::{Endpoint, RelayMode, SecretKey, endpoint::presets}; +use n0_error::{AnyError as Error, Result, StdResultExt}; +use tracing::{info, warn}; + +// An example ALPN that we are using to communicate over the `Endpoint` +const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0"; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + println!("\nlisten (unreliable) example!\n"); + let secret_key = SecretKey::generate(); + println!("public key: {}", secret_key.public()); + + // Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers. + let endpoint = Endpoint::builder(presets::N0) + // The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase. + .secret_key(secret_key) + // set the ALPN protocols this endpoint will accept on incoming connections + .alpns(vec![EXAMPLE_ALPN.to_vec()]) + // `RelayMode::Default` means that we will use the default relay servers to holepunch and relay. + // Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls. + // Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS + // If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code + .relay_mode(RelayMode::Default) + // you can choose a port to bind to, but passing in `0` will bind the socket to a random available port + .bind() + .await?; + + let me = endpoint.id(); + println!("endpoint id: {me}"); + println!("endpoint listening addresses:"); + + // wait for the endpoint to be online + endpoint.online().await; + + let endpoint_addr = endpoint.addr(); + let local_addrs = endpoint_addr + .ip_addrs() + .map(|addr| { + let addr = addr.to_string(); + println!("\t{addr}"); + addr + }) + .collect::>() + .join(" "); + let relay_url = endpoint_addr + .relay_urls() + .next() + .expect("Should have a relay URL, assuming a default endpoint setup."); + println!("endpoint relay server url: {relay_url}"); + println!("\nin a separate terminal run:"); + + println!( + "\tcargo run --example connect-unreliable -- --endpoint-id {me} --addrs \"{local_addrs}\" --relay-url {relay_url}\n" + ); + // accept incoming connections, returns a normal QUIC connection + + while let Some(incoming) = endpoint.accept().await { + let mut accepting = match incoming.accept() { + Ok(accepting) => accepting, + Err(err) => { + warn!("incoming connection failed: {err:#}"); + // we can carry on in these cases: + // this can be caused by retransmitted datagrams + continue; + } + }; + let alpn = accepting.alpn().await?; + let conn = accepting.await?; + let endpoint_id = conn.remote_id(); + info!( + "new (unreliable) connection from {endpoint_id} with ALPN {}", + String::from_utf8_lossy(&alpn), + ); + // spawn a task to handle reading and writing off of the connection + tokio::spawn(async move { + // use the `noq` API to read a datagram off the connection, and send a datagra, in return + while let Ok(message) = conn.read_datagram().await { + let message = String::from_utf8(message.into()).anyerr()?; + println!("received: {message}"); + + let message = format!("hi! you connected to {me}. bye bye"); + conn.send_datagram(message.as_bytes().to_vec().into()) + .anyerr()?; + } + + Ok::<_, Error>(()) + }); + } + // stop with SIGINT (ctrl-c) + + Ok(()) +} diff --git a/vendor/iroh/examples/listen.rs b/vendor/iroh/examples/listen.rs new file mode 100644 index 0000000..115ae18 --- /dev/null +++ b/vendor/iroh/examples/listen.rs @@ -0,0 +1,116 @@ +//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect two devices. +//! +//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection. +//! run this example from the project root: +//! $ cargo run --example listen +use std::time::Duration; + +use iroh::{ + Endpoint, RelayMode, SecretKey, + endpoint::{ConnectionError, presets}, +}; +use n0_error::{Result, StdResultExt}; +use tracing::{debug, info, warn}; + +// An example ALPN that we are using to communicate over the `Endpoint` +const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0"; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + println!("\nlisten example!\n"); + let secret_key = SecretKey::generate(); + println!("public key: {}", secret_key.public()); + + // Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay protocol and relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers. + let endpoint = Endpoint::builder(presets::N0) + // The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase. + .secret_key(secret_key) + // set the ALPN protocols this endpoint will accept on incoming connections + .alpns(vec![EXAMPLE_ALPN.to_vec()]) + // `RelayMode::Default` means that we will use the default relay servers to holepunch and relay. + // Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls. + // Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS + // If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code + .relay_mode(RelayMode::Default) + // you can choose a port to bind to, but passing in `0` will bind the socket to a random available port + .bind() + .await?; + + let me = endpoint.id(); + println!("endpoint id: {me}"); + println!("endpoint listening addresses:"); + + // wait for the endpoint to be online + endpoint.online().await; + let endpoint_addr = endpoint.addr(); + + let local_addrs = endpoint_addr + .ip_addrs() + .map(|addr| { + let addr = addr.to_string(); + println!("\t{addr}"); + addr + }) + .collect::>() + .join(" "); + let relay_url = endpoint_addr.relay_urls().next().expect("missing relay"); + println!("endpoint relay server url: {relay_url}"); + println!("\nin a separate terminal run:"); + + println!( + "\tcargo run --example connect -- --endpoint-id {me} --addrs \"{local_addrs}\" --relay-url {relay_url}\n" + ); + // accept incoming connections, returns a normal QUIC connection + while let Some(incoming) = endpoint.accept().await { + let mut accepting = match incoming.accept() { + Ok(accepting) => accepting, + Err(err) => { + warn!("incoming connection failed: {err:#}"); + // we can carry on in these cases: + // this can be caused by retransmitted datagrams + continue; + } + }; + let alpn = accepting.alpn().await?; + let conn = accepting.await?; + let endpoint_id = conn.remote_id(); + info!( + "new connection from {endpoint_id} with ALPN {}", + String::from_utf8_lossy(&alpn), + ); + + // spawn a task to handle reading and writing off of the connection + tokio::spawn(async move { + // accept a bi-directional QUIC connection + // use the `noq` APIs to send and recv content + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + debug!("accepted bi stream, waiting for data..."); + let message = recv.read_to_end(100).await.anyerr()?; + let message = String::from_utf8(message).anyerr()?; + println!("received: {message}"); + + let message = format!("hi! you connected to {me}. bye bye"); + send.write_all(message.as_bytes()).await.anyerr()?; + // call `finish` to close the connection gracefully + send.finish().anyerr()?; + + // We sent the last message, so wait for the client to close the connection once + // it received this message. + let res = tokio::time::timeout(Duration::from_secs(3), async move { + let closed = conn.closed().await; + if !matches!(closed, ConnectionError::ApplicationClosed(_)) { + println!("endpoint {endpoint_id} disconnected with an error: {closed:#}"); + } + }) + .await; + if res.is_err() { + println!("endpoint {endpoint_id} did not disconnect within 3 seconds"); + } + n0_error::Ok(()) + }); + } + // stop with SIGINT (ctrl-c) + + Ok(()) +} diff --git a/vendor/iroh/examples/monitor-connections.rs b/vendor/iroh/examples/monitor-connections.rs new file mode 100644 index 0000000..6ece2de --- /dev/null +++ b/vendor/iroh/examples/monitor-connections.rs @@ -0,0 +1,159 @@ +use std::{sync::Arc, time::Duration}; + +use iroh::{ + Endpoint, + endpoint::{ + AfterHandshakeOutcome, Closed, Connection, EndpointHooks, WeakConnectionHandle, presets, + }, +}; +use n0_error::{Result, StackResultExt, StdResultExt, ensure_any}; +use n0_future::task::AbortOnDropHandle; +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinSet, +}; +use tracing::{Instrument, info, info_span}; + +const ALPN: &[u8] = b"iroh/test"; + +#[tokio::main] +async fn main() -> Result { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .init(); + + let monitor = Monitor::new(); + let server = Endpoint::builder(presets::Minimal) + .alpns(vec![ALPN.to_vec()]) + .hooks(monitor.clone()) + .bind() + .instrument(info_span!("server")) + .await?; + let server_addr = server.addr(); + + let count = 2; + + let client_task = tokio::spawn( + async move { + let client = Endpoint::builder(presets::Minimal) + .bind() + .instrument(info_span!("client")) + .await?; + for _i in 0..count { + let conn = client.connect(server_addr.clone(), ALPN).await?; + let mut s = conn.accept_uni().await.anyerr()?; + let data = s.read_to_end(2).await.anyerr()?; + ensure_any!(data == b"hi", "unexpected data"); + conn.close(23u32.into(), b"bye"); + } + client.close().await; + n0_error::Ok(client) + } + .instrument(info_span!("client")), + ); + + let server_task = tokio::spawn( + async move { + for _i in 0..count { + let conn = server + .accept() + .await + .context("server endpoint closed")? + .await?; + let mut s = conn.open_uni().await.anyerr()?; + s.write_all(b"hi").await.anyerr()?; + s.finish().anyerr()?; + conn.closed().await; + } + server.close().await; + n0_error::Ok(()) + } + .instrument(info_span!("server")), + ); + client_task.await.std_context("client")?.context("client")?; + server_task.await.std_context("server")?.context("server")?; + tokio::time::sleep(Duration::from_secs(1)).await; + drop(monitor); + Ok(()) +} + +/// Our connection monitor impl. +/// +/// This here only logs connection open and close events via tracing. +/// It could also maintain a datastructure of all connections, or send the stats to some metrics service. +#[derive(Clone, Debug)] +struct Monitor { + tx: UnboundedSender, + _task: Arc>, +} + +/// Static info captured at handshake time, paired with a weak handle to the connection. +/// +/// We capture `alpn` and `remote_id` at hook time because [`WeakConnectionHandle`] only +/// exposes [`upgrade`] and [`closed`], so reading these fields after the connection has +/// been dropped would otherwise be impossible. +/// +/// [`upgrade`]: WeakConnectionHandle::upgrade +/// [`closed`]: WeakConnectionHandle::closed +#[derive(Debug)] +struct MonitoredConnection { + alpn: Vec, + remote_id: iroh::EndpointId, + handle: WeakConnectionHandle, +} + +impl EndpointHooks for Monitor { + async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome { + let info = MonitoredConnection { + alpn: conn.alpn().to_vec(), + remote_id: conn.remote_id(), + handle: conn.weak_handle(), + }; + self.tx.send(info).ok(); + AfterHandshakeOutcome::Accept + } +} + +impl Monitor { + fn new() -> Self { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let task = tokio::spawn(Self::run(rx).instrument(info_span!("watcher"))); + Self { + tx, + _task: Arc::new(AbortOnDropHandle::new(task)), + } + } + + async fn run(mut rx: UnboundedReceiver) { + let mut tasks = JoinSet::new(); + loop { + tokio::select! { + Some(MonitoredConnection { alpn, remote_id, handle }) = rx.recv() => { + let alpn = String::from_utf8_lossy(&alpn).to_string(); + let remote = remote_id.fmt_short(); + let rtt = handle.upgrade().and_then(|c| c.paths().iter().map(|p| p.rtt()).min()); + info!(%remote, %alpn, ?rtt, "new connection"); + tasks.spawn(async move { + match handle.closed().await { + Some(Closed { reason, stats, .. }) => { + // We have access to the final stats of the connection! + info!(%remote, %alpn, ?reason, udp_rx=stats.udp_rx.bytes, udp_tx=stats.udp_tx.bytes, "connection closed"); + } + None => { + // The connection was closed before we could register our stats-on-close listener. + info!(%remote, %alpn, "connection closed before tracking started"); + } + } + }.instrument(tracing::Span::current())); + } + Some(res) = tasks.join_next(), if !tasks.is_empty() => res.expect("conn close task panicked"), + else => break, + } + while let Some(res) = tasks.join_next().await { + res.expect("conn close task panicked"); + } + } + } +} diff --git a/vendor/iroh/examples/pq-only-key-exchange.rs b/vendor/iroh/examples/pq-only-key-exchange.rs new file mode 100644 index 0000000..7d82a15 --- /dev/null +++ b/vendor/iroh/examples/pq-only-key-exchange.rs @@ -0,0 +1,94 @@ +//! Force iroh to negotiate ONLY a post-quantum key exchange (X25519MLKEM768). +//! +//! Requires `tls-aws-lc-rs` (the only rustls backend with ML-KEM today). +//! Stripping `kx_groups` to the PQ group makes PQ *required* rather than +//! *preferred*: peers without it fail the TLS handshake. +//! +//! Note: iroh's `crypto_provider` is shared with relay/discovery TLS, and +//! n0's infra does not support PQ key exchange yet — so a PQ-only endpoint +//! can't use n0 public relays or discovery servers. +//! +//! ## Usage +//! +//! `tls-aws-lc-rs` is required (the example explicitly constructs an +//! aws-lc-rs `CryptoProvider`, so the backend must be linked): +//! +//! cargo run --example pq-key-exchange --features=tls-aws-lc-rs +//! +//! With iroh's default features still on, both `ring` and `aws-lc-rs` get +//! linked. That's harmless — we wire the aws-lc-rs provider in directly via +//! `Builder::crypto_provider`. +use std::sync::Arc; + +use iroh::{ + RelayMode, + endpoint::{Endpoint, presets::Empty}, +}; +use n0_error::{Result, StdResultExt}; +use rustls::crypto::aws_lc_rs; + +const ALPN: &[u8] = b"iroh-example/pq-key-exchange/0"; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let pq = pq_only_provider(); + + let server = Endpoint::builder(Empty) + .crypto_provider(pq.clone()) + .alpns(vec![ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + .bind() + .await?; + let server_addr = server.addr(); + + let server_task = tokio::spawn({ + let server = server.clone(); + async move { + let conn = server + .accept() + .await + .expect("incoming") + .accept() + .anyerr()? + .await + .anyerr()?; + let mut recv = conn.accept_uni().await.anyerr()?; + let msg = recv.read_to_end(1024).await.anyerr()?; + println!("server received: {:?}", String::from_utf8_lossy(&msg)); + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"pong over PQ").await.anyerr()?; + send.finish().anyerr()?; + conn.closed().await; + n0_error::Ok(()) + } + }); + + let client = Endpoint::builder(Empty) + .crypto_provider(pq) + .relay_mode(RelayMode::Disabled) + .bind() + .await?; + let conn = client.connect(server_addr, ALPN).await.anyerr()?; + println!("client handshake done (X25519MLKEM768 was the only kx offered)"); + + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"ping over PQ").await.anyerr()?; + send.finish().anyerr()?; + let mut recv = conn.accept_uni().await.anyerr()?; + let msg = recv.read_to_end(1024).await.anyerr()?; + println!("client received: {:?}", String::from_utf8_lossy(&msg)); + + conn.close(0u32.into(), b"done"); + server_task.await.anyerr()?.anyerr()?; + client.close().await; + server.close().await; + Ok(()) +} + +fn pq_only_provider() -> Arc { + let mut p = aws_lc_rs::default_provider(); + p.kx_groups = vec![aws_lc_rs::kx_group::X25519MLKEM768]; + Arc::new(p) +} diff --git a/vendor/iroh/examples/prefer-pq-key-exchange.rs b/vendor/iroh/examples/prefer-pq-key-exchange.rs new file mode 100644 index 0000000..fbed007 --- /dev/null +++ b/vendor/iroh/examples/prefer-pq-key-exchange.rs @@ -0,0 +1,90 @@ +//! Prefer post-quantum key exchange when available, fall back to classical. +//! +//! Unlike `pq-only-key-exchange`, this example keeps classical kx groups in +//! the list so the endpoint can still talk to peers that don't support +//! ML-KEM, *and* so n0's relay/discovery TLS (classical kx) keeps working. +//! Putting `X25519MLKEM768` first in `kx_groups` means rustls negotiates PQ +//! whenever both peers support it, classical otherwise. +//! +//! Note: rustls' `aws_lc_rs::default_provider()` only puts `X25519MLKEM768` +//! first when rustls is built with its `prefer-post-quantum` feature; without +//! it, PQ is offered last. We override `kx_groups` here so the policy is +//! independent of how rustls was compiled, and print the list at startup. +//! +//! ## Usage +//! +//! `tls-aws-lc-rs` is required: +//! +//! cargo run --example prefer-pq-key-exchange --features=tls-aws-lc-rs +use std::sync::Arc; + +use iroh::endpoint::{Endpoint, presets::N0}; +use n0_error::{Result, StdResultExt}; +use rustls::crypto::aws_lc_rs::{self, kx_group}; + +const ALPN: &[u8] = b"iroh-example/prefer-pq-key-exchange/0"; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let mut provider = aws_lc_rs::default_provider(); + provider.kx_groups = vec![ + kx_group::X25519MLKEM768, + kx_group::X25519, + kx_group::SECP256R1, + kx_group::SECP384R1, + ]; + let kx_names: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect(); + println!("kx_groups (in offer order): {kx_names:?}"); + let pq = Arc::new(provider); + + let server = Endpoint::builder(N0) + .crypto_provider(pq.clone()) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await?; + server.online().await; + let server_addr = server.addr(); + println!("server addr: {server_addr:?}"); + + let server_task = tokio::spawn({ + let server = server.clone(); + async move { + let conn = server + .accept() + .await + .expect("incoming") + .accept() + .anyerr()? + .await + .anyerr()?; + let mut recv = conn.accept_uni().await.anyerr()?; + let msg = recv.read_to_end(1024).await.anyerr()?; + println!("server received: {:?}", String::from_utf8_lossy(&msg)); + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"pong over PQ-preferred").await.anyerr()?; + send.finish().anyerr()?; + conn.closed().await; + n0_error::Ok(()) + } + }); + + let client = Endpoint::builder(N0).crypto_provider(pq).bind().await?; + client.online().await; + let conn = client.connect(server_addr, ALPN).await.anyerr()?; + println!("client handshake done (X25519MLKEM768 preferred, classical kx kept as fallback)"); + + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"ping over PQ-preferred").await.anyerr()?; + send.finish().anyerr()?; + let mut recv = conn.accept_uni().await.anyerr()?; + let msg = recv.read_to_end(1024).await.anyerr()?; + println!("client received: {:?}", String::from_utf8_lossy(&msg)); + + conn.close(0u32.into(), b"done"); + server_task.await.anyerr()?.anyerr()?; + client.close().await; + server.close().await; + Ok(()) +} diff --git a/vendor/iroh/examples/remote-info.rs b/vendor/iroh/examples/remote-info.rs new file mode 100644 index 0000000..ffc36a2 --- /dev/null +++ b/vendor/iroh/examples/remote-info.rs @@ -0,0 +1,447 @@ +//! Example for using an iroh hook to collect information about remote endpoints. +//! +//! This implements a [`RemoteMap`] which collects information about all connections and paths from an iroh endpoint. +//! The remote map can be cloned and inspected from other tasks at any time. It contains both data about all +//! currently active connections, and an aggregate status for each remote that remains available even after +//! all connections to the endpoint have been closed. + +use std::time::{Duration, SystemTime}; + +use iroh::{Endpoint, EndpointAddr, endpoint::presets}; +use n0_error::{Result, StackResultExt, StdResultExt, ensure_any}; +use n0_future::IterExt; +use tracing::{Instrument, info, info_span}; + +use crate::remote_map::RemoteMap; + +const ALPN: &[u8] = b"iroh/test"; + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .init(); + + // Create the remote map and hook. + let (hook, remote_map) = RemoteMap::new(); + + // Bind our endpoint and install the remote map hook. + let server = Endpoint::builder(presets::N0) + .alpns(vec![ALPN.to_vec()]) + .hooks(hook) + .bind() + .instrument(info_span!("server")) + .await?; + // Wait for our endpoint to be fully online. + server.online().await; + let server_addr = server.addr(); + + // Spawn a task that creates `count` client endpoints that each connect to our server. + let count = 3; + let client_task = tokio::spawn(run_clients(server_addr, count)); + + // Spawn a task that prints info from the remote map while some connections are active. + // You can use this info to make decisions about remotes. + let _inspect_task = tokio::task::spawn({ + let remote_map = remote_map.clone(); + async move { + // Wait a bit. + tokio::time::sleep(Duration::from_millis(500)).await; + println!("== while connections are active == "); + log_active(&remote_map); + log_aggregate(&remote_map); + println!(); + } + }); + + // Let the server accept `count` connections in parallel. + // The server keeps all connections open for at least 500 milliseconds. + std::iter::repeat_with(async || { + let conn = server + .accept() + .await + .context("server endpoint closed")? + .await?; + info!("accepted"); + let mut s = conn.open_uni().await.anyerr()?; + // wait a bit. + tokio::time::sleep(Duration::from_millis(500)).await; + s.write_all(b"hi").await.anyerr()?; + s.finish().anyerr()?; + conn.closed().await; + info!("closed"); + n0_error::Ok(()) + }) + .take(count) + .enumerate() + .map(|(i, fut)| fut.instrument(info_span!("server-conn", %i))) + .try_join_all() + .await?; + + // Print the remote map again. + println!("== all connections closed =="); + log_active(&remote_map); + log_aggregate(&remote_map); + + server.close().await; + client_task.await.std_context("client")?.context("client")?; + + Ok(()) +} + +/// Uses the current connection info to print info about a remote. +/// +/// Uses the info about *currently active* connections, which return `None` if no connections are active. +fn log_active(remote_map: &RemoteMap) { + println!("current remote state:"); + for (id, info) in remote_map.read().iter() { + println!( + "[{}] is_active {}, connections {}, ip_path {:?}, relay_path {:?}, current_min_rtt {:?}", + id.fmt_short(), + info.is_active(), + info.active_connections(), + info.has_ip_path(), + info.has_relay_path(), + info.current_min_rtt(), + ); + } +} + +/// Uses the aggregated info to print info about a remote. +/// +/// The aggregated info is updated for all connection and path changes, and stays at the latest values +/// even if all connections are closed. +fn log_aggregate(remote_map: &RemoteMap) { + println!("aggregate remote state:"); + for (id, info) in remote_map.read().iter() { + let aggregate = info.aggregate(); + println!( + "[{}] min_rtt {:?}, max_rtt {:?}, ip_path {:?}, relay_path {}, last_update {:?} ago", + id.fmt_short(), + aggregate.rtt_min, + aggregate.rtt_max, + aggregate.ip_path, + aggregate.relay_path, + SystemTime::now() + .duration_since(aggregate.last_update) + .unwrap_or_default() + ); + } +} + +async fn run_clients(server_addr: EndpointAddr, count: usize) -> Result { + std::iter::repeat_with(async || { + let client = Endpoint::builder(presets::N0) + .bind() + .instrument(info_span!("client")) + .await?; + let conn = client.connect(server_addr.clone(), ALPN).await?; + info!("connected"); + let mut s = conn.accept_uni().await.anyerr()?; + let data = s.read_to_end(2).await.anyerr()?; + ensure_any!(data == b"hi", "unexpected data"); + conn.close(23u32.into(), b"bye"); + info!("closed"); + client.close().await; + n0_error::Ok(()) + }) + .take(count) + .enumerate() + .map(|(i, fut)| fut.instrument(info_span!("client", %i))) + .try_join_all() + .await?; + Ok(()) +} + +mod remote_map { + //! Implementation of a remote map and hook to track information about all remote endpoints to which an iroh endpoint + //! has connections with. + + use std::{ + collections::HashMap, + sync::{Arc, RwLock, RwLockReadGuard}, + time::{Duration, SystemTime}, + }; + + use iroh::{ + EndpointId, TransportAddr, + endpoint::{ + AfterHandshakeOutcome, Connection, EndpointHooks, PathEvent, WeakConnectionHandle, + }, + }; + use n0_future::{StreamExt, task::AbortOnDropHandle}; + use tokio::{sync::mpsc, task::JoinSet}; + use tracing::{Instrument, debug, info, info_span}; + + /// Information about a remote info. + #[derive(Debug, Default)] + pub struct RemoteInfo { + aggregate: Aggregate, + connections: HashMap, + } + + /// Aggregate information about a remote info. + #[derive(Debug)] + pub struct Aggregate { + /// Minimal RTT observed over all paths to this remote. + pub rtt_min: Duration, + /// Maximal RTT observed over all paths to this remote. + pub rtt_max: Duration, + /// Whether we ever had an IP path to this remote. + pub ip_path: bool, + /// Whether we ever had a relay path to this remote. + pub relay_path: bool, + /// Time this aggregate was last updated. + pub last_update: SystemTime, + } + + impl Default for Aggregate { + fn default() -> Self { + Self { + rtt_min: Duration::MAX, + rtt_max: Duration::ZERO, + ip_path: false, + relay_path: false, + last_update: SystemTime::UNIX_EPOCH, + } + } + } + + impl Aggregate { + fn update(&mut self, addr: TransportAddr, stats: iroh::endpoint::PathStats) { + self.last_update = SystemTime::now(); + self.ip_path |= addr.is_ip(); + self.relay_path |= addr.is_relay(); + debug!("path update {addr} {stats:?}"); + self.rtt_min = self.rtt_min.min(stats.rtt); + self.rtt_max = self.rtt_max.max(stats.rtt); + } + } + + impl RemoteInfo { + /// Returns an aggregate of stats for this remote. + /// + /// This includes info from closed connections. + pub fn aggregate(&self) -> &Aggregate { + &self.aggregate + } + + /// Returns the minimal RTT of all currently active paths. + /// + /// Returns `None` if there are no active connections. + pub fn current_min_rtt(&self) -> Option { + self.upgraded() + .filter_map(|c| c.paths().iter().map(|p| p.rtt()).min()) + .min() + } + + /// Returns whether any active connection to the remote has an active IP path. + pub fn has_ip_path(&self) -> bool { + self.upgraded().any(|c| c.paths().iter().any(|p| p.is_ip())) + } + + /// Returns whether any active connection to the remote has an active relay path. + pub fn has_relay_path(&self) -> bool { + self.upgraded() + .any(|c| c.paths().iter().any(|p| p.is_relay())) + } + + /// Returns `true` if there are active connections to this remote. + pub fn is_active(&self) -> bool { + self.active_connections() > 0 + } + + /// Returns the number of active connections to this remote. + pub fn active_connections(&self) -> usize { + self.upgraded().count() + } + + /// Returns an iterator over all active handles upgraded to a [`Connection`]. + fn upgraded(&self) -> impl Iterator { + self.connections + .values() + .filter_map(WeakConnectionHandle::upgrade) + } + } + + type RemoteMapInner = Arc>>; + + /// Contains information about remote nodes our endpoint has or had connections with. + #[derive(Clone, Debug)] + pub struct RemoteMap { + map: RemoteMapInner, + _task: Arc>, + } + + /// Hook to collect information about remote endpoints from an endpoint. + #[derive(Debug)] + pub struct RemoteMapHook { + tx: mpsc::Sender, + } + + /// Pairing of a connection's remote endpoint id, captured at handshake time, with a + /// weak handle to the connection. The `remote_id` is captured eagerly because + /// [`WeakConnectionHandle`] only exposes [`upgrade`] and [`closed`]. + /// + /// [`upgrade`]: WeakConnectionHandle::upgrade + /// [`closed`]: WeakConnectionHandle::closed + #[derive(Debug, Clone)] + pub struct TrackedConnection { + pub remote_id: EndpointId, + pub handle: WeakConnectionHandle, + } + + impl EndpointHooks for RemoteMapHook { + async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome { + info!(remote=%conn.remote_id().fmt_short(), "after_handshake"); + // We track the connection via a weak handle so this map does not keep the + // connection alive past the application's last reference to it. + let tracked = TrackedConnection { + remote_id: conn.remote_id(), + handle: conn.weak_handle(), + }; + self.tx.send(tracked).await.ok(); + AfterHandshakeOutcome::Accept + } + } + + impl RemoteMap { + /// Creates a new [`RemoteMapHook`] and [`RemoteMap`]. + pub fn new() -> (RemoteMapHook, Self) { + Self::with_max_retention(Duration::from_secs(60 * 5)) + } + + /// Creates a new [`RemoteMapHook`] and [`RemoteMap`] and configure the retention time. + /// + /// `retention_time` is the time entries for remote endpoints remain in the map after the last connection has closed. + pub fn with_max_retention(retention_time: Duration) -> (RemoteMapHook, Self) { + let (tx, rx) = mpsc::channel(8); + let map = RemoteMapInner::default(); + let task = tokio::spawn( + Self::run(rx, map.clone(), retention_time) + .instrument(info_span!("remote-map-task")), + ); + let map = Self { + map, + _task: Arc::new(AbortOnDropHandle::new(task)), + }; + let hook = RemoteMapHook { tx }; + (hook, map) + } + + /// Read the current state of the remote map. + /// + /// Returns a [`RwLockReadGuard`] with the actual remote map. Don't hold over await points! + pub fn read(&self) -> RwLockReadGuard<'_, HashMap> { + self.map.read().expect("poisoned") + } + + async fn run( + mut rx: mpsc::Receiver, + map: RemoteMapInner, + retention_time: Duration, + ) { + let mut tasks = JoinSet::new(); + let mut conn_id = 0; + + // Spawn a task to clear expired entries. + let expiry_task = tasks.spawn(Self::clear_expired(retention_time, map.clone())); + + // Main loop + loop { + tokio::select! { + Some(conn) = rx.recv() => { + conn_id += 1; + Self::on_connection(&mut tasks, map.clone(), conn_id, conn); + } + Some(res) = tasks.join_next(), if !tasks.is_empty() => { + res.expect("conn close task panicked"); + } + else => break, + } + } + + // Abort expiry task and join remaining tasks. + expiry_task.abort(); + while let Some(res) = tasks.join_next().await { + if let Err(err) = &res + && !err.is_cancelled() + { + res.expect("conn close task panicked"); + } + } + } + + fn on_connection( + tasks: &mut JoinSet<()>, + map: RemoteMapInner, + conn_id: u64, + conn: TrackedConnection, + ) { + // Store conn info for full introspection possibility. + { + let mut inner = map.write().expect("poisoned"); + inner + .entry(conn.remote_id) + .or_default() + .connections + .insert(conn_id, conn.handle.clone()); + } + + // Track path changes to update stats aggregate. + if let Some(mut path_events) = conn.handle.upgrade().map(|conn| conn.path_events()) { + tasks.spawn( + async move { + while let Some(event) = path_events.next().await { + let mut inner = map.write().expect("poisoned"); + let info = inner.entry(conn.remote_id).or_default(); + match event { + PathEvent::Closed { + remote_addr, + last_stats, + .. + } => { + info.aggregate.update(remote_addr, *last_stats); + } + _ => { + if let Some(conn) = conn.handle.upgrade() { + for path in conn.paths().iter() { + info.aggregate + .update(path.remote_addr().clone(), path.stats()); + } + } + } + } + } + let mut inner = map.write().expect("poisoned"); + let info = inner.entry(conn.remote_id).or_default(); + info.connections.remove(&conn_id); + info.aggregate.last_update = SystemTime::now(); + } + .instrument(tracing::Span::current()), + ); + } + } + + async fn clear_expired( + retention_time: Duration, + map: Arc>>, + ) { + let mut interval = tokio::time::interval(retention_time); + loop { + interval.tick().await; + let now = SystemTime::now(); + let mut inner = map.write().expect("poisoned"); + inner.retain(|_remote, info| { + info.is_active() + || now + .duration_since(info.aggregate().last_update) + .map(|age| age < retention_time) + .unwrap_or(true) + }); + } + } + } +} diff --git a/vendor/iroh/examples/screening-connection.rs b/vendor/iroh/examples/screening-connection.rs new file mode 100644 index 0000000..076c51d --- /dev/null +++ b/vendor/iroh/examples/screening-connection.rs @@ -0,0 +1,150 @@ +//! Very basic example to showcase how to write a protocol that rejects new +//! connections based on internal state. Useful when you want an endpoint to +//! stop accepting new connections for some reason only known to the endpoint. Maybe +//! it's doing a migration, starting up, in a "maintenance mode", or serving +//! too many connections. +//! +//! ## Usage +//! +//! cargo run --example screening-connection +use std::sync::{ + Arc, + atomic::{AtomicU64, Ordering}, +}; + +use iroh::{ + Endpoint, EndpointAddr, + endpoint::{Accepting, Connection, presets}, + protocol::{AcceptError, ProtocolHandler, Router}, +}; +use n0_error::{Result, StdResultExt, e}; + +/// Each protocol is identified by its ALPN string. +/// +/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake, +/// and the connection is aborted unless both endpoints pass the same bytestring. +const ALPN: &[u8] = b"iroh-example/screening-connection/0"; + +#[tokio::main] +async fn main() -> Result<()> { + let router = start_accept_side().await?; + // Wait for the endpoint to be reachable + router.endpoint().online().await; + let endpoint_addr = router.endpoint().addr(); + + // call connect three times. connection index 1 will be an odd number, and rejected. + connect_side(&endpoint_addr).await?; + if let Err(err) = connect_side(&endpoint_addr).await { + println!("Error connecting: {}", err); + } + connect_side(&endpoint_addr).await?; + + // This makes sure the endpoint in the router is closed properly and connections close gracefully + router.shutdown().await.anyerr()?; + + Ok(()) +} + +async fn connect_side(addr: &EndpointAddr) -> Result<()> { + let endpoint = Endpoint::bind(presets::N0).await?; + + // Open a connection to the accepting endpoint + let conn = endpoint.connect(addr.clone(), ALPN).await?; + + // Open a bidirectional QUIC stream + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + + // Send some data to be echoed + send.write_all(b"Hello, world!").await.anyerr()?; + + // Signal the end of data for this particular stream + send.finish().anyerr()?; + + // Receive the echo, but limit reading up to maximum 1000 bytes + let response = recv.read_to_end(1000).await.anyerr()?; + assert_eq!(&response, b"Hello, world!"); + + // Explicitly close the whole connection. + conn.close(0u32.into(), b"bye!"); + + // The above call only queues a close message to be sent (see how it's not async!). + // We need to actually call this to make sure this message is sent out. + endpoint.close().await; + // If we don't call this, but continue using the endpoint, we then the queued + // close call will eventually be picked up and sent. + // But always try to wait for endpoint.close().await to go through before dropping + // the endpoint to ensure any queued messages are sent through and connections are + // closed gracefully. + Ok(()) +} + +async fn start_accept_side() -> Result { + let endpoint = Endpoint::bind(presets::N0).await?; + + let echo = ScreenedEcho { + conn_attempt_count: Arc::new(AtomicU64::new(0)), + }; + + // Build our protocol handler and add our protocol, identified by its ALPN, and spawn the endpoint. + let router = Router::builder(endpoint).accept(ALPN, echo).spawn(); + + Ok(router) +} + +/// This is the same as the echo example, but keeps an internal count of the +/// number of connections that have been attempted. This is to demonstrate how +/// to plumb state into the protocol handler +#[derive(Debug, Clone)] +struct ScreenedEcho { + conn_attempt_count: Arc, +} + +impl ProtocolHandler for ScreenedEcho { + /// `on_accepting` allows us to intercept a connection as it's being formed, + /// which is the right place to cut off a connection as early as possible. + /// This is an optional method on the ProtocolHandler trait. + async fn on_accepting(&self, accepting: Accepting) -> Result { + self.conn_attempt_count.fetch_add(1, Ordering::Relaxed); + let count = self.conn_attempt_count.load(Ordering::Relaxed); + + // reject every other connection + if count.is_multiple_of(2) { + println!("rejecting connection"); + return Err(e!(AcceptError::NotAllowed)); + } + + // To allow normal connection construction, await the accepting future & return + let conn = accepting.await?; + Ok(conn) + } + + /// The `accept` method is called for each incoming connection for our ALPN. + /// This is the primary place to kick off work in response to a new connection. + /// + /// The returned future runs on a newly spawned tokio task, so it can run as long as + /// the connection lasts. + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + // We can get the remote's endpoint id from the connection. + let endpoint_id = connection.remote_id(); + println!("accepted connection from {endpoint_id}"); + + // Our protocol is a simple request-response protocol, so we expect the + // connecting peer to open a single bi-directional stream. + let (mut send, mut recv) = connection.accept_bi().await?; + + // Echo any bytes received back directly. + // This will keep copying until the sender signals the end of data on the stream. + let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; + println!("Copied over {bytes_sent} byte(s)"); + + // By calling `finish` on the send stream we signal that we will not send anything + // further, which makes the receive stream on the other end terminate. + send.finish()?; + + // Wait until the remote closes the connection, which it does once it + // received the response. + connection.closed().await; + + Ok(()) + } +} diff --git a/vendor/iroh/examples/search.rs b/vendor/iroh/examples/search.rs new file mode 100644 index 0000000..b89e268 --- /dev/null +++ b/vendor/iroh/examples/search.rs @@ -0,0 +1,228 @@ +//! Example protocol for running search on a remote endpoint. +//! +//! We are building a very simple protocol here. +//! +//! Our protocol allows querying the text stored on the other endpoint. +//! +//! The example is contrived - we only use memory endpoints, and our database is a hashmap in a mutex, +//! and our queries just match if the query string appears as-is. +//! +//! ## Usage +//! +//! In one terminal, run +//! +//! cargo run --example search -- listen "hello-world" "foo-bar" "hello-moon" +//! +//! This spawns an iroh endpoint with three blobs. It will print the endpoint's endpoint id. +//! +//! In another terminal, run +//! +//! cargo run --example search -- query hello +//! +//! Replace with the endpoint id from above. This will connect to the listening endpoint with our +//! protocol and query for the string `hello`. The listening endpoint will return a number of how many +//! strings match the query. +//! +//! For this example, this will print: +//! +//! Found 2 matches +//! +//! That's it! Follow along in the code below, we added a bunch of comments to explain things. + +use std::{collections::BTreeSet, sync::Arc}; + +use clap::Parser; +use iroh::{ + Endpoint, EndpointId, + endpoint::{Connection, presets}, + protocol::{AcceptError, ProtocolHandler, Router}, +}; +use n0_error::{Result, StdResultExt}; +use tokio::sync::Mutex; +use tracing_subscriber::{EnvFilter, prelude::*}; + +#[derive(Debug, Parser)] +pub struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Parser)] +pub enum Command { + /// Spawn an endpoint in listening mode. + Listen { + /// Each text string will be imported as a blob and inserted into the search database. + text: Vec, + }, + /// Query a remote endpoint for data and print the results. + Query { + /// The endpoint id of the endpoint we want to query. + endpoint_id: EndpointId, + /// The text we want to match. + query: String, + }, +} + +/// Each protocol is identified by its ALPN string. +/// +/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake, +/// and the connection is aborted unless both endpoints pass the same bytestring. +const ALPN: &[u8] = b"iroh-example/text-search/0"; + +#[tokio::main] +async fn main() -> Result<()> { + setup_logging(); + let args = Cli::parse(); + + // Build an endpoint + let endpoint = Endpoint::bind(presets::N0).await?; + + // Build our protocol handler. The `builder` exposes access to various subsystems in the + // iroh endpoint. In our case, we need a blobs client and the endpoint. + let proto = BlobSearch::new(endpoint.clone()); + + let builder = Router::builder(endpoint); + + // Add our protocol, identified by our ALPN, to the endpoint, and spawn the endpoint. + let router = builder.accept(ALPN, proto.clone()).spawn(); + + match args.command { + Command::Listen { text } => { + let endpoint_id = router.endpoint().id(); + println!("our endpoint id: {endpoint_id}"); + + // Insert the text strings as blobs and index them. + for text in text.into_iter() { + proto.insert(text).await?; + } + + // Wait for Ctrl-C to be pressed. + tokio::signal::ctrl_c().await.anyerr()?; + } + Command::Query { endpoint_id, query } => { + // Query the remote endpoint. + // This will send the query over our protocol, read hashes on the reply stream, + // and download each hash over iroh-blobs. + let num_matches = proto.query_remote(endpoint_id, &query).await?; + + // Print out our query results. + println!("Found {num_matches} matches"); + } + } + + router.shutdown().await.anyerr()?; + + Ok(()) +} + +#[derive(Debug, Clone)] +struct BlobSearch { + endpoint: Endpoint, + blobs: Arc>>, +} + +impl ProtocolHandler for BlobSearch { + /// The `accept` method is called for each incoming connection for our ALPN. + /// + /// The returned future runs on a newly spawned tokio task, so it can run as long as + /// the connection lasts. + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + // We can get the remote's endpoint id from the connection. + let endpoint_id = connection.remote_id(); + println!("accepted connection from {endpoint_id}"); + + // Our protocol is a simple request-response protocol, so we expect the + // connecting peer to open a single bi-directional stream. + let (mut send, mut recv) = connection.accept_bi().await?; + + // We read the query from the receive stream, while enforcing a max query length. + let query_bytes = recv.read_to_end(64).await.map_err(AcceptError::from_err)?; + + // Now, we can perform the actual query on our local database. + let query = String::from_utf8(query_bytes).map_err(AcceptError::from_err)?; + let num_matches = self.query_local(&query).await; + + // We want to return a list of hashes. We do the simplest thing possible, and just send + // one hash after the other. Because the hashes have a fixed size of 32 bytes, this is + // very easy to parse on the other end. + send.write_all(&num_matches.to_le_bytes()) + .await + .map_err(AcceptError::from_err)?; + + // By calling `finish` on the send stream we signal that we will not send anything + // further, which makes the receive stream on the other end terminate. + send.finish()?; + + // Wait until the remote closes the connection, which it does once it + // received the response. + connection.closed().await; + + Ok(()) + } +} + +impl BlobSearch { + /// Create a new protocol handler. + pub fn new(endpoint: Endpoint) -> Self { + Self { + endpoint, + blobs: Default::default(), + } + } + + /// Query a remote endpoint, download all matching blobs and print the results. + pub async fn query_remote(&self, endpoint_id: EndpointId, query: &str) -> Result { + // Establish a connection to our endpoint. + // We use the default address lookup in iroh, so we can connect by endpoint id without + // providing further information. + let conn = self.endpoint.connect(endpoint_id, ALPN).await?; + + // Open a bi-directional in our connection. + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + + // Send our query. + send.write_all(query.as_bytes()).await.anyerr()?; + + // Finish the send stream, signalling that no further data will be sent. + // This makes the `read_to_end` call on the accepting side terminate. + send.finish().anyerr()?; + + // The response is a 64 bit integer + // We simply read it into a byte buffer. + let mut num_matches = [0u8; 8]; + + // Read 8 bytes from the stream. + recv.read_exact(&mut num_matches).await.anyerr()?; + + let num_matches = u64::from_le_bytes(num_matches); + + // Dropping the connection here will close it. + + Ok(num_matches) + } + + /// Query the local database. + /// + /// Returns how many matches were found. + pub async fn query_local(&self, query: &str) -> u64 { + let guard = self.blobs.lock().await; + let count: usize = guard.iter().filter(|text| text.contains(query)).count(); + count as u64 + } + + /// Insert a text string into the database. + pub async fn insert(&self, text: String) -> Result<()> { + let mut guard = self.blobs.lock().await; + guard.insert(text); + Ok(()) + } +} + +/// Set the RUST_LOG env var to one of {debug,info,warn} to see logging. +fn setup_logging() { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) + .with(EnvFilter::from_default_env()) + .try_init() + .ok(); +} diff --git a/vendor/iroh/examples/transfer.rs b/vendor/iroh/examples/transfer.rs new file mode 100644 index 0000000..061081e --- /dev/null +++ b/vendor/iroh/examples/transfer.rs @@ -0,0 +1,1372 @@ +//! Transfer data between two endpoints and print various stats and metrics. +//! +//! This example implements a transfer protocol to upload or download data between two iroh endpoints +//! with a time or size limit. After the transfer finishes, statistics about the transfer and the used +//! network paths are printed. +//! +//! It is not the typical "simple" example, yet it may be interesting to read because it uses most of +//! iroh's endpoint builder options. We use it for manual testing before release, and it also runs as +//! part of our CI infrastructure. +//! +//! You can use this example to easily test iroh connectivity between devices. Usage is straightforward: +//! +//! ```sh +//! # Run in release mode and with all features and print available commands and options: +//! cargo run --example transfer --release --all-features -- help +//! +//! # Run a provider endpoint on a device +//! cargo run --example transfer --release --all-features -- provide +//! +//! # And connect to the provider endpoint from another device +//! cargo run --example transfer --release --all-features -- fetch PROVIDER_ENDPOINT_ID +//! ``` + +use std::{ + fmt, + fs::File, + net::{SocketAddr, SocketAddrV4, SocketAddrV6}, + path::{Path, PathBuf}, + str::FromStr, + time::{Duration, Instant}, +}; + +use bytes::Bytes; +use chrono::Local; +use clap::{Parser, Subcommand, ValueEnum}; +use console::Style; +use data_encoding::HEXLOWER; +use derive_more::{Display, From}; +use indicatif::HumanBytes; +use ipnet::{Ipv4Net, Ipv6Net}; +use iroh::{ + Endpoint, EndpointAddr, EndpointId, RelayMap, RelayMode, RelayUrl, SecretKey, TransportAddr, + address_lookup::{ + AddrFilter, + dns::DnsAddressLookup, + pkarr::{N0_DNS_PKARR_RELAY_PROD, N0_DNS_PKARR_RELAY_STAGING, PkarrPublisher}, + }, + dns::{DnsResolver, N0_DNS_ENDPOINT_ORIGIN_PROD, N0_DNS_ENDPOINT_ORIGIN_STAGING}, + endpoint::{ + BindOpts, Connection, ConnectionError, PathEvent, PathId, QuicTransportConfig, RecvStream, + SendStream, VarInt, WriteError, presets, + }, +}; +use n0_error::{Result, StackResultExt, StdResultExt, anyerr, ensure_any}; +use n0_future::StreamExt; +use postcard::experimental::max_size::MaxSize; +use serde::{Deserialize, Serialize, Serializer}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + task::JoinHandle, + time::timeout, +}; +use tracing::{Instrument, Span, debug, error, info, info_span, instrument, warn}; +use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; +use url::Url; + +/// ALPN of our transport protocol. +const TRANSFER_ALPN: &[u8] = b"n0/iroh/transfer/example/1"; + +const DEV_RELAY_URL: &str = "http://localhost:3340"; +const DEV_PKARR_RELAY_URL: &str = "http://localhost:8080/pkarr"; +const DEV_DNS_ORIGIN_DOMAIN: &str = "irohdns.example"; +const DEV_DNS_SERVER: &str = "127.0.0.1:5300"; + +/// Connection error code for a gracefully closed connection. +const GRACEFUL_CLOSE: VarInt = VarInt::from_u32(1); + +/// Transfer data between iroh endpoints. +/// +/// This is a useful example to test connection establishment and transfer speed. +/// +/// Note that some options are only available with optional features: +/// +/// --relay-only needs the `test-utils` feature +/// +/// --dev needs the `test-utils` feature +/// +/// --mdns needs the `mdns` feature +/// +/// To emit qlog files, enable the `qlog` feature and set the QLOGDIR +/// environment variable to the path where qlog files should be written to. +/// +/// To enable all features, run the example with --all-features: +/// +/// cargo run --release --example transfer --all-features -- ARGS +#[derive(Parser, Debug)] +#[command(name = "transfer")] +struct Cli { + /// Output format. + #[clap(global = true, long, value_enum, default_value_t)] + output: OutputMode, + #[clap(flatten)] + log: LogArgs, + #[command(subcommand)] + command: Commands, +} + +#[derive(Parser, Debug)] +struct LogArgs { + /// Save trace and qlog logs to ./logs/ + #[clap(global = true, long, conflicts_with = "logs_path")] + logs: bool, + /// Save trace and qlog logs the specified path + #[clap(global = true, long, conflicts_with = "logs")] + logs_path: Option, + /// Generate qlog logs. Needs --logs or --logs_path. + /// + /// Alternatively, set the QLOGDIR environment variable. + /// + /// Has no effect if the `qlog` feature is not enabled. + #[clap(global = true, long)] + qlog: bool, +} + +impl LogArgs { + fn init(self, command: &Commands, id: EndpointId) -> Result> { + let dir = match (self.logs_path, self.logs) { + (Some(path), _) => Some(path), + (_, true) => Some(PathBuf::from(format!( + "./logs/transfer-{command}-{}-{}", + Local::now().format("%y%m%d.%H%M%S"), + id.fmt_short() + ))), + _ => None, + }; + if let Some(dir) = dir { + std::fs::create_dir_all(&dir) + .with_context(|_| format!("failed to create log directory at {}", dir.display()))?; + let tracing_file = dir.join(format!("logs-{command}")); + init_tracing(Some(&tracing_file)); + Ok(Some(LogSettings { + dir, + #[cfg(feature = "qlog")] + qlog: self.qlog, + })) + } else { + init_tracing(None); + Ok(None) + } + } +} + +struct LogSettings { + dir: PathBuf, + #[cfg(feature = "qlog")] + qlog: bool, +} + +#[derive(Clone, Copy, Default, Debug, Eq, PartialEq, clap::ValueEnum, Serialize)] +enum Env { + /// Use the production servers hosted by number0. + Prod, + /// Use the staging servers hosted by number0. + #[default] + Staging, + /// Use localhost servers. + /// + /// To run the DNS server: + /// cargo run --bin iroh-dns-server + /// To run the relay server: + /// cargo run --bin iroh-relay --features server -- --dev + Dev, +} + +impl Env { + fn relay_mode(self) -> RelayMode { + match self { + Env::Prod => RelayMode::Default, + Env::Staging => RelayMode::Staging, + Env::Dev => RelayMode::Custom(RelayMap::from( + RelayUrl::from_str(DEV_RELAY_URL).expect("valid url"), + )), + } + } + + fn pkarr_relay_url(self) -> Url { + match self { + Env::Prod => N0_DNS_PKARR_RELAY_PROD.parse(), + Env::Staging => N0_DNS_PKARR_RELAY_STAGING.parse(), + Env::Dev => DEV_PKARR_RELAY_URL.parse(), + } + .expect("valid url") + } + + fn dns_origin_domain(self) -> String { + match self { + Env::Prod => N0_DNS_ENDPOINT_ORIGIN_PROD.to_string(), + Env::Staging => N0_DNS_ENDPOINT_ORIGIN_STAGING.to_string(), + Env::Dev => DEV_DNS_ORIGIN_DOMAIN.to_string(), + } + } +} + +#[derive(Serialize, Deserialize, ValueEnum, Default, Debug, Clone, Copy)] +enum Mode { + /// We send data to the remote, measuring our upload speed. + Upload, + /// We receive data from the remote, measuring our download speed. + #[default] + Download, + /// We send and receive data in parallel. + Bidi, + /// We keep the connection open without sending data. + Ping, +} + +#[derive(Serialize, Deserialize, MaxSize, derive_more::Debug, Clone, Copy)] +enum Length { + #[debug("Size({})", HumanBytes(*_0))] + Size(u64), + #[debug("Duration({_0:?})")] + Duration(#[serde(with = "duration_micros")] Duration), +} + +impl Length { + fn remaining(&self, start: Instant, size: usize) -> (Duration, usize) { + match self { + Length::Duration(limit) => (limit.saturating_sub(start.elapsed()), usize::MAX), + Length::Size(limit) => (Duration::MAX, (*limit as usize).saturating_sub(size)), + } + } +} + +#[derive(Debug, Serialize, Clone, Copy)] +enum RequestKind { + Upload, + Download, +} + +#[derive(Serialize, Deserialize, MaxSize, Debug, Clone)] +enum Request { + Download(Length), + Upload, +} + +impl Request { + async fn read(recv: &mut RecvStream) -> Result { + let header_len = recv.read_u32().await.anyerr()? as usize; + ensure_any!( + header_len <= Self::POSTCARD_MAX_SIZE, + "received invalid header length" + ); + let mut buf = vec![0u8; header_len]; + recv.read_exact(&mut buf).await.anyerr()?; + let request = postcard::from_bytes(&buf).std_context("failed to decode request")?; + debug!("received request {request:?}"); + Ok(request) + } + + async fn write(&self, send: &mut SendStream) -> Result<()> { + debug!("sending request {self:?}"); + let buf = postcard::to_stdvec(&self).unwrap(); + send.write_u32(buf.len() as u32).await.anyerr()?; + send.write_all(&buf).await.anyerr()?; + Ok(()) + } +} + +#[derive(Debug, Clone, clap::Parser, Serialize)] +#[serde(tag = "kind")] +struct EndpointArgs { + /// Set the environment for relay, pkarr, and DNS servers. + /// + /// If other options are set, those will override the environment defaults. + #[clap(short, long, value_enum, default_value_t)] + env: Env, + /// Set one or more relay servers to use. + #[clap(long)] + relay_url: Vec, + /// Authorization token sent to each `--relay-url`. + /// + /// Has no effect unless `--relay-url` is also set. + #[clap(long)] + relay_auth_token: Option, + /// Disable relays completely. + #[clap(long, conflicts_with = "relay_url")] + no_relay: bool, + /// Disable Address Lookup completely. + #[clap(long, conflicts_with_all = ["pkarr_relay_url", "no_pkarr_publish", "dns_origin_domain", "no_dns_resolve"])] + no_address_lookup: bool, + /// If set no direct connections will be established. + #[clap(long)] + relay_only: bool, + /// Use a custom pkarr server. + #[clap(long)] + pkarr_relay_url: Option, + /// Disable publishing endpoint info to pkarr. + #[clap(long, conflicts_with = "pkarr_relay_url")] + no_pkarr_publish: bool, + /// Use a custom domain when resolving endpoint info via DNS. + #[clap(long)] + dns_origin_domain: Option, + /// Use a custom DNS server for resolving relay and endpoint info domains. + #[clap(long)] + dns_server: Option, + /// Do not resolve endpoint info via DNS. + #[clap(long)] + no_dns_resolve: bool, + #[clap(long)] + /// Enable mDNS Address Lookup. + mdns: bool, + /// Set the default IPv4 bind address. + #[clap(long)] + bind_addr_v4: Option, + /// Set additional IPv4 bind addresses. + /// + /// Syntax is "addr/mask:port", so e.g. "10.0.0.1/16:1234". + /// The mask is used to define for which destinations this bind address is used. + #[clap(long)] + bind_addr_v4_additional: Vec, + /// Set the default IPv6 bind address. + #[clap(long)] + bind_addr_v6: Option, + /// Set additional IPv6 bind addresses. + /// + /// Syntax is "addr/mask:port", so e.g. "2001:db8::1/16:1234". + /// The mask is used to define for which destinations this bind address is used. + #[clap(long)] + bind_addr_v6_additional: Vec, + /// Disable all default bind addresses. + #[clap(long)] + no_default_bind: bool, + /// Receive window size. + /// + /// This controls the maximum amount of data that may be inflight for any stream in the connection. + /// Increasing this value gives higher throughput at high latencies, at the cost of + /// more memory usage. + /// + /// The receive window is usually calculated as the product of desired throughput + /// and expected maximum roundtrip latency (RTT), i.e. for a throughput of 100 MBit/s + /// and a RTT of 200ms, you would set the receive window to (100 Mbit/s / 8) * 0.2s = 2.5M. + /// + /// The default is 1.25 MB, which gives a max throughput of 100 MBit/s at 100ms RTT. + /// + /// Accepts values like "5M", "2000K", etc. + #[clap(long, value_parser = parse_byte_size)] + receive_window: Option, +} + +#[derive(Subcommand, Debug, derive_more::Display)] +enum Commands { + /// Provide data. + #[display("provide")] + Provide { + #[clap(flatten)] + endpoint_args: EndpointArgs, + }, + /// Fetch data. + #[display("fetch")] + Fetch { + /// Endpoint id of the remote to connect to. + remote_id: EndpointId, + /// Transfer mode. + #[clap(long, value_enum, default_value_t)] + mode: Mode, + /// Limit the transferred data size. + #[clap(long, value_parser = parse_byte_size, conflicts_with = "duration")] + size: Option, + /// Limit the duration of the transfer, in seconds. + /// + /// [default: 10] + #[clap(long, conflicts_with = "size")] + duration: Option, + /// Optionally set a relay URL for the remote. + #[clap(long)] + remote_relay_url: Option, + /// Optionally set direct addresses for the remote. + #[clap(long)] + remote_direct_address: Vec, + #[clap(flatten)] + endpoint_args: EndpointArgs, + }, +} + +/// How long we maximally wait for a clean shutdown +const SHUTDOWN_TIME: Duration = Duration::from_secs(4); + +#[tokio::main] +async fn main() -> Result<()> { + let Cli { + command, + output, + log, + } = Cli::parse(); + + let output = Output::new(output); + + // Create secret key if not set. + let secret_key = match std::env::var("IROH_SECRET") { + Ok(s) => SecretKey::from_str(&s) + .context("Failed to parse IROH_SECRET environment variable as iroh secret key")?, + Err(_) => { + let s = SecretKey::generate(); + output.emit(SecretGenerated { + secret_key: HEXLOWER.encode(&s.to_bytes()), + }); + s + } + }; + + // Determine file logging path and init tracing subscriber. + let log = log.init(&command, secret_key.public())?; + let endpoint_args = match &command { + Commands::Provide { endpoint_args } => endpoint_args, + Commands::Fetch { endpoint_args, .. } => endpoint_args, + }; + output.emit_if_json(&endpoint_args); + let endpoint = endpoint_args + .clone() + .bind_endpoint(secret_key, output, log.as_ref()) + .await?; + + match run_command(command, &endpoint, output).await { + Ok(()) => (), + Err(err) => { + error!(?err, "run_command failed"); + eprintln!("{err:#}"); + } + } + + close_endpoint_with_timeout(&endpoint, output).await; + + if let Some(log) = log { + output.emit(LogsSaved { + path: log.dir.clone(), + }); + } + + Ok(()) +} + +async fn run_command(command: Commands, endpoint: &Endpoint, output: Output) -> Result<()> { + match command { + Commands::Provide { endpoint_args: _ } => provide(endpoint, output).await?, + Commands::Fetch { + remote_id, + remote_relay_url, + remote_direct_address, + endpoint_args: _, + mode, + size, + duration, + } => { + let length = match (size, duration) { + (Some(size), None) => Length::Size(size), + (None, Some(duration)) => Length::Duration(Duration::from_secs(duration)), + (None, None) => Length::Duration(Duration::from_secs(10)), + (Some(_), Some(_)) => unreachable!("--size and --duration args are conflicting"), + }; + let addrs = remote_relay_url + .into_iter() + .map(TransportAddr::Relay) + .chain(remote_direct_address.into_iter().map(TransportAddr::Ip)); + let remote_addr = EndpointAddr::from_parts(remote_id, addrs); + fetch(endpoint, remote_addr, length, mode, output).await? + } + } + Ok(()) +} + +impl EndpointArgs { + async fn bind_endpoint( + self, + secret_key: SecretKey, + output: Output, + log: Option<&LogSettings>, + ) -> Result { + let mut builder = Endpoint::builder(presets::Minimal); + if self.no_relay { + // nothing to do + } else if !self.relay_url.is_empty() { + let token = self.relay_auth_token.clone(); + let mut relay_map = RelayMap::from_iter(self.relay_url); + if let Some(ref token) = token { + relay_map = relay_map.with_auth_token(token); + } + builder = builder.relay_mode(RelayMode::Custom(relay_map)); + } else { + builder = builder.relay_mode(self.env.relay_mode()); + }; + builder = builder.secret_key(secret_key); + if self.no_relay { + builder = builder.addr_filter(AddrFilter::ip_only()); + } else { + builder = builder.addr_filter(AddrFilter::relay_only()); + } + + if Env::Dev == self.env { + #[cfg(feature = "test-utils")] + { + builder = builder.ca_tls_config(iroh::tls::CaTlsConfig::insecure_skip_verify()); + } + #[cfg(not(feature = "test-utils"))] + { + n0_error::bail_any!( + "Must have the `test-utils` feature enabled when using the `--env=dev` flag" + ) + } + } + + if !self.no_address_lookup { + if !self.no_pkarr_publish { + let url = self + .pkarr_relay_url + .unwrap_or_else(|| self.env.pkarr_relay_url()); + builder = builder.address_lookup(PkarrPublisher::builder(url)); + } + + if !self.no_dns_resolve { + let domain = self + .dns_origin_domain + .unwrap_or_else(|| self.env.dns_origin_domain()); + builder = builder.address_lookup(DnsAddressLookup::builder(domain)); + } + } + + if let Some(host) = self.dns_server { + let addr = tokio::net::lookup_host(host) + .await + .std_context("Failed to resolve DNS server address")? + .next() + .std_context("Failed to resolve DNS server address")?; + builder = builder.dns_resolver(DnsResolver::with_nameserver(addr)); + } else if self.env == Env::Dev { + let addr = DEV_DNS_SERVER.parse().expect("valid addr"); + builder = builder.dns_resolver(DnsResolver::with_nameserver(addr)); + } + + if self.relay_only || self.no_default_bind { + builder = builder.clear_ip_transports(); + } + + if let Some(addr) = self.bind_addr_v4 { + builder = builder.bind_addr(addr)?; + } + for addr in self.bind_addr_v4_additional { + let (addr, prefix_len) = parse_ipv4_net(&addr) + .with_context(|_| format!("invalid bind-addr-v4-additional: {addr}"))?; + builder = builder + .bind_addr_with_opts(addr, BindOpts::default().set_prefix_len(prefix_len))?; + } + + if let Some(addr) = self.bind_addr_v6 { + builder = builder.bind_addr(addr)?; + } + for addr in self.bind_addr_v6_additional { + let (addr, prefix_len) = parse_ipv6_net(&addr) + .with_context(|_| format!("invalid bind-addr-v6-additional: {addr}"))?; + builder = builder + .bind_addr_with_opts(addr, BindOpts::default().set_prefix_len(prefix_len))?; + } + + let mut cfg = QuicTransportConfig::builder(); + + // Adjust the receive window, if configured. + // By default noq and iroh set the connection-level receive window to VarInt::MAX, and + // the stream receive window to 1.25 MB (for a throughput of 100 MBit/s at 100ms latency). + // We leave the connection-level receive window at the default and adjust the + // stream-level receive window. The distinction between connection-level and stream-level + // doesn't matter here because we don't have concurrent data-intensive streams + // in the transfer protocol. + // We also set the send_window to 2 * stream_receive_window. By default, noq sets this + // to 8 * 1.25MB. The transfer protocol doesn't use concurrent data streams, so 2 times + // our receive window is enough. + if let Some(size) = self.receive_window { + cfg = cfg.stream_receive_window(size.into()); + cfg = cfg.send_window(size as u64 * 2); + } + + #[cfg(feature = "qlog")] + match (std::env::var("QLOGDIR").ok(), log) { + (Some(dir), _) => cfg = cfg.qlog_from_path(dir, "transfer"), + (_, Some(log)) if log.qlog => cfg = cfg.qlog_from_path(&log.dir, ""), + _ => {} + } + #[cfg(not(feature = "qlog"))] + let _ = log; + + builder = builder.transport_config(cfg.build()); + + let endpoint = builder.alpns(vec![TRANSFER_ALPN.to_vec()]).bind().await?; + + if self.mdns { + n0_error::bail_any!( + "mDNS address lookup is no longer built into iroh; use the `iroh-mdns-address-lookup` crate instead", + ); + } + + if self.relay_only { + endpoint.online().await; + } else if !self.no_relay { + timeout(Duration::from_secs(3), endpoint.online()) + .await + .ok(); + } + + let endpoint_addr = endpoint.addr(); + output.emit(EndpointBound { + endpoint_id: endpoint.id(), + direct_addresses: endpoint_addr.ip_addrs().copied().collect(), + relay_url: endpoint_addr.relay_urls().next().cloned(), + }); + + Ok(endpoint) + } +} + +async fn provide(endpoint: &Endpoint, output: Output) -> Result<()> { + for id in 0.. { + // Accept incoming connections until Ctrl-C is pressed. + let incoming = tokio::select! { + Some(incoming) = endpoint.accept() => incoming, + _ = tokio::signal::ctrl_c() => break, + else => break + }; + // Spawn a task for each connection. + tokio::spawn( + async move { + let accepting = match incoming.accept() { + Ok(accepting) => accepting, + Err(err) => { + warn!("incoming connection failed: {err:#}"); + // we can carry on in these cases: + // this can be caused by retransmitted datagrams + return; + } + }; + match accepting.await { + Ok(conn) => { + info!(remote = %conn.remote_id().fmt_short(), "connection accepted"); + output.emit_with_remote(conn.remote_id(), ConnectionAccepted { id }); + handle_connection(conn, output).await; + } + Err(err) => warn!("incoming connection failed during handshake: {err:#}"), + } + } + .instrument(info_span!("accept", id, remote = tracing::field::Empty)), + ); + } + + Ok(()) +} + +async fn handle_connection(conn: Connection, output: Output) { + let start = Instant::now(); + let remote_id = conn.remote_id(); + // Spawn a background task that prints connection type changes and collects stats of all paths. + let stats_task = spawn_path_watcher(conn.clone(), Some(remote_id), output); + + // Accept incoming streams in a loop until the connection is closed by the remote. + let close_reason = loop { + let (send, recv) = match conn.accept_bi().await { + Ok(streams) => streams, + Err(err) => break err, + }; + tokio::spawn( + async move { + if let Err(err) = handle_request(remote_id, send, recv, output).await { + warn!("[{}] Request failed: {err:#}", remote_id.fmt_short()); + } + } + .instrument(Span::current()), + ); + }; + + let is_graceful = matches!( + &close_reason, + ConnectionError::ApplicationClosed(f) if f.error_code == GRACEFUL_CLOSE + ); + let error = (!is_graceful).then(|| format!("{close_reason:#}")); + info!(?error, "connection closed"); + output.emit_with_remote( + remote_id, + ConnectionClosed { + error, + duration: start.elapsed(), + }, + ); + stats_task.await.expect("stats task panicked"); +} + +#[instrument("handle", skip_all, fields(id=send.id().index()))] +async fn handle_request( + remote_id: EndpointId, + send: SendStream, + mut recv: RecvStream, + output: Output, +) -> Result<()> { + let request = Request::read(&mut recv) + .await + .context("failed to read request")?; + output.emit_with_remote(remote_id, HandleRequest { request: &request }); + match request { + Request::Download(length) => { + let stats = send_data(send, recv, length).await?; + output.emit_with_remote(remote_id, UploadComplete { stats }); + } + Request::Upload => { + let stats = drain_stream(recv, send, None).await?; + output.emit_with_remote(remote_id, DownloadComplete { stats }); + } + } + Ok(()) +} + +async fn fetch( + endpoint: &Endpoint, + remote_addr: EndpointAddr, + length: Length, + mode: Mode, + output: Output, +) -> Result<()> { + // Attempt to connect, over the given ALPN. Returns a connection. + let start = Instant::now(); + let conn = endpoint.connect(remote_addr, TRANSFER_ALPN).await?; + let remote_id = conn.remote_id(); + output.emit(Connected { + remote_id, + duration: start.elapsed(), + }); + // Spawn a background task that prints connection type changes and collects stats of all paths. + let stats_task = spawn_path_watcher(conn.clone(), None, output); + + output.emit(StartRequest { mode, length }); + // Perform requests depending on the request mode. + let request_fut = async { + match mode { + Mode::Upload => { + perform_request(&conn, RequestKind::Upload, length, start, output).await? + } + Mode::Download => { + perform_request(&conn, RequestKind::Download, length, start, output).await? + } + Mode::Bidi => { + tokio::try_join!( + perform_request(&conn, RequestKind::Download, length, start, output), + perform_request(&conn, RequestKind::Upload, length, start, output), + )?; + } + Mode::Ping => { + let Length::Duration(duration) = length else { + n0_error::bail_any!("--mode ping needs --duration to be set") + }; + tokio::time::sleep(duration).await; + } + } + // We finished our requests. Close the connection with our graceful error code. + conn.close(GRACEFUL_CLOSE, b"done"); + n0_error::Ok(()) + }; + + // Wait for the request to complete, or for the user to interrupt it with Ctrl-C + let res = tokio::select! { + res = request_fut => res, + _ = tokio::signal::ctrl_c() => Err(anyerr!("Cancelled")) + }; + + // If the connection hasn't been closed above, close it now. + if conn.close_reason().is_none() { + conn.close(0u32.into(), b"shutdown"); + } + + let error = conn + .close_reason() + .filter(|reason| !matches!(reason, ConnectionError::LocallyClosed)) + .map(|reason| format!("{reason:#}")); + output.emit(ConnectionClosed { + error, + duration: start.elapsed(), + }); + + stats_task.await.expect("stats task panicked"); + res +} + +/// Close the endpoint, with a timeout, and emit emit once done. +async fn close_endpoint_with_timeout(endpoint: &Endpoint, output: Output) { + let shutdown_start = Instant::now(); + let timed_out = timeout(SHUTDOWN_TIME, endpoint.close()).await.is_err(); + + output.emit(EndpointClosed { + duration: shutdown_start.elapsed(), + timed_out, + }); +} + +#[instrument("request", skip_all, fields(id = tracing::field::Empty))] +async fn perform_request( + conn: &Connection, + request_kind: RequestKind, + length: Length, + conn_start: Instant, + output: Output, +) -> Result<()> { + let (mut send, recv) = conn.open_bi().await.anyerr()?; + Span::current().record("id", send.id().index()); + match request_kind { + RequestKind::Download => { + Request::Download(length).write(&mut send).await?; + let stats = drain_stream(recv, send, Some(conn_start)).await?; + output.emit(DownloadComplete { stats }); + } + RequestKind::Upload => { + Request::Upload.write(&mut send).await?; + let stats = send_data(send, recv, length).await?; + output.emit(UploadComplete { stats }); + } + } + Ok(()) +} + +/// Drain `recv`, and once done finish `send`. +/// +/// We use [`SendStream::finish`] as a confirmation once we fully read [`RecvStream`]. The remote will wait +/// for this event and not close the connection earlire. +#[instrument("drain_stream", skip_all)] +async fn drain_stream( + mut recv: RecvStream, + mut send: SendStream, + started_at: Option, +) -> Result { + debug!("start"); + let start = Instant::now(); + let mut read = 0; + let mut num_chunks: u64 = 0; + let mut time_to_first_byte = None; + + // These are 32 buffers, for reading approximately 32kB at once + let mut bufs: [Bytes; 32] = std::array::from_fn(|_| Bytes::new()); + + while let Some(n) = recv.read_many_chunks(&mut bufs[..]).await.anyerr()? { + // Update time to first byte if still empty and started_at is set. + if let (None, Some(started_at)) = (time_to_first_byte, started_at) { + time_to_first_byte = Some(started_at.elapsed()); + } + read += bufs.iter().take(n).map(Bytes::len).sum::(); + num_chunks += 1; + } + + send.finish().anyerr()?; + + let stats = DownloadStats { + size: read as u64, + time_to_first_byte, + num_chunks, + duration: start.elapsed(), + }; + debug!(?stats, "done"); + Ok(stats) +} + +/// Send data on `send` for `length`, afterwards wait for `recv` to be closed. +/// +/// When done sending, we wait for [`RecvStream`] to be closed. The remote will finish its corresponding +/// send stream once it has read all our data. This ensures that we don't close the connection before the remote +/// has fully read our data. +#[instrument("send_data", skip_all)] +async fn send_data( + mut send: SendStream, + mut recv: RecvStream, + length: Length, +) -> Result { + debug!(?length, "start"); + const DATA: &[u8] = &[0xAB; 1024 * 1024]; + let data = Bytes::from_static(DATA); + + let start = Instant::now(); + let mut total = 0; + loop { + let (remaining_time, remaining_size) = length.remaining(start, total); + let chunk = if remaining_size == 0 || remaining_time == Duration::ZERO { + break; + } else if remaining_size < data.len() { + data.slice(..remaining_size) + } else { + data.clone() + }; + total += write_chunk_timeout(&mut send, chunk, remaining_time) + .await + .std_context("failed to send data")?; + } + + send.finish().std_context("failed to finish stream")?; + + debug!("sending finished, wait for confirmation"); + recv.read_to_end(0).await.anyerr()?; + + let stats = UploadStats { + size: total as u64, + duration: start.elapsed(), + }; + debug!(?stats, "done"); + Ok(stats) +} + +/// Writes as much of [`Bytes`] to a [`SendStream`] as possible within `timeout`. +/// +/// Completes once `chunk` is fully written or after `timeout` elapses, whatever comes first. +/// +/// Returns the number of bytes written. +async fn write_chunk_timeout( + send: &mut SendStream, + chunk: Bytes, + timeout: Duration, +) -> Result { + // This follows the pattern of [`SendStream::write_all_chunks`] but with a timeout applied. + let timeout = tokio::time::sleep(timeout); + tokio::pin!(timeout); + let mut bufs = &mut [chunk][..]; + let mut total = 0; + while !bufs.is_empty() { + tokio::select! { + _ = &mut timeout => break, + written = send.write_many_chunks(&mut bufs) => { + total += written?; + } + } + } + Ok(total) +} + +fn parse_byte_size(s: &str) -> std::result::Result { + let cfg = parse_size::Config::new().with_binary(); + cfg.parse_size(s) +} + +fn spawn_path_watcher( + conn: Connection, + remote_id: Option, + output: Output, +) -> JoinHandle<()> { + let print = move |conn: &Connection| { + let path = match conn.paths().iter().find(|p| p.is_selected()) { + Some(p) => SelectedPath::Selected { + id: p.id(), + addr: p.remote_addr().clone(), + rtt: p.rtt(), + }, + None => SelectedPath::None, + }; + output.emit_maybe_remote(remote_id, ConnectionTypeChanged { path }); + }; + tokio::spawn(async move { + let mut events = conn.path_events(); + print(&conn); + let mut paths = Vec::new(); + while let Some(event) = events.next().await { + match event { + PathEvent::Selected { .. } => print(&conn), + PathEvent::Closed { + id, + remote_addr, + last_stats, + .. + } => { + paths.push(PathData { + id, + remote_addr, + rtt: last_stats.rtt, + bytes_sent: last_stats.udp_tx.bytes, + bytes_recv: last_stats.udp_rx.bytes, + }); + } + _ => {} + } + } + output.emit_maybe_remote(remote_id, PathStats { paths }); + }) +} + +fn parse_ipv4_net(s: &str) -> Result<(SocketAddrV4, u8)> { + let (net, port) = s.split_once(":").std_context("missing colon")?; + let net: Ipv4Net = net.parse().std_context("invalid net")?; + let port: u16 = port.parse().std_context("invalid port")?; + + Ok((SocketAddrV4::new(net.addr(), port), net.prefix_len())) +} + +fn parse_ipv6_net(s: &str) -> Result<(SocketAddrV6, u8)> { + let (net, port) = s.rsplit_once(":").std_context("missing colon")?; + let net: Ipv6Net = net.parse().std_context("invalid net")?; + let port: u16 = port.parse().std_context("invalid port")?; + Ok((SocketAddrV6::new(net.addr(), port, 0, 0), net.prefix_len())) +} + +#[derive(ValueEnum, Default, Debug, Clone, Copy)] +enum OutputMode { + /// Print human-readable text. + #[default] + Text, + /// Print newline-delimited JSON. + Json, +} + +#[derive(Debug, Clone, Copy)] +struct Output { + mode: OutputMode, + start: Instant, +} + +impl Output { + fn new(mode: OutputMode) -> Self { + Self { + mode, + start: Instant::now(), + } + } + + fn time(&self) -> impl fmt::Display { + Style::new() + .dim() + .italic() + .apply_to(format!("{:>6.3}s", self.start.elapsed().as_secs_f32())) + } + + fn emit(&self, event: impl Serialize + fmt::Display) { + info!("{event}"); + match self.mode { + OutputMode::Text => println!("{event} {}", self.time()), + OutputMode::Json => println!( + "{}", + serde_json::to_string(&Timestamped::now(event)).unwrap() + ), + } + } + + fn emit_with_remote(&self, remote: EndpointId, event: impl Serialize + fmt::Display) { + info!(remote=%remote.fmt_short(), "{event}"); + match self.mode { + OutputMode::Text => println!( + "{} {event} {}", + Style::new() + .dim() + .apply_to(format!("[{}]", remote.fmt_short())), + self.time() + ), + OutputMode::Json => println!( + "{}", + serde_json::to_string(&Timestamped::now(RemoteEvent::new(remote, event))).unwrap() + ), + } + } + + fn emit_maybe_remote(&self, remote: Option, event: impl Serialize + fmt::Display) { + match remote { + None => self.emit(event), + Some(remote) => self.emit_with_remote(remote, event), + } + } + + fn emit_if_json(&self, event: &impl Serialize) { + if matches!(self.mode, OutputMode::Json) { + println!( + "{}", + serde_json::to_string(&Timestamped::now(event)).unwrap() + ) + } + } +} + +#[derive(Serialize, Debug, Clone, Display)] +#[display("Generated a new endpoint secret. To reuse, set\n\tIROH_SECRET={secret_key}")] +#[serde(tag = "kind")] +struct SecretGenerated { + secret_key: String, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "kind")] +struct EndpointBound { + endpoint_id: EndpointId, + direct_addresses: Vec, + relay_url: Option, +} + +impl fmt::Display for EndpointBound { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Our endpoint id:\n\t{}", self.endpoint_id)?; + writeln!(f, "Our direct addresses:")?; + for addr in &self.direct_addresses { + writeln!(f, "\t{addr}")?; + } + match &self.relay_url { + Some(url) => write!(f, "Our home relay server:\t{url}")?, + None => write!(f, "No home relay server found")?, + } + Ok(()) + } +} + +#[derive(Serialize, Debug, Clone, Display)] +#[serde(tag = "kind")] +#[display("Connection type changed to {path}")] +struct ConnectionTypeChanged { + #[serde(flatten)] + path: SelectedPath, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "status")] +enum SelectedPath { + Selected { + #[serde(skip)] + id: PathId, + addr: TransportAddr, + #[serde(with = "duration_micros")] + rtt: Duration, + }, + None, +} + +impl fmt::Display for SelectedPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Selected { addr, rtt, id } => { + write!(f, "{addr:?} [id:{id}] (RTT: {})", fmt_duration(*rtt)) + } + Self::None => write!(f, "none"), + } + } +} + +#[derive(Serialize, Debug, Clone, Display)] +#[serde(tag = "kind")] +#[display("Connected to {remote_id} in {}", fmt_duration(*duration))] +struct Connected { + remote_id: EndpointId, + #[serde(with = "duration_micros")] + duration: Duration, +} + +#[derive(Serialize, Debug, Clone, Display)] +#[serde(tag = "kind")] +#[display("Starting {mode:?} request with {length:?}")] +struct StartRequest { + mode: Mode, + length: Length, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "kind")] +struct EndpointClosed { + #[serde(with = "duration_micros")] + duration: Duration, + timed_out: bool, +} + +impl fmt::Display for EndpointClosed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let duration = fmt_duration(self.duration); + match self.timed_out { + false => write!(f, "Shutdown took {duration}"), + true => write!(f, "Shutdown timed out after {duration}",), + } + } +} + +#[derive(Serialize, Debug, Clone)] +struct PathData { + #[serde(skip)] + id: PathId, + remote_addr: TransportAddr, + #[serde(with = "duration_micros")] + rtt: Duration, + bytes_sent: u64, + bytes_recv: u64, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "kind")] +struct PathStats { + paths: Vec, +} + +impl fmt::Display for PathStats { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Path stats:")?; + for path in &self.paths { + write!( + f, + "\n\t[{:>2}] {:?}: RTT {}, tx={}, rx={}", + path.id, + path.remote_addr, + fmt_duration(path.rtt), + path.bytes_sent, + path.bytes_recv, + )?; + } + Ok(()) + } +} + +#[derive(Serialize, Debug, Clone, Display)] +#[serde(tag = "kind")] +#[display("{stats}")] +struct DownloadComplete { + #[serde(flatten)] + stats: DownloadStats, +} + +#[derive(Serialize, Debug, Clone, Display)] +#[serde(tag = "kind")] +#[display("{stats}")] +struct UploadComplete { + #[serde(flatten)] + stats: UploadStats, +} + +#[derive(Serialize, Debug, Clone, Copy, Display)] +#[display("Accepted connection (trace id: {id})")] +#[serde(tag = "kind")] +struct ConnectionAccepted { + id: u64, +} + +#[derive(Serialize, Debug, Clone, Display)] +#[serde(tag = "kind")] +#[display("Handling {request:?} request")] +struct HandleRequest<'a> { + request: &'a Request, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "kind")] +struct ConnectionClosed { + #[serde(with = "duration_micros")] + duration: Duration, + error: Option, +} + +impl fmt::Display for ConnectionClosed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let time = format!("(total time: {})", fmt_duration(self.duration)); + match &self.error { + Some(err) => write!(f, "Connection closed with error: {err} {time}"), + None => write!(f, "Connection closed {time}",), + } + } +} + +#[derive(Serialize, Debug, Clone, Display)] +#[display( + "Downloaded: {:>10} in {}, {:>10}/s ({}{} chunks)", + HumanBytes(self.size).to_string(), + fmt_duration(self.duration), + HumanBytes((self.size as f64 / self.duration.as_secs_f64()) as u64), + self.time_to_first_byte + .map(|t| format!("time to first byte {}, ", fmt_duration(t))) + .unwrap_or_default(), + self.num_chunks +)] +struct DownloadStats { + size: u64, + #[serde( + serialize_with = "duration_micros_opt", + skip_serializing_if = "Option::is_none" + )] + time_to_first_byte: Option, + num_chunks: u64, + #[serde(with = "duration_micros")] + duration: Duration, +} + +#[derive(Serialize, Debug, Clone, Display)] +#[display( + "Uploaded: {:>10} in {}, {:>10}/s", + HumanBytes(self.size).to_string(), + fmt_duration(self.duration), + HumanBytes((self.size as f64 / self.duration.as_secs_f64()) as u64) +)] +struct UploadStats { + size: u64, + #[serde(with = "duration_micros")] + duration: Duration, +} + +#[derive(Serialize, Debug, Clone, Display)] +#[display("Logs saved to {}", path.display())] +struct LogsSaved { + path: PathBuf, +} + +#[derive(Serialize, Debug, Clone, From, Display)] +#[display("[{}] {inner}", remote_id.fmt_short())] +struct RemoteEvent { + #[serde(flatten)] + inner: T, + remote_id: EndpointId, +} + +impl RemoteEvent { + fn new(remote_id: EndpointId, inner: T) -> Self { + Self { remote_id, inner } + } +} + +#[derive(Serialize)] +struct Timestamped { + timestamp: String, + #[serde(flatten)] + inner: T, +} + +impl Timestamped { + fn now(inner: T) -> Self { + Self { + timestamp: chrono::Utc::now().to_rfc3339(), + inner, + } + } +} + +fn duration_micros_opt( + value: &Option, + serializer: S, +) -> Result { + match value { + Some(d) => serializer.serialize_u64(d.as_micros() as u64), + None => serializer.serialize_none(), + } +} + +mod duration_micros { + use std::time::Duration; + + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(duration: &Duration, serializer: S) -> Result { + serializer.serialize_u64(duration.as_micros() as u64) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let millis = u64::deserialize(deserializer)?; + Ok(Duration::from_micros(millis)) + } +} + +pub fn init_tracing(path: Option<&Path>) { + let layer = tracing_subscriber::fmt::layer().with_line_number(true); + if let Some(path) = path { + let file = File::create(path).expect("failed to create trace log file"); + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("iroh=trace,transfer=trace,noq=trace")); + let layer = layer.with_writer(file).with_filter(filter); + tracing_subscriber::registry().with(layer).init() + } else { + let layer = layer + .with_writer(std::io::stderr) + .with_filter(EnvFilter::from_default_env()); + tracing_subscriber::registry().with(layer).init() + } +} + +fn fmt_duration(d: Duration) -> impl fmt::Display { + if d > Duration::from_secs(1) { + format!("{:.2}s", d.as_secs_f32()) + } else if d > Duration::from_millis(1) { + format!("{}ms", d.as_millis()) + } else { + format!("{}µs", d.as_micros()) + } +} diff --git a/vendor/iroh/release.toml b/vendor/iroh/release.toml new file mode 100644 index 0000000..cedd20e --- /dev/null +++ b/vendor/iroh/release.toml @@ -0,0 +1 @@ +pre-release-hook = ["git", "cliff", "--repository", "../", "--unreleased", "--prepend", "../CHANGELOG.md", "--tag", "{{version}}" ] \ No newline at end of file diff --git a/vendor/iroh/src/address_lookup.rs b/vendor/iroh/src/address_lookup.rs new file mode 100644 index 0000000..e3ddb71 --- /dev/null +++ b/vendor/iroh/src/address_lookup.rs @@ -0,0 +1,1483 @@ +//! Lookup the address of an Endpoint ID. +//! +//! To connect to an iroh endpoint a [`EndpointAddr`] is needed, which may contain a +//! [`RelayUrl`] or one or more *direct addresses* in addition to the [`EndpointId`]. +//! +//! Since there is a conversion from [`EndpointId`] to [`EndpointAddr`], you can also +//! connect directly with a [`EndpointId`]. +//! +//! For this to work however, the endpoint has to get the addressing information by +//! other means. +//! +//! [`AddressLookup`] is an automated system for an [`Endpoint`] to retrieve this addressing +//! information. Each iroh endpoint will automatically publish their own addressing +//! information. Usually this means publishing which [`RelayUrl`] to use for their +//! [`EndpointId`], but they could also publish their direct addresses. +//! +//! The [`AddressLookup`] trait is used to define an address lookup system. This allows multiple +//! implementations to co-exist because there are many possible ways to implement this. +//! Each [`Endpoint`] can use the address lookup mechanisms most suitable to the application. +//! The [`Builder::address_lookup`] method is used to add an address lookup mechanism to an +//! [`Endpoint`]. +//! +//! Each address lookup service receives the full set of transport addresses when publishing, +//! but may only publish a subset of them based on its own constraints. +//! +//! To control which addresses are published to a particular service, you can supply an +//! [`AddrFilter`] on its builder (e.g. [`PkarrPublisherBuilder::addr_filter`]). The filter +//! receives the full set of addresses and returns an ordered [`Vec`], allowing you to both +//! remove addresses you don't want published and prioritize the ones you do. Each service +//! may apply additional filtering on top based on its own constraints, but will not publish +//! addresses outside of what the filter returns. See each service's documentation for details. +//! +//! Some generally useful Address Lookup implementations are provided: +//! +//! - [`MemoryLookup`] which allows application to add and remove out-of-band addressing +//! information. +//! +//! - The [`address_lookup::DnsAddressLookup`] which performs lookups via the standard DNS systems. To publish +//! to this DNS server a [`PkarrPublisher`] is needed. [Number 0] runs a public instance +//! of a [`PkarrPublisher`] with attached DNS server which is globally available and a +//! reliable default choice. +//! +//! - The [`PkarrResolver`] which can perform lookups from designated [pkarr relay servers] +//! using HTTP. +//! +//! mDNS-based and Mainline-DHT-based Address Lookup services live in +//! separate crates: [`iroh-mdns-address-lookup`] and +//! [`iroh-mainline-address-lookup`]. +//! +//! [`iroh-mdns-address-lookup`]: https://docs.rs/iroh-mdns-address-lookup +//! [`iroh-mainline-address-lookup`]: https://docs.rs/iroh-mainline-address-lookup +//! +//! To use multiple Address Lookups simultaneously you can call [`Builder::address_lookup`]. +//! This will use [`AddressLookupServices`] under the hood, which performs lookups to all +//! Address Lookup systems at the same time. +//! +//! [`Builder::address_lookup`] takes any type that implements [`AddressLookupBuilder`]. You can +//! implement that trait on a builder struct if your Address Lookup needs information +//! from the endpoint it is mounted on. After endpoint construction, your Address Lookup +//! is built by calling [`AddressLookupBuilder::into_address_lookup`], passing the finished [`Endpoint`] to your +//! builder. +//! +//! If your Address Lookup does not need any information from its endpoint, you can +//! pass the Address Lookup service directly to [`Builder::address_lookup`]: All types that +//! implement [`AddressLookup`] also have a blanket implementation of [`AddressLookupBuilder`]. +//! +//! # Examples +//! +//! A very common setup is to enable DNS Address Lookup, which needs to be done in two parts as a +//! [`PkarrPublisher`] and [`address_lookup::DnsAddressLookup`]: +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # { +//! use iroh::{ +//! Endpoint, SecretKey, +//! address_lookup::{self, AddrFilter, PkarrPublisher}, +//! endpoint::{RelayMode, presets}, +//! }; +//! +//! # async fn wrapper() -> n0_error::Result<()> { +//! let ep = Endpoint::builder(presets::Minimal) +//! .addr_filter(AddrFilter::relay_only()) +//! .address_lookup(PkarrPublisher::n0_dns()) +//! .address_lookup(address_lookup::DnsAddressLookup::n0_dns()) +//! .bind() +//! .await?; +//! # Ok(()) +//! # } +//! # } +//! ``` +//! +//! [`EndpointAddr`]: iroh_base::EndpointAddr +//! [`RelayUrl`]: crate::RelayUrl +//! [`Builder::address_lookup`]: crate::endpoint::Builder::address_lookup +//! [`address_lookup::DnsAddressLookup`]: crate::address_lookup::DnsAddressLookup +//! [Number 0]: https://n0.computer +//! [`PkarrResolver`]: pkarr::PkarrResolver +//! [`PkarrPublisher`]: pkarr::PkarrPublisher +//! [`PkarrPublisherBuilder::addr_filter`]: pkarr::PkarrPublisherBuilder::addr_filter +//! [pkarr relay servers]: https://pkarr.org/#servers +//! [`MemoryLookup`]: memory::MemoryLookup + +use std::{ + borrow::{Borrow, Cow}, + pin::Pin, + sync::{Arc, RwLock}, + task::{Poll, ready}, +}; + +use iroh_base::{EndpointAddr, EndpointId}; +pub use iroh_dns::{ParseError, endpoint_info::AddrFilter}; +use n0_error::{AnyError, e, stack_error}; +use n0_future::{MergeBounded, Stream, boxed::BoxStream}; +use tracing::debug; + +pub use crate::endpoint_info::{EndpointData, EndpointInfo, UserData}; +use crate::{Endpoint, endpoint::EndpointError}; + +#[cfg(not(wasm_browser))] +pub mod dns; +pub mod memory; +mod metrics; +pub mod pkarr; + +#[cfg(not(wasm_browser))] +pub use dns::*; +pub use memory::*; +pub use metrics::{Metrics, ServiceLabels}; +pub use pkarr::*; + +/// Trait for structs that can be converted into [`AddressLookup`]s. +/// +/// This trait is implemented on builders for Address Lookup's. Any type that implements this +/// trait can be added as a Address Lookup in [`Builder::address_lookup`]. +/// +/// Any type that implements [`AddressLookup`] also implements [`AddressLookupBuilder`]. +/// +/// Iroh uses this trait to allow configuring the set of address lookup services on +/// the endpoint builder, while also providing them access to information about the +/// endpoint to [`AddressLookupBuilder::into_address_lookup`]. +/// +/// [`Builder::address_lookup`]: crate::endpoint::Builder::address_lookup +pub trait AddressLookupBuilder: Send + Sync + std::fmt::Debug + 'static { + /// Turns this builder into a ready-to-use [`AddressLookup`]. + /// + /// If an error is returned, building the endpoint will fail with this error. + fn into_address_lookup( + self, + endpoint: &Endpoint, + ) -> Result; +} + +/// An [`AddressLookup`] wrapper that filters addresses before publishing. +#[derive(Debug, Clone)] +pub struct FilteredAddressLookup { + inner: T, + filter: AddrFilter, +} + +impl FilteredAddressLookup { + /// Wraps an address lookup with an address filter. + /// + /// The filter allows to specify which addresses the address + /// lookup service will publish. + pub fn new(inner: T, filter: AddrFilter) -> Self { + Self { inner, filter } + } + + /// Removes the filter wrapper and returns the inner address lookup. + pub fn into_inner(self) -> T { + self.inner + } +} + +impl AsRef for FilteredAddressLookup { + fn as_ref(&self) -> &T { + &self.inner + } +} + +impl AddressLookup for FilteredAddressLookup { + fn publish(&self, data: &EndpointData) { + let data = data.apply_filter(&self.filter); + self.inner.publish(data.borrow()); + } + + fn resolve(&self, endpoint_id: EndpointId) -> Option>> { + self.inner.resolve(endpoint_id) + } +} + +/// Blanket no-op impl of `AddressLookupBuilder` for `T: AddressLookup`. +impl AddressLookupBuilder for T { + fn into_address_lookup( + self, + _endpoint: &Endpoint, + ) -> Result { + Ok(self) + } +} + +/// Non-public dyn-compatible version of [`AddressLookupBuilder`], used in [`crate::endpoint::Builder`]. +pub(crate) trait DynAddressLookupBuilder: Send + Sync + std::fmt::Debug + 'static { + /// See [`AddressLookupBuilder::into_address_lookup`] + fn into_address_lookup( + self: Box, + endpoint: &Endpoint, + ) -> Result, AddressLookupBuilderError>; +} + +impl DynAddressLookupBuilder for T { + fn into_address_lookup( + self: Box, + endpoint: &Endpoint, + ) -> Result, AddressLookupBuilderError> { + let addr_lookup: Box = + Box::new(AddressLookupBuilder::into_address_lookup(*self, endpoint)?); + Ok(addr_lookup) + } +} + +/// [`AddressLookupBuilder`] errors +#[allow(missing_docs)] +#[stack_error(derive, add_meta, from_sources, std_sources)] +#[non_exhaustive] +pub enum AddressLookupBuilderError { + #[error("Service '{provenance}' error")] + User { + provenance: &'static str, + source: AnyError, + }, + #[error(transparent)] + EndpointClosed { source: EndpointError }, +} + +impl AddressLookupBuilderError { + /// Creates a new user error from an arbitrary error type. + pub fn from_err( + provenance: &'static str, + source: T, + ) -> Self { + e!(AddressLookupBuilderError::User { + provenance, + source: AnyError::from_std(source) + }) + } + + /// Creates a new user error from an arbitrary boxed error type. + pub fn from_err_box( + provenance: &'static str, + source: Box, + ) -> Self { + e!(AddressLookupBuilderError::User { + provenance, + source: AnyError::from_std_box(source) + }) + } +} + +/// [`AddressLookup`] errors +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +#[derive(Clone)] +pub enum AddressLookupFailed { + #[error("No address lookup configured")] + NoServiceConfigured, + #[error( + "All address lookup services failed or produced no results{}", + { + let errors = errors.iter().map(|err| format!("{err:#}")).collect::>(); + if !errors.is_empty() { + format!("\n {}", errors.join("\n ")) + } else { + String::new() + } + } + )] + NoResults { errors: Vec }, +} + +/// Error returned by address lookup services when failing to perform the lookup. +#[stack_error(derive, add_meta)] +#[error("Service '{provenance}' failed")] +#[derive(Clone)] +pub struct Error { + provenance: &'static str, + #[error(source)] + source: Arc, +} + +impl Error { + /// Creates a new user error from an arbitrary error type. + #[track_caller] + pub fn from_err( + provenance: &'static str, + source: T, + ) -> Self { + Self::from_err_any(provenance, AnyError::from_std(source)) + } + + /// Creates a new user error from an arbitrary boxed error type. + #[track_caller] + pub fn from_err_box( + provenance: &'static str, + source: Box, + ) -> Self { + Self::from_err_any(provenance, AnyError::from_std_box(source)) + } + + /// Creates a new user error from an arbitrary error type that can be converted into [`AnyError`]. + #[track_caller] + pub fn from_err_any(provenance: &'static str, source: impl Into) -> Self { + Self::new(provenance, Arc::new(source.into())) + } +} + +/// AddressLookup system for [`super::Endpoint`]. +/// +/// This trait defines publishing and resolving addressing information for a [`EndpointId`]. +/// This enables connecting to other endpoints with only knowing the [`EndpointId`], by using this +/// [`AddressLookup`] system to look up the actual addressing information. It is common for +/// implementations to require each endpoint to publish their own information before it can be +/// looked up by other endpoints. +/// +/// The published addressing information can include both a [`RelayUrl`] and/or direct +/// addresses. See [`EndpointData`] for details. +/// +/// To allow for Address Lookup, the [`super::Endpoint`] will call `publish` whenever +/// Address Lookup information changes. If an Address Lookup mechanism requires a periodic +/// refresh, it should start its own task. +/// +/// [`RelayUrl`]: crate::RelayUrl +pub trait AddressLookup: std::fmt::Debug + Send + Sync + 'static { + /// Publishes the given [`EndpointData`] to the Address Lookup mechanism. + /// + /// This is fire and forget, since the [`Endpoint`] can not wait for successful + /// publishing. If publishing is async, the implementation should start its own task. + /// + /// This will be called from a tokio task, so it is safe to spawn new tasks. + /// These tasks will be run on the runtime of the [`super::Endpoint`]. + fn publish(&self, _data: &EndpointData) {} + + /// Resolves the [`Item`] for the given [`EndpointId`]. + /// + /// Once the returned [`BoxStream`] is dropped, the service should stop any pending + /// work. + fn resolve(&self, _endpoint_id: EndpointId) -> Option>> { + None + } +} + +impl AddressLookup for Arc { + fn publish(&self, data: &EndpointData) { + self.as_ref().publish(data); + } + + fn resolve(&self, endpoint_id: EndpointId) -> Option>> { + self.as_ref().resolve(endpoint_id) + } +} + +/// Address lookup results from [`AddressLookup`]s. +/// +/// This is the item in the streams returned from [`AddressLookup::resolve`]. +/// It contains the [`EndpointData`] about the resolved endpoint addresses, +/// and some additional metadata about the address lookup system. +/// +/// This struct derefs to [`EndpointData`], so you can access the methods from [`EndpointData`] +/// directly from [`Item`]. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Item { + /// The endpoint info for the endpoint, as discovered by the Address Lookup. + endpoint_info: EndpointInfo, + /// A static string to identify the Address Lookup source. + /// + /// Should be uniform per Address Lookup. + provenance: &'static str, + /// Optional timestamp when this endpoint address info was last updated. + /// + /// Must be microseconds since the unix epoch. + // TODO(ramfox): this is currently unused. As we develop more `AddressLookup`s, we may discover that we do not need this. It is only truly relevant when comparing `relay_urls`, since we can attempt to dial any number of socket addresses, but expect each endpoint to have one "home relay" that we will attempt to contact them on. This means we would need some way to determine which relay url to choose between, if more than one relay url is reported. + last_updated: Option, +} + +impl Item { + /// Creates a new [`Item`] from a [`EndpointInfo`]. + pub fn new( + endpoint_info: EndpointInfo, + provenance: &'static str, + last_updated: Option, + ) -> Self { + Self { + endpoint_info, + provenance, + last_updated, + } + } + + /// Returns the endpoint id of the discovered endpoint. + pub fn endpoint_id(&self) -> EndpointId { + self.endpoint_info.endpoint_id + } + + /// Returns the [`EndpointInfo`] for the discovered endpoint. + pub fn endpoint_info(&self) -> &EndpointInfo { + &self.endpoint_info + } + + /// Returns the provenance of this Address Lookup item. + /// + /// The provenance is a static string which identifies the Address Lookup service that produced + /// this item. + pub fn provenance(&self) -> &'static str { + self.provenance + } + + /// Returns the optional timestamp when this endpoint info was last updated. + /// + /// The value is microseconds since the unix epoch. + pub fn last_updated(&self) -> Option { + self.last_updated + } + + /// Returns an [`EndpointAddr`] by cloning the needed fields. + pub fn to_endpoint_addr(&self) -> EndpointAddr { + self.endpoint_info.to_endpoint_addr() + } + + /// Converts into an [`EndpointAddr`] without cloning. + pub fn into_endpoint_addr(self) -> EndpointAddr { + self.endpoint_info.into_endpoint_addr() + } + + /// Returns any user-defined data. + pub fn user_data(&self) -> Option { + self.endpoint_info().data.user_data().cloned() + } +} + +impl std::ops::Deref for Item { + type Target = EndpointData; + fn deref(&self) -> &Self::Target { + &self.endpoint_info.data + } +} + +impl From for EndpointInfo { + fn from(item: Item) -> Self { + item.endpoint_info + } +} + +/// Registry for address lookup services for an [`Endpoint`]. +/// +/// The endpoint will use this registry both for publishing its own [`EndpointInfo`] +/// and for resolving addresses of remote endpoints. +/// +/// See [`AddressLookup`] and [`Self::resolve`] for details. +/// +/// [`Endpoint`]: crate::Endpoint +#[derive(Debug, Default, Clone)] +pub struct AddressLookupServices { + services: Arc>>>, + /// The data last published, used to publish when adding a new service. + last_data: Arc>>, + /// Optional filter applied to all data before publishing to any service. + addr_filter: Arc>>, + /// Metrics for lookup outcomes. + metrics: Arc, +} + +impl AddressLookupServices { + /// Creates a registry recording lookup outcomes in `metrics`. + pub(crate) fn with_metrics(metrics: Arc) -> Self { + Self { + metrics, + ..Default::default() + } + } + + /// Sets the address filter applied before publishing to any service. + /// + /// When set, all address data is filtered once before being distributed + /// to the individual address lookup services. This ensures consistent + /// filtering regardless of how many services are configured. + pub fn set_addr_filter(&self, filter: AddrFilter) { + *self.addr_filter.write().expect("poisoned") = Some(filter); + } + + /// Adds an [`AddressLookup`] service. + /// + /// If there is historical Address Lookup data, it will be published immediately on this service. + pub fn add(&self, service: impl AddressLookup + 'static) { + self.add_boxed(Box::new(service)) + } + + /// Adds an already `Box`ed [`AddressLookup`] service. + /// + /// If there is historical Address Lookup data, it will be published immediately on this service. + pub fn add_boxed(&self, service: Box) { + { + let data = self.last_data.read().expect("poisoned"); + if let Some(data) = &*data { + service.publish(data) + } + } + self.services.write().expect("poisoned").push(service); + } + + /// Are there any services configured? + pub fn is_empty(&self) -> bool { + self.services.read().expect("poisoned").is_empty() + } + + /// Returns the number of services configured. + pub fn len(&self) -> usize { + self.services.read().expect("poisoned").len() + } + + /// Removes all configured services. + pub fn clear(&self) { + let mut services = self.services.write().expect("poisoned"); + services.clear(); + } + + /// Publish endpoint data on all configured services. + pub(crate) fn publish(&self, data: &EndpointData) { + let data = match &*self.addr_filter.read().expect("poisoned") { + Some(filter) => data.apply_filter(filter), + None => Cow::Borrowed(data), + }; + let services = self.services.read().expect("poisoned"); + for service in &*services { + service.publish(&data); + } + + self.last_data + .write() + .expect("poisoned") + .replace(data.into_owned()); + } + + /// Resolves the addressing information for an [`EndpointId`] across all configured services. + /// + /// All services are queried concurrently and their results are merged into a + /// single stream. Each [`Item`] is yielded as `Ok(Ok(item))` as soon as it + /// is produced, allowing the caller to act on the first usable address + /// while slower services are still working. + /// + /// Errors from individual services are yielded inline as `Ok(Err(error))` + /// and do not terminate the stream: a single failing service must not hide + /// results from others still in flight. Inline errors are also buffered; + /// if all services finish without yielding any [`Item`], the stream ends + /// with a single [`AddressLookupFailed::NoResults`] carrying the buffered + /// errors. If at least one [`Item`] was yielded, buffered errors are + /// discarded and the stream ends with `None`. + /// + /// If no services are configured, the stream yields a single + /// [`AddressLookupFailed::NoServiceConfigured`] error and then ends. + /// + /// Dropping the returned stream signals all underlying services to stop any + /// pending work, as documented on [`AddressLookup::resolve`]. + pub fn resolve( + &self, + endpoint_id: EndpointId, + ) -> impl Stream, AddressLookupFailed>> + use<> { + self.metrics.lookups.inc(); + let services = self.services.read().expect("poisoned"); + if services.is_empty() { + AddressLookupStream::empty(self.metrics.clone()) + } else { + let streams = services + .iter() + .filter_map(|service| service.resolve(endpoint_id)); + AddressLookupStream::new(streams, self.metrics.clone()) + } + } +} + +/// Stream returned by [`AddressLookupServices::resolve`]. +/// +/// Merges the per-service result streams. Yields each successful item as +/// `Ok(Ok(item))` as soon as it is produced, and each per-service error +/// inline as `Ok(Err(error))`, so a single failing service cannot hide +/// results from others still in flight. Inline errors are also buffered. +/// +/// Once all services are done, the stream ends with `None` if at least one +/// item was yielded; otherwise it yields a single +/// [`AddressLookupFailed::NoResults`] carrying all buffered errors, then ends. +/// +/// If no services are configured, the stream yields a single +/// [`AddressLookupFailed::NoServiceConfigured`] error, then ends. +struct AddressLookupStream { + streams: Option>>>, + errors: Vec, + did_emit: bool, + closed: bool, + metrics: Arc, +} + +impl AddressLookupStream { + fn empty(metrics: Arc) -> Self { + Self { + streams: None, + errors: Vec::new(), + did_emit: false, + closed: false, + metrics, + } + } + + fn new( + streams: impl Iterator>>, + metrics: Arc, + ) -> Self { + Self { + streams: Some(MergeBounded::from_iter(streams)), + errors: Vec::new(), + did_emit: false, + closed: false, + metrics, + } + } +} + +impl Stream for AddressLookupStream { + type Item = Result, AddressLookupFailed>; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + if this.closed { + return Poll::Ready(None); + } + let mut inner = match this.streams.as_mut() { + Some(inner) => inner, + None => { + this.closed = true; + this.metrics.lookups_failed.inc(); + return Poll::Ready(Some(Err(e!(AddressLookupFailed::NoServiceConfigured)))); + } + }; + let item = match ready!(Pin::new(&mut inner).poll_next(cx)) { + Some(Ok(item)) => { + this.did_emit = true; + this.metrics + .service_results + .get_or_create(&ServiceLabels::new(item.provenance())) + .inc(); + Some(Ok(Ok(item))) + } + Some(Err(error)) => { + debug!("address lookup error: {error:#}"); + this.metrics + .service_errors + .get_or_create(&ServiceLabels::new(error.provenance)) + .inc(); + this.errors.push(error.clone()); + Some(Ok(Err(error))) + } + None => { + this.closed = true; + if !this.did_emit { + this.metrics.lookups_failed.inc(); + let errors = std::mem::take(&mut this.errors); + Some(Err(e!(AddressLookupFailed::NoResults { errors }))) + } else { + None + } + } + }; + Poll::Ready(item) + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex}, + time::{Duration, SystemTime}, + }; + + use iroh_base::{EndpointAddr, SecretKey, TransportAddr}; + use n0_error::{AnyError, Result, StackResultExt}; + use n0_future::{StreamExt, time}; + use n0_tracing_test::traced_test; + use rand::{CryptoRng, RngExt, SeedableRng}; + use tokio_util::task::AbortOnDropHandle; + + use super::*; + use crate::{ + Endpoint, + endpoint::{ConnectOptions, IdleTimeout, QuicTransportConfig, presets}, + }; + + type InfoStore = HashMap; + + #[derive(Debug, Clone, Default)] + struct TestAddressLookupShared { + endpoints: Arc>, + } + + impl TestAddressLookupShared { + fn create_address_lookup(&self, endpoint_id: EndpointId) -> TestAddressLookup { + TestAddressLookup { + endpoint_id, + shared: self.clone(), + publish: true, + resolve_wrong: false, + delay: Duration::from_millis(200), + } + } + + fn create_lying_address_lookup(&self, endpoint_id: EndpointId) -> TestAddressLookup { + TestAddressLookup { + endpoint_id, + shared: self.clone(), + publish: false, + resolve_wrong: true, + delay: Duration::from_millis(100), + } + } + } + + #[derive(Debug)] + struct TestAddressLookup { + endpoint_id: EndpointId, + shared: TestAddressLookupShared, + publish: bool, + resolve_wrong: bool, + delay: Duration, + } + + impl AddressLookup for TestAddressLookup { + fn publish(&self, data: &EndpointData) { + if !self.publish { + return; + } + let now = system_time_now(); + self.shared + .endpoints + .lock() + .unwrap() + .insert(self.endpoint_id, (data.clone(), now)); + } + + fn resolve(&self, endpoint_id: EndpointId) -> Option>> { + let addr_info = if self.resolve_wrong { + let ts = system_time_now() - 100_000; + let port: u16 = rand::rng().random_range(10_000..20_000); + // "240.0.0.0/4" is reserved and unreachable + let addr: SocketAddr = format!("240.0.0.1:{port}").parse().unwrap(); + let data = EndpointData::from_iter([TransportAddr::Ip(addr)]); + Some((data, ts)) + } else { + self.shared + .endpoints + .lock() + .unwrap() + .get(&endpoint_id) + .cloned() + }; + let stream = match addr_info { + Some((data, ts)) => { + let item = Item::new( + EndpointInfo::from_parts(endpoint_id, data), + "test-addr-lookup", + Some(ts), + ); + let delay = self.delay; + let fut = async move { + time::sleep(delay).await; + tracing::debug!("resolve: {} = {item:?}", endpoint_id.fmt_short()); + Ok(item) + }; + n0_future::stream::once_future(fut).boxed() + } + None => n0_future::stream::empty().boxed(), + }; + Some(stream) + } + } + + #[derive(Debug, Clone)] + struct EmptyAddressLookup; + + impl AddressLookup for EmptyAddressLookup { + fn publish(&self, _data: &EndpointData) {} + + fn resolve(&self, _endpoint_id: EndpointId) -> Option>> { + Some(n0_future::stream::empty().boxed()) + } + } + + /// An address lookup that yields a single `Err(_)` after `delay` and then ends. + /// + /// Used to reproduce the issue where one resolver's error truncates the merged + /// stream of a [`AddressLookupServices`], hiding subsequent successful results + /// from other resolvers. + #[derive(Debug, Clone)] + struct FailingAddressLookup { + delay: Duration, + } + + impl AddressLookup for FailingAddressLookup { + fn publish(&self, _data: &EndpointData) {} + + fn resolve(&self, _endpoint_id: EndpointId) -> Option>> { + let delay = self.delay; + let fut = async move { + time::sleep(delay).await; + Err(Error::from_err( + "failing-test", + std::io::Error::other("simulated resolver failure"), + )) + }; + Some(n0_future::stream::once_future(fut).boxed()) + } + } + + /// An address lookup whose `resolve` stream never yields and never ends. + #[derive(Debug, Clone)] + struct HangingAddressLookup; + + impl AddressLookup for HangingAddressLookup { + fn publish(&self, _data: &EndpointData) {} + + fn resolve(&self, _endpoint_id: EndpointId) -> Option>> { + Some(n0_future::stream::pending().boxed()) + } + } + + const TEST_ALPN: &[u8] = b"n0/iroh/test"; + + /// This is a smoke test for our Address Lookupmechanism. + #[tokio::test] + #[traced_test] + async fn address_lookup_simple_shared() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + + let eir_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = + new_endpoint(&mut rng, |ep| eir_shared.create_address_lookup(ep.id())).await; + + let (ep2, _guard2) = + new_endpoint(&mut rng, |ep| eir_shared.create_address_lookup(ep.id())).await; + let ep1_addr = EndpointAddr::new(ep1.id()); + let _conn = ep2.connect(ep1_addr, TEST_ALPN).await?; + Ok(()) + } + + /// This is a smoke test to ensure a Address Lookup can be + /// `Arc`-d, and Address Lookup will still work + #[tokio::test] + #[traced_test] + async fn address_lookup_simple_shared_with_arc() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let address_lookup_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = new_endpoint(&mut rng, |ep| { + Arc::new(address_lookup_shared.create_address_lookup(ep.id())) + }) + .await; + + let (ep2, _guard2) = new_endpoint(&mut rng, |ep| { + Arc::new(address_lookup_shared.create_address_lookup(ep.id())) + }) + .await; + let ep1_addr = EndpointAddr::new(ep1.id()); + let _conn = ep2.connect(ep1_addr, TEST_ALPN).await?; + Ok(()) + } + + /// This test adds an empty Address Lookupwhich provides no addresses. + #[tokio::test] + #[traced_test] + async fn address_lookup_combined_with_empty_and_right() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let address_lookup_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = new_endpoint(&mut rng, |ep| { + address_lookup_shared.create_address_lookup(ep.id()) + }) + .await; + let (ep2, _guard2) = new_endpoint_add(&mut rng, |ep| { + let addr_lookup1 = EmptyAddressLookup; + let addr_lookup2 = address_lookup_shared.create_address_lookup(ep.id()); + ep.address_lookup() + .expect("endpoint is still open") + .add(addr_lookup1); + ep.address_lookup() + .expect("endpoint is still open") + .add(addr_lookup2); + }) + .await; + + let ep1_addr = EndpointAddr::new(ep1.id()); + + assert_eq!( + ep2.address_lookup().expect("endpoint is still open").len(), + 2 + ); + let _conn = ep2 + .connect(ep1_addr, TEST_ALPN) + .await + .context("connecting")?; + Ok(()) + } + + /// This test adds a "lying" address_lookup service which provides a wrong address. + /// This is to make sure that as long as one of the services returns a working address, we + /// will connect successfully. + #[tokio::test] + #[traced_test] + async fn address_lookup_combined_with_empty_and_wrong() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let address_lookup_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = new_endpoint(&mut rng, |ep| { + address_lookup_shared.create_address_lookup(ep.id()) + }) + .await; + + let (ep2, _guard2) = new_endpoint_add(&mut rng, |ep| { + let address_lookup1 = EmptyAddressLookup; + let address_lookup2 = address_lookup_shared.create_lying_address_lookup(ep.id()); + let address_lookup3 = address_lookup_shared.create_address_lookup(ep.id()); + let address_lookup = ep.address_lookup().unwrap(); + address_lookup.add(address_lookup1); + address_lookup.add(address_lookup2); + address_lookup.add(address_lookup3); + }) + .await; + + let _conn = ep2.connect(ep1.id(), TEST_ALPN).await?; + Ok(()) + } + + /// Regression test for https://github.com/n0-computer/iroh/issues/4125 + /// + /// When one resolver in a [`AddressLookupServices`] returns `Err(_)` early, + /// the merged stream is currently replaced with `pending()`, hiding any later + /// `Ok(_)` results from other resolvers. This test verifies that a slow but + /// successful resolver still delivers its result after a fast failing one errors. + #[tokio::test] + #[traced_test] + async fn address_lookup_succeeds_after_other_resolver_errors() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let address_lookup_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = new_endpoint_add(&mut rng, |ep| { + ep.address_lookup() + .unwrap() + .add(address_lookup_shared.create_address_lookup(ep.id())); + }) + .await; + + let (ep2, _guard2) = new_endpoint_add(&mut rng, |ep| { + // Failing resolver errors first (50ms), working resolver delivers later (200ms). + // Without the fix, the error from the failing resolver terminates the stream + // before the working resolver's result arrives, so connect times out. + let failing = FailingAddressLookup { + delay: Duration::from_millis(50), + }; + // This lookup has a delay of 200ms (hardcoded). + let working = address_lookup_shared.create_address_lookup(ep.id()); + let address_lookup = ep.address_lookup().unwrap(); + address_lookup.add(failing); + address_lookup.add(working); + }) + .await; + + let _conn = ep2 + .connect(ep1.id(), TEST_ALPN) + .await + .context("connect after other resolver errored")?; + Ok(()) + } + + /// Regression test: a hanging address lookup for one peer must not block + /// concurrent `connect()` calls to other peers. + /// + /// `Socket::handle_actor_message` used to `.await` the per-remote + /// `RemoteStateActor` reply when handling `ResolveRemote`, so a slow or + /// hanging lookup would serialise every other `connect()` behind it via the + /// single socket actor. This tests that we do not serialize here anymore. + #[tokio::test(flavor = "current_thread", start_paused = true)] + #[traced_test] + async fn concurrent_connects_not_blocked_by_slow_lookup() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let shared = TestAddressLookupShared::default(); + + let (ep_server, _guard_server) = + new_endpoint(&mut rng, |ep| shared.create_address_lookup(ep.id())).await; + + // Client uses a working lookup (resolves the server) plus a hanging + // lookup. For unknown ids the merged stream pends forever, because + // the working lookup ends immediately with no item while the hanging + // one never closes. + let (ep_client, _guard_client) = new_endpoint_add(&mut rng, |ep| { + let lookup = ep.address_lookup().unwrap(); + lookup.add(shared.create_address_lookup(ep.id())); + lookup.add(HangingAddressLookup); + }) + .await; + + // One connect to an unknown id; the socket actor will `.await` its + // hanging `ResolveRemote` forever. + let offline_id = SecretKey::from_bytes(&rng.random()).public(); + let _offline_connect = AbortOnDropHandle::new(tokio::spawn({ + let ep = ep_client.clone(); + async move { + let _ = ep.connect(offline_id, TEST_ALPN).await; + } + })); + // Wait until that `ResolveRemote` is in flight in the socket actor. + time::sleep(Duration::from_millis(200)).await; + + // With the bug this connect is queued behind the hanging one and + // never makes progress; with the fix it completes promptly. + let _conn = time::timeout( + Duration::from_secs(1), + ep_client.connect(ep_server.id(), TEST_ALPN), + ) + .await + .expect("online connect blocked behind hanging lookup")?; + Ok(()) + } + + /// Regression test: Pending address lookup must keep the `RemoteStateActor` alive. + /// + /// Previously, this test failed with an `InternalConsistencyError` because the + /// `RemoteStateActor` was marked idle and shut down while there were pending + /// `ResolveRemote` requests still queued. + #[tokio::test(flavor = "current_thread", start_paused = true)] + #[traced_test] + async fn pending_resolve_survives_actor_idle_timeout() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let ep = Endpoint::builder(presets::Minimal) + .secret_key(SecretKey::from_bytes(&rng.random())) + .bind() + .await?; + ep.address_lookup() + .expect("endpoint is still open") + .add(HangingAddressLookup); + + let offline_id = SecretKey::from_bytes(&rng.random()).public(); + let connect_task = tokio::spawn(async move { ep.connect(offline_id, TEST_ALPN).await }); + + // `iroh::socket::remote_map::remote_state::ACTOR_MAX_IDLE_TIMEOUT` + // is 60s. Sleep for longer so that we are sure that the idle timeout expired. + let res = time::timeout(Duration::from_secs(65), connect_task).await; + // We expect the timeout to elapse, because the address lookup does not resolve. + // Before the fix, this would produce an `InternalConsistencyError` after the actor's idle timeout expired. + assert!(res.is_err(), "expected Elapsed, got {res:?}"); + Ok(()) + } + + /// Concurrent `connect` calls to the same peer must both wait for the in-flight address lookup. + /// + /// Reproduces the race where the second `ResolveRemote` for the same + /// remote used to drain the first call's pending resolve tx with a + /// spurious `NoResults` while the address lookup was still in flight. In + /// that state the first call returned `Err(NoAddress)` immediately; the + /// second call waited for the lookup and succeeded. + #[tokio::test(flavor = "current_thread", start_paused = true)] + #[traced_test] + async fn concurrent_connects_do_not_race_on_empty_resolve() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let shared = TestAddressLookupShared::default(); + + let (ep_server, _guard_server) = + new_endpoint(&mut rng, |ep| shared.create_address_lookup(ep.id())).await; + + // Client shares the same lookup, so the server's real address is + // discoverable; the 200ms lookup delay opens the race window. + let (ep_client, _guard_client) = + new_endpoint(&mut rng, |ep| shared.create_address_lookup(ep.id())).await; + + let server_id = ep_server.id(); + let first = tokio::spawn({ + let ep = ep_client.clone(); + async move { ep.connect(server_id, TEST_ALPN).await } + }); + + // Let the first call register its resolve request and trigger the + // address lookup before the second call arrives. + time::sleep(Duration::from_millis(10)).await; + + let second = tokio::spawn({ + let ep = ep_client.clone(); + async move { ep.connect(server_id, TEST_ALPN).await } + }); + + let (first, second) = tokio::join!(first, second); + let first = first.unwrap(); + let second = second.unwrap(); + + assert!( + first.is_ok(), + "first connect must wait for lookup, got: {:?}", + first.err() + ); + assert!( + second.is_ok(), + "second connect must wait for lookup, got: {:?}", + second.err() + ); + Ok(()) + } + + /// This test only has the "lying" address lookup system. It is here to make sure that this actually fails. + #[tokio::test] + #[traced_test] + async fn address_lookup_combined_wrong_only() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + + let address_lookup_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = new_endpoint(&mut rng, |ep| { + address_lookup_shared.create_address_lookup(ep.id()) + }) + .await; + + let (ep2, _guard2) = new_endpoint_add(&mut rng, |ep| { + let address_lookup1 = address_lookup_shared.create_lying_address_lookup(ep.id()); + ep.address_lookup().unwrap().add(address_lookup1) + }) + .await; + + // 10x faster test via a 3s idle timeout instead of the 30s default + let cfg = QuicTransportConfig::builder() + .keep_alive_interval(Duration::from_secs(1)) + .max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(3)).unwrap())) + .build(); + let opts = ConnectOptions::new().with_transport_config(cfg); + + let res = ep2 + .connect_with_opts(ep1.id(), TEST_ALPN, opts) + .await? // -> Connecting works + .await; // -> Connection is expected to fail + assert!(res.is_err()); + Ok(()) + } + + /// This test first adds a wrong address manually (e.g. from an outdated&endpoint_id ticket). + /// Connect should still succeed because the address lookup service service will be invoked (after a delay). + #[tokio::test] + #[traced_test] + async fn address_lookup_with_wrong_existing_addr() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + + let address_lookup_shared = TestAddressLookupShared::default(); + let (ep1, _guard1) = new_endpoint(&mut rng, |ep| { + address_lookup_shared.create_address_lookup(ep.id()) + }) + .await; + let (ep2, _guard2) = new_endpoint(&mut rng, |ep| { + address_lookup_shared.create_address_lookup(ep.id()) + }) + .await; + + let ep1_wrong_addr = EndpointAddr::from_parts( + ep1.id(), + [TransportAddr::Ip("240.0.0.1:1000".parse().unwrap())], + ); + let _conn = ep2.connect(ep1_wrong_addr, TEST_ALPN).await?; + Ok(()) + } + + /// Lookup outcomes are counted per service. + #[tokio::test] + #[traced_test] + async fn address_lookup_metrics() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let endpoint_id = SecretKey::from_bytes(&rng.random()).public(); + + // One succeeding and one failing service. + let succeeding = MemoryLookup::with_provenance("static-test"); + let data = EndpointData::from_iter([TransportAddr::Ip("127.0.0.1:1".parse().unwrap())]); + succeeding.add_endpoint_info(EndpointInfo::from_parts(endpoint_id, data)); + let services = AddressLookupServices::default(); + services.add(succeeding); + services.add(FailingAddressLookup { + delay: Duration::from_millis(10), + }); + let _results: Vec<_> = services.resolve(endpoint_id).collect().await; + + let metrics = &services.metrics; + assert_eq!(metrics.lookups.get(), 1); + assert_eq!(metrics.lookups_failed.get(), 0); + assert_eq!( + metrics + .service_results + .get(&ServiceLabels::new("static-test")) + .map(|counter| counter.get()), + Some(1) + ); + assert_eq!( + metrics + .service_errors + .get(&ServiceLabels::new("failing-test")) + .map(|counter| counter.get()), + Some(1) + ); + + // Only failing services: the lookup itself is counted as failed. + let services = AddressLookupServices::default(); + services.add(FailingAddressLookup { + delay: Duration::from_millis(10), + }); + let _results: Vec<_> = services.resolve(endpoint_id).collect().await; + assert_eq!(services.metrics.lookups_failed.get(), 1); + + // No services configured: also counted as failed. + let services = AddressLookupServices::default(); + let _results: Vec<_> = services.resolve(endpoint_id).collect().await; + assert_eq!(services.metrics.lookups.get(), 1); + assert_eq!(services.metrics.lookups_failed.get(), 1); + + Ok(()) + } + + #[test] + fn concurrent_address_lookup_addr_filter() { + use iroh_base::RelayUrl; + + // Create a service that records what it receives. + #[derive(Debug, Clone, Default)] + struct RecordingLookup { + published: Arc>>, + } + impl AddressLookup for RecordingLookup { + fn publish(&self, data: &EndpointData) { + self.published.lock().unwrap().push(data.clone()); + } + fn resolve(&self, _endpoint_id: EndpointId) -> Option>> { + None + } + } + + let recorder = RecordingLookup::default(); + let lookup = AddressLookupServices::default(); + lookup.set_addr_filter(AddrFilter::relay_only()); + lookup.add(recorder.clone()); + + let relay_url: RelayUrl = "https://relay.example.com".parse().unwrap(); + let ip_addr: SocketAddr = "1.2.3.4:1234".parse().unwrap(); + let data = EndpointData::from_iter([ + TransportAddr::Relay(relay_url.clone()), + TransportAddr::Ip(ip_addr), + ]); + lookup.publish(&data); + + let published = recorder.published.lock().unwrap(); + assert_eq!(published.len(), 1); + let addrs: Vec<_> = published[0].addrs().cloned().collect(); + assert_eq!(addrs, vec![TransportAddr::Relay(relay_url)]); + assert!( + !addrs.contains(&TransportAddr::Ip(ip_addr)), + "IP address should have been filtered out" + ); + } + + async fn new_endpoint D>( + rng: &mut R, + create_address_lookup: F, + ) -> (Endpoint, AbortOnDropHandle>) { + new_endpoint_add(rng, |ep| { + let address_lookup = create_address_lookup(ep); + ep.address_lookup() + .expect("endpoint is still open") + .add(address_lookup); + }) + .await + } + + async fn new_endpoint_add( + rng: &mut R, + add_address_lookup: F, + ) -> (Endpoint, AbortOnDropHandle>) { + let secret = SecretKey::from_bytes(&rng.random()); + + let ep = Endpoint::builder(presets::Minimal) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await + .unwrap(); + add_address_lookup(&ep); + + let handle = tokio::spawn({ + let ep = ep.clone(); + async move { + // Keep connections alive until the task is dropped. + let mut connections = Vec::new(); + // we skip accept() errors, they can be caused by retransmits + while let Some(accepting) = ep.accept().await.and_then(|inc| inc.accept().ok()) { + // Just accept incoming connections, but don't do anything with them. + let conn = accepting.await.context("accepting")?; + connections.push(conn); + } + + Ok::<_, AnyError>(()) + } + }); + + (ep, AbortOnDropHandle::new(handle)) + } + + fn system_time_now() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("time drift") + .as_micros() as u64 + } +} + +/// This module contains end-to-end tests for DNS address lookup service. +/// +/// The tests run a minimal test DNS server to resolve against, and a minimal pkarr relay to +/// publish to. The DNS and pkarr servers share their state. +#[cfg(test)] +mod test_dns_pkarr { + use iroh_base::{EndpointAddr, SecretKey, TransportAddr}; + use iroh_dns::endpoint_info::UserData; + use iroh_relay::tls::{CaTlsConfig, default_provider}; + use n0_error::{Result, StackResultExt}; + use n0_future::time::Duration; + use n0_tracing_test::traced_test; + use rand::{RngExt, SeedableRng}; + + use crate::{ + address_lookup::{EndpointData, PkarrPublisher}, + dns::DnsResolver, + endpoint_info::EndpointInfo, + test_utils::{DnsPkarrServer, dns_server::run_dns_server, pkarr_dns_state::State}, + }; + + const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); + + #[tokio::test] + #[traced_test] + async fn dns_resolve() -> Result<()> { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let origin = "testdns.example".to_string(); + let state = State::new(origin.clone()); + let (nameserver, _dns_drop_guard) = run_dns_server(state.clone()) + .await + .context("Running DNS server")?; + + let secret_key = SecretKey::from_bytes(&rng.random()); + let endpoint_info = EndpointInfo::new(secret_key.public()) + .with_relay_url("https://relay.example".parse().unwrap()); + let signed_packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?; + state + .upsert(signed_packet) + .context("update and insert signed packet")?; + + let resolver = DnsResolver::with_nameserver(nameserver); + let resolved = resolver + .lookup_endpoint_by_id(&endpoint_info.endpoint_id, &origin) + .await?; + + assert_eq!(resolved, endpoint_info); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn pkarr_publish_dns_resolve() -> Result<()> { + let origin = "testdns.example".to_string(); + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + + let dns_pkarr_server = DnsPkarrServer::run_with_origin(origin.clone()) + .await + .context("DnsPkarrServer")?; + + let secret_key = SecretKey::from_bytes(&rng.random()); + let endpoint_id = secret_key.public(); + + let relay_url = Some(TransportAddr::Relay( + "https://relay.example".parse().unwrap(), + )); + + let tls_config = CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"); + let resolver = dns_pkarr_server.dns_resolver(); + let publisher = PkarrPublisher::builder(dns_pkarr_server.pkarr_url().clone()) + .build(secret_key, tls_config); + let user_data: UserData = "foobar".parse().unwrap(); + let data = EndpointData::from_iter(relay_url.clone()).with_user_data(user_data.clone()); + // does not block, update happens in background task + publisher.update_endpoint_data(&data); + // wait until our shared state received the update from pkarr publishing + dns_pkarr_server + .on_endpoint(&endpoint_id, PUBLISH_TIMEOUT) + .await + .context("wait for on endpoint update")?; + let resolved = resolver + .lookup_endpoint_by_id(&endpoint_id, &origin) + .await?; + println!("resolved {resolved:?}"); + + let expected_addr = EndpointAddr::from_parts(endpoint_id, relay_url); + + assert_eq!(resolved.to_endpoint_addr(), expected_addr); + assert_eq!(resolved.user_data(), Some(&user_data)); + Ok(()) + } + + #[cfg(with_crypto_provider)] + const TEST_ALPN: &[u8] = b"TEST"; + + #[cfg(with_crypto_provider)] + #[tokio::test] + #[traced_test] + async fn pkarr_publish_dns_address_lookup() -> Result<()> { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + + let dns_pkarr_server = DnsPkarrServer::run().await.context("DnsPkarrServer run")?; + let (relay_map, _relay_url, _relay_guard) = crate::test_utils::run_relay_server().await?; + + let (ep1, _guard1) = + ep_with_address_lookup(&mut rng, &relay_map, &dns_pkarr_server).await?; + let (ep2, _guard2) = + ep_with_address_lookup(&mut rng, &relay_map, &dns_pkarr_server).await?; + + // wait until our shared state received the update from pkarr publishing + dns_pkarr_server + .on_endpoint(&ep1.id(), PUBLISH_TIMEOUT) + .await + .context("wait for on endpoint update")?; + + // we connect only by endpoint id! + let _conn = ep2.connect(ep1.id(), TEST_ALPN).await?; + Ok(()) + } + + #[cfg(with_crypto_provider)] + async fn ep_with_address_lookup( + rng: &mut R, + relay_map: &iroh_relay::RelayMap, + dns_pkarr_server: &DnsPkarrServer, + ) -> Result<( + crate::Endpoint, + n0_future::task::AbortOnDropHandle>, + )> { + use n0_future::task::AbortOnDropHandle; + + use crate::{Endpoint, RelayMode, endpoint::presets}; + + let secret_key = SecretKey::from_bytes(&rng.random()); + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .secret_key(secret_key.clone()) + .alpns(vec![TEST_ALPN.to_vec()]) + .preset(dns_pkarr_server.preset()) + .bind() + .await?; + + let handle = tokio::spawn({ + let ep = ep.clone(); + async move { + // we skip accept() errors, they can be caused by retransmits + + use n0_error::AnyError; + while let Some(accepting) = ep.accept().await.and_then(|inc| inc.accept().ok()) { + let _conn = accepting.await.context("accepting")?; + // Just accept incoming connections, but don't do anything with them. + } + + Ok::<_, AnyError>(()) + } + }); + + Ok((ep, AbortOnDropHandle::new(handle))) + } +} diff --git a/vendor/iroh/src/address_lookup/dns.rs b/vendor/iroh/src/address_lookup/dns.rs new file mode 100644 index 0000000..786efae --- /dev/null +++ b/vendor/iroh/src/address_lookup/dns.rs @@ -0,0 +1,136 @@ +//! DNS endpoint discovery for iroh + +use iroh_base::EndpointId; +use iroh_dns::dns::DnsResolver; +pub use iroh_dns::dns::{N0_DNS_ENDPOINT_ORIGIN_PROD, N0_DNS_ENDPOINT_ORIGIN_STAGING}; +use n0_future::boxed::BoxStream; +use tracing::{Instrument, debug, debug_span, trace}; + +use crate::{ + Endpoint, + address_lookup::{ + AddressLookup, AddressLookupBuilder, AddressLookupBuilderError, + Error as AddressLookupError, Item as AddressLookupItem, + }, + endpoint::force_staging_infra, +}; + +/// Delays after which additional DNS lookup calls are issued. +/// +/// Each query has its own timeout of 3s. This means that a lookup will finally +/// abort after 6 seconds. +pub(crate) const DNS_STAGGERING_MS: &[u64] = &[200, 300, 600, 1000, 2000, 3000]; + +/// DNS endpoint discovery +/// +/// When asked to resolve a [`EndpointId`], this service performs a lookup in the Domain Name System (DNS). +/// +/// It uses the [`Endpoint`]'s DNS resolver to query for `TXT` records under the domain +/// `_iroh..`: +/// +/// * `_iroh`: is the record name +/// * `` is the [`EndpointId`] encoded in [`z-base-32`] format +/// * `` is the endpoint origin domain as set in [`DnsAddressLookup::builder`]. +/// +/// Each TXT record returned from the query is expected to contain a string in the format `=`. +/// If a TXT record contains multiple character strings, they are concatenated first. +/// The supported attributes are: +/// * `relay=`: The URL of the home relay server of the endpoint +/// +/// The DNS resolver defaults to using the nameservers configured on the host system, but can be changed +/// with [`crate::endpoint::Builder::dns_resolver`]. +/// +/// [`z-base-32`]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt +/// [`Endpoint`]: crate::Endpoint +#[derive(Debug)] +pub struct DnsAddressLookup { + origin_domain: String, + dns_resolver: DnsResolver, +} + +/// Builder for [`DnsAddressLookup`]. +/// +/// See [`DnsAddressLookup::builder`]. +#[derive(Debug)] +pub struct DnsAddressLookupBuilder { + origin_domain: String, + dns_resolver: Option, +} + +impl DnsAddressLookupBuilder { + /// Sets the DNS resolver to use. + pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self { + self.dns_resolver = Some(dns_resolver); + self + } + + /// Builds a [`DnsAddressLookup`] with the passed [`DnsResolver`]. + pub fn build(self) -> DnsAddressLookup { + DnsAddressLookup { + dns_resolver: self.dns_resolver.unwrap_or_default(), + origin_domain: self.origin_domain, + } + } +} + +impl DnsAddressLookup { + /// Creates a [`DnsAddressLookupBuilder`] that implements [`AddressLookupBuilder`]. + pub fn builder(origin_domain: String) -> DnsAddressLookupBuilder { + DnsAddressLookupBuilder { + origin_domain, + dns_resolver: None, + } + } + + /// Creates a new DNS address lookup using the `iroh.link` domain. + /// + /// This uses the [`N0_DNS_ENDPOINT_ORIGIN_PROD`] domain. + /// + /// When running with the environment variable `IROH_FORCE_STAGING_RELAYS` + /// set to any non empty value the [`N0_DNS_ENDPOINT_ORIGIN_STAGING`] domain + /// is used instead. + pub fn n0_dns() -> DnsAddressLookupBuilder { + if force_staging_infra() { + Self::builder(N0_DNS_ENDPOINT_ORIGIN_STAGING.to_string()) + } else { + Self::builder(N0_DNS_ENDPOINT_ORIGIN_PROD.to_string()) + } + } +} + +impl AddressLookupBuilder for DnsAddressLookupBuilder { + fn into_address_lookup( + mut self, + endpoint: &Endpoint, + ) -> Result { + if self.dns_resolver.is_none() { + self.dns_resolver = Some(endpoint.dns_resolver()?.clone()); + } + Ok(self.build()) + } +} + +impl AddressLookup for DnsAddressLookup { + fn resolve( + &self, + endpoint_id: EndpointId, + ) -> Option>> { + let resolver = self.dns_resolver.clone(); + let origin_domain = self.origin_domain.clone(); + let span = + debug_span!("DnsAddressLookup", lookup_id=%endpoint_id.fmt_short(), %origin_domain); + let fut = async move { + trace!("starting DNS lookup"); + let endpoint_info = resolver + .lookup_endpoint_by_id_staggered(&endpoint_id, &origin_domain, DNS_STAGGERING_MS) + .await + .inspect_err(|err| debug!("DNS lookup failed: {err:#}")) + .map_err(|e| AddressLookupError::from_err_any("dns", e))?; + debug!(info=?endpoint_info, "DNS lookup success"); + Ok(AddressLookupItem::new(endpoint_info, "dns", None)) + } + .instrument(span); + let stream = n0_future::stream::once_future(fut); + Some(Box::pin(stream)) + } +} diff --git a/vendor/iroh/src/address_lookup/memory.rs b/vendor/iroh/src/address_lookup/memory.rs new file mode 100644 index 0000000..6587e3d --- /dev/null +++ b/vendor/iroh/src/address_lookup/memory.rs @@ -0,0 +1,306 @@ +//! An in-memory address lookup system to manually add endpoint addressing information. +//! +//! Often an application might get endpoint addressing information out-of-band in an +//! application-specific way. [`EndpointTicket`]'s are one common way used to achieve this. +//! This addressing information is often only usable for a limited time so needs to +//! be able to be removed again once you know it is no longer useful. +//! +//! This is where the [`MemoryLookup`] is useful: it allows applications to add and +//! retract endpoint addressing information that is otherwise out-of-band to iroh. +//! +//! [`EndpointTicket`]: https://docs.rs/iroh-tickets/latest/iroh_tickets/endpoint/struct.EndpointTicket.html + +use std::{ + collections::{BTreeMap, btree_map::Entry}, + sync::{Arc, RwLock}, +}; + +use iroh_base::EndpointId; +use n0_future::{ + boxed::BoxStream, + stream::{self, StreamExt}, + time::SystemTime, +}; + +use super::{AddressLookup, EndpointData, EndpointInfo, Error, Item}; + +/// An in-memory address lookup system to manually add endpoint addressing information. +/// +/// Often an application might get endpoint addressing information out-of-band in an +/// application-specific way. [`EndpointTicket`]'s are one common way used to achieve this. +/// This addressing information is often only usable for a limited time so needs to +/// be able to be removed again once you know it is no longer useful. +/// +/// This is where the [`MemoryLookup`] is useful: it allows applications to add and +/// retract endpoint addressing information that is otherwise out-of-band to iroh. +/// +/// # Examples +/// +/// ```no_run +/// # #[cfg(with_crypto_provider)] // Endpoint::bind needs a crypto provider +/// # { +/// use iroh::{ +/// Endpoint, EndpointAddr, TransportAddr, address_lookup::memory::MemoryLookup, +/// endpoint::presets, +/// }; +/// use iroh_base::SecretKey; +/// +/// # #[tokio::main] +/// # async fn wrapper() -> n0_error::Result<()> { +/// // Create the Address Lookup and endpoint. +/// let address_lookup = MemoryLookup::new(); +/// +/// let _ep = Endpoint::builder(presets::N0) +/// .address_lookup(address_lookup.clone()) +/// .bind() +/// .await?; +/// +/// // Sometime later add a RelayUrl for our endpoint. +/// let id = SecretKey::generate().public(); +/// // You can pass either `EndpointInfo` or `EndpointAddr` to `add_endpoint_info`. +/// address_lookup.add_endpoint_info(EndpointAddr { +/// id, +/// addrs: [TransportAddr::Relay("https://example.com".parse()?)] +/// .into_iter() +/// .collect(), +/// }); +/// +/// # Ok(()) +/// # } +/// # } +/// ``` +/// +/// [`EndpointTicket`]: https://docs.rs/iroh-tickets/latest/iroh_tickets/endpoint/struct.EndpointTicket.html +#[derive(Debug, Clone)] +pub struct MemoryLookup { + endpoints: Arc>>, + provenance: &'static str, +} + +impl Default for MemoryLookup { + fn default() -> Self { + Self { + endpoints: Default::default(), + provenance: Self::PROVENANCE, + } + } +} + +#[derive(Debug)] +struct StoredEndpointInfo { + data: EndpointData, + last_updated: SystemTime, +} + +impl MemoryLookup { + /// The provenance string for this Address Lookup implementation. + /// + /// This is mostly used for debugging information and allows understanding the origin of + /// addressing information used by an iroh [`Endpoint`]. + /// + /// [`Endpoint`]: crate::Endpoint + pub const PROVENANCE: &'static str = "memory_lookup"; + + /// Creates a new empty Memory Lookup instance. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new Memory Lookup instance with the provided `provenance`. + /// + /// The provenance is part of [`address_lookup::Item`]s returned from [`Self::resolve`]. + /// It is mostly used for debugging information and allows understanding the origin of + /// addressing information used by an iroh [`Endpoint`]. + /// + /// [`Endpoint`]: crate::Endpoint + /// [`address_lookup::Item`]: crate::address_lookup::Item + pub fn with_provenance(provenance: &'static str) -> Self { + Self { + endpoints: Default::default(), + provenance, + } + } + + /// Creates a Memory Lookup instance from endpoint addresses. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(with_crypto_provider)] // Endpoint::bind needs a crypto provider + /// # { + /// use std::{net::SocketAddr, str::FromStr}; + /// + /// use iroh::{Endpoint, EndpointAddr, address_lookup::memory::MemoryLookup, endpoint::presets}; + /// + /// # fn get_addrs() -> Vec { + /// # Vec::new() + /// # } + /// # #[tokio::main] + /// # async fn wrapper() -> n0_error::Result<()> { + /// // get addrs from somewhere + /// let addrs = get_addrs(); + /// + /// // create a MemoryLookup from the list of addrs. + /// let address_lookup = MemoryLookup::from_endpoint_info(addrs); + /// // create an endpoint with the memory lookup address_lookup + /// let endpoint = Endpoint::builder(presets::N0) + /// .address_lookup(address_lookup) + /// .bind() + /// .await?; + /// # Ok(()) + /// # } + /// # } + /// ``` + pub fn from_endpoint_info(infos: impl IntoIterator>) -> Self { + let res = Self::default(); + for info in infos { + res.add_endpoint_info(info); + } + res + } + + /// Sets endpoint addressing information for the given endpoint ID. + /// + /// This will completely overwrite any existing info for the endpoint. + /// + /// Returns the [`EndpointData`] of the previous entry, or `None` if there was no previous + /// entry for this endpoint ID. + pub fn set_endpoint_info( + &self, + endpoint_info: impl Into, + ) -> Option { + let last_updated = SystemTime::now(); + let EndpointInfo { endpoint_id, data } = endpoint_info.into(); + let mut guard = self.endpoints.write().expect("poisoned"); + let previous = guard.insert(endpoint_id, StoredEndpointInfo { data, last_updated }); + previous.map(|x| x.data) + } + + /// Augments endpoint addressing information for the given endpoint ID. + /// + /// The provided addressing information is combined with the existing info in the memory + /// lookup. Any new direct addresses are added to those already present while the + /// relay URL is overwritten. + pub fn add_endpoint_info(&self, endpoint_info: impl Into) { + let last_updated = SystemTime::now(); + let EndpointInfo { endpoint_id, data } = endpoint_info.into(); + let mut guard = self.endpoints.write().expect("poisoned"); + match guard.entry(endpoint_id) { + Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + existing.data.add_addrs(data.addrs().cloned()); + existing.data.set_user_data(data.user_data().cloned()); + existing.last_updated = last_updated; + } + Entry::Vacant(entry) => { + entry.insert(StoredEndpointInfo { data, last_updated }); + } + } + } + + /// Returns endpoint addressing information for the given endpoint ID. + pub fn get_endpoint_info(&self, endpoint_id: EndpointId) -> Option { + let guard = self.endpoints.read().expect("poisoned"); + let info = guard.get(&endpoint_id)?; + Some(EndpointInfo::from_parts(endpoint_id, info.data.clone())) + } + + /// Removes all endpoint addressing information for the given endpoint ID. + /// + /// Any removed information is returned. + pub fn remove_endpoint_info(&self, endpoint_id: EndpointId) -> Option { + let mut guard = self.endpoints.write().expect("poisoned"); + let info = guard.remove(&endpoint_id)?; + Some(EndpointInfo::from_parts(endpoint_id, info.data)) + } +} + +impl AddressLookup for MemoryLookup { + fn publish(&self, _data: &EndpointData) {} + + fn resolve(&self, endpoint_id: EndpointId) -> Option>> { + let guard = self.endpoints.read().expect("poisoned"); + let info = guard.get(&endpoint_id); + match info { + Some(endpoint_info) => { + let last_updated = endpoint_info + .last_updated + .duration_since(SystemTime::UNIX_EPOCH) + .expect("time drift") + .as_micros() as u64; + let item = Item::new( + EndpointInfo::from_parts(endpoint_id, endpoint_info.data.clone()), + self.provenance, + Some(last_updated), + ); + Some(stream::iter(Some(Ok(item))).boxed()) + } + None => None, + } + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use iroh_base::{EndpointAddr, SecretKey, TransportAddr}; + use n0_error::{Result, StackResultExt}; + + use super::*; + use crate::{Endpoint, endpoint::presets}; + + #[tokio::test] + async fn test_basic() -> Result { + let address_lookup = MemoryLookup::new(); + + let _ep = Endpoint::builder(presets::Minimal) + .address_lookup(address_lookup.clone()) + .bind() + .await?; + + let key = SecretKey::from_bytes(&[0u8; 32]); + let addr = EndpointAddr::from_parts( + key.public(), + [TransportAddr::Relay("https://example.com".parse()?)], + ); + let user_data = Some("foobar".parse().unwrap()); + let endpoint_info = EndpointInfo::from(addr.clone()).with_user_data(user_data.clone()); + address_lookup.add_endpoint_info(endpoint_info.clone()); + + let back = address_lookup + .get_endpoint_info(key.public()) + .context("no addr")?; + + assert_eq!(back, endpoint_info); + assert_eq!(back.user_data(), user_data.as_ref()); + assert_eq!(back.into_endpoint_addr(), addr); + + let removed = address_lookup + .remove_endpoint_info(key.public()) + .context("nothing removed")?; + assert_eq!(removed, endpoint_info); + let res = address_lookup.get_endpoint_info(key.public()); + assert!(res.is_none()); + + Ok(()) + } + + #[tokio::test] + async fn test_provenance() -> Result { + let address_lookup = MemoryLookup::with_provenance("foo"); + let key = SecretKey::from_bytes(&[0u8; 32]); + let addr = EndpointAddr::from_parts( + key.public(), + [TransportAddr::Relay("https://example.com".parse()?)], + ); + address_lookup.add_endpoint_info(addr); + let mut stream = address_lookup.resolve(key.public()).unwrap(); + let item = stream.next().await.unwrap()?; + assert_eq!(item.provenance(), "foo"); + assert_eq!( + item.relay_urls().next(), + Some(&("https://example.com".parse()?)) + ); + + Ok(()) + } +} diff --git a/vendor/iroh/src/address_lookup/metrics.rs b/vendor/iroh/src/address_lookup/metrics.rs new file mode 100644 index 0000000..ea199a8 --- /dev/null +++ b/vendor/iroh/src/address_lookup/metrics.rs @@ -0,0 +1,48 @@ +//! Metrics for address lookup. + +use iroh_metrics::{Counter, EncodeLabelSet, Family, MetricsGroup}; +use serde::{Deserialize, Serialize}; + +/// Labels identifying an address lookup service by its provenance string, +/// see [`crate::address_lookup::Item::provenance`]. +#[derive( + Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EncodeLabelSet, +)] +pub struct ServiceLabels { + /// Provenance string of the service. + pub service: String, +} + +impl ServiceLabels { + /// Creates labels for the given service provenance. + pub fn new(service: impl Into) -> Self { + Self { + service: service.into(), + } + } +} + +/// Metrics collected by address lookup. +/// +/// A lookup is one call to [`AddressLookupServices::resolve`] and queries all +/// configured services at once. Each service can yield several results and +/// errors per lookup; those are counted in the `service_*` counters, labeled +/// by the service's provenance (e.g. `dns`, `pkarr`). +/// +/// [`AddressLookupServices::resolve`]: crate::address_lookup::AddressLookupServices::resolve +#[derive(Debug, Serialize, Deserialize, MetricsGroup)] +#[non_exhaustive] +#[metrics(name = "address_lookup", default)] +pub struct Metrics { + /// Lookups started. + pub lookups: Counter, + /// Lookups that ended without a single result. + /// + /// Includes lookups with no services configured. Lookups abandoned early + /// (e.g. once a connection is established) are not counted. + pub lookups_failed: Counter, + /// Results yielded per service. + pub service_results: Family, + /// Errors yielded per service. + pub service_errors: Family, +} diff --git a/vendor/iroh/src/address_lookup/pkarr.rs b/vendor/iroh/src/address_lookup/pkarr.rs new file mode 100644 index 0000000..a00878f --- /dev/null +++ b/vendor/iroh/src/address_lookup/pkarr.rs @@ -0,0 +1,683 @@ +//! An address lookup service which publishes and resolves endpoint information using a [pkarr] relay. +//! +//! Public-Key Addressable Resource Records, [pkarr], is a system which allows publishing +//! [DNS Resource Records] owned by a particular [`SecretKey`] under a name derived from its +//! corresponding [`PublicKey`], also known as the [`EndpointId`]. Additionally this pkarr +//! Resource Record is signed using the same [`SecretKey`], ensuring authenticity of the +//! record. +//! +//! Pkarr normally stores these records on the [Mainline DHT], but also provides two bridges +//! that do not require clients to directly interact with the DHT: +//! +//! - Resolvers are servers which expose the pkarr Resource Record under a domain name, +//! e.g. `o3dks..6uyy.dns.iroh.link`. This allows looking up the pkarr Resource Records +//! using normal DNS clients. These resolvers would normally perform lookups on the +//! Mainline DHT augmented with a local cache to improve performance. +//! +//! - Relays are servers which allow both publishing and looking up of the pkarr Resource +//! Records using HTTP PUT and GET requests. They will usually perform the publishing to +//! the Mainline DHT on behalf on the client as well as cache lookups performed on the DHT +//! to improve performance. +//! +//! [`PkarrPublisher`] filters published addresses: only relay addresses are published by default. +//! To change this behavior, use [`PkarrPublisherBuilder::addr_filter`] and set it to e.g. [`AddrFilter::unfiltered`]. +//! This can be useful to enable publishing IP addresses if the iroh endpoint is reachable via public +//! IP addresses. +//! +//! For address lookup in iroh the pkarr Resource Records contain the addressing information, +//! providing endpoints which retrieve the pkarr Resource Record with enough detail +//! to contact the iroh endpoint. +//! +//! There are several Address Lookup's built on top of pkarr, which can be composed +//! to the application's needs: +//! +//! - [`PkarrPublisher`], which publishes to a pkarr relay server using HTTP. +//! +//! - [`PkarrResolver`], which resolves from a pkarr relay server using HTTP. +//! +//! - [`address_lookup::DnsAddressLookup`], which resolves from a DNS server. +//! +//! Mainline-DHT-based pkarr publishing/lookup lives in the +//! [`iroh-mainline-address-lookup`] crate. +//! +//! [`iroh-mainline-address-lookup`]: https://docs.rs/iroh-mainline-address-lookup +//! [pkarr]: https://pkarr.org +//! [DNS Resource Records]: https://en.wikipedia.org/wiki/Domain_Name_System#Resource_records +//! [Mainline DHT]: https://en.wikipedia.org/wiki/Mainline_DHT +//! [`SecretKey`]: crate::SecretKey +//! [`PublicKey`]: crate::PublicKey +//! [`EndpointId`]: crate::EndpointId +//! [`address_lookup::DnsAddressLookup`]: crate::address_lookup::DnsAddressLookup +//! [`N0` preset]: crate::endpoint::presets::N0 +//! [`AddrFilter`]: crate::address_lookup::AddrFilter +//! [`AddrFilter::relay_only`]: crate::address_lookup::AddrFilter::relay_only +//! [`AddrFilter::unfiltered`]: crate::address_lookup::AddrFilter::unfiltered +//! [`PkarrPublisherBuilder::addr_filter`]: PkarrPublisherBuilder::addr_filter + +use std::sync::Arc; + +use iroh_base::{EndpointId, RelayUrl, SecretKey}; +use iroh_dns::{ + EncodingError, + endpoint_info::{AddrFilter, EndpointInfo}, + pkarr::{SignedPacket, SignedPacketVerifyError}, +}; +use n0_error::{AnyError, anyerr, e, stack_error}; +use n0_future::{ + boxed::BoxStream, + task::{self, AbortOnDropHandle}, + time::{self, Duration, Instant}, +}; +use n0_watcher::{Disconnected, Watchable, Watcher as _}; +use tracing::{Instrument, debug, info_span, trace, warn}; +use url::Url; + +#[cfg(not(wasm_browser))] +use crate::dns::DnsResolver; +use crate::{ + Endpoint, + address_lookup::{ + AddressLookup, AddressLookupBuilder, AddressLookupBuilderError, EndpointData, + Error as AddressLookupError, Item as AddressLookupItem, + }, + endpoint::force_staging_infra, + util::reqwest_client_builder, +}; + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub enum PkarrError { + #[error("Invalid public key")] + PublicKey { + #[error(std_err)] + source: iroh_base::KeyParsingError, + }, + #[error("Packet failed to verify")] + Verify { + #[error(std_err)] + source: SignedPacketVerifyError, + }, + #[error("Invalid relay URL")] + InvalidRelayUrl { url: RelayUrl }, + #[error("Error sending http request")] + HttpSend { source: AnyError }, + #[error("Error resolving http request")] + HttpRequest { status: http::StatusCode }, + #[error("Http payload error")] + HttpPayload { source: AnyError }, + #[error("EncodingError")] + Encoding { source: EncodingError }, +} + +impl From for AddressLookupError { + fn from(err: PkarrError) -> Self { + AddressLookupError::from_err_any("pkarr", err) + } +} + +/// The production pkarr relay run by [number 0]. +/// +/// This server is both a pkarr relay server as well as a DNS resolver, see the [module +/// documentation]. However it does not interact with the Mainline DHT, so is a more +/// central service. It is a reliable service to use for address lookup. +/// +/// [number 0]: https://n0.computer +/// [module documentation]: crate::address_lookup::pkarr +pub const N0_DNS_PKARR_RELAY_PROD: &str = "https://dns.iroh.link/pkarr"; +/// The testing pkarr relay run by [number 0]. +/// +/// This server operates similarly to [`N0_DNS_PKARR_RELAY_PROD`] but is not as reliable. +/// It is meant for more experimental use and testing purposes. +/// +/// [number 0]: https://n0.computer +pub const N0_DNS_PKARR_RELAY_STAGING: &str = "https://staging-dns.iroh.link/pkarr"; + +/// Default TTL for the records in the pkarr signed packet. +/// +/// The Time To Live (TTL) tells DNS caches how long to store a record. It is ignored by the +/// `iroh-dns-server`, e.g. as running on [`N0_DNS_PKARR_RELAY_PROD`], as the home server +/// keeps the records for the domain. When using the pkarr relay no DNS is involved and the +/// setting is ignored. +// TODO(flub): huh? +pub const DEFAULT_PKARR_TTL: u32 = 30; + +/// Interval in which to republish the endpoint info even if unchanged: 5 minutes. +pub const DEFAULT_REPUBLISH_INTERVAL: Duration = Duration::from_secs(60 * 5); + +/// Builder for [`PkarrPublisher`]. +/// +/// See [`PkarrPublisher::builder`]. +#[derive(Debug)] +pub struct PkarrPublisherBuilder { + pkarr_relay: Url, + ttl: u32, + republish_interval: Duration, + filter: AddrFilter, + #[cfg(not(wasm_browser))] + dns_resolver: Option, +} + +impl PkarrPublisherBuilder { + /// See [`PkarrPublisher::builder`]. + fn new(pkarr_relay: Url) -> Self { + Self { + pkarr_relay, + ttl: DEFAULT_PKARR_TTL, + republish_interval: DEFAULT_REPUBLISH_INTERVAL, + filter: AddrFilter::relay_only(), + #[cfg(not(wasm_browser))] + dns_resolver: None, + } + } + + /// See [`PkarrPublisher::n0_dns`]. + fn n0_dns() -> Self { + let pkarr_relay = match force_staging_infra() { + true => N0_DNS_PKARR_RELAY_STAGING, + false => N0_DNS_PKARR_RELAY_PROD, + }; + + let pkarr_relay: Url = pkarr_relay.parse().expect("url is valid"); + Self::new(pkarr_relay) + } + + /// Sets the TTL (time-to-live) for published packets. + /// + /// Default is [`DEFAULT_PKARR_TTL`]. + pub fn ttl(mut self, ttl: u32) -> Self { + self.ttl = ttl; + self + } + + /// Sets the interval after which packets are republished even if our endpoint info did not change. + /// + /// Default is [`DEFAULT_REPUBLISH_INTERVAL`]. + pub fn republish_interval(mut self, republish_interval: Duration) -> Self { + self.republish_interval = republish_interval; + self + } + + /// Sets the DNS resolver to use for resolving the pkarr relay URL. + #[cfg(not(wasm_browser))] + pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self { + self.dns_resolver = Some(dns_resolver); + self + } + + /// Sets the address filter to control which addresses are published to the pkarr server. + /// + /// By default [`AddrFilter::relay_only`] is used. This avoids leaking IP addresses to the + /// public pkarr server. + /// + /// However, enabling IP address publishing can be useful, e.g. when iroh runs on a machine + /// connected to the internet via public IP addresses without a firewall. + /// In such cases, publishing them can make dialing such endpoints via DNS or Pkarr lookup + /// faster, potentially skipping a relay connection altogether. + pub fn addr_filter(mut self, filter: AddrFilter) -> Self { + self.filter = filter; + self + } + + /// Builds the [`PkarrPublisher`] with the passed secret key for signing packets. + /// + /// This publisher will be able to publish [pkarr](https://pkarr.org) records for [`SecretKey`]. + pub fn build(self, secret_key: SecretKey, tls_config: rustls::ClientConfig) -> PkarrPublisher { + PkarrPublisher::new( + secret_key, + self.pkarr_relay, + self.ttl, + self.republish_interval, + #[cfg(not(wasm_browser))] + self.dns_resolver.unwrap_or_default(), + tls_config, + self.filter, + ) + } +} + +impl AddressLookupBuilder for PkarrPublisherBuilder { + fn into_address_lookup( + mut self, + endpoint: &Endpoint, + ) -> Result { + #[cfg(not(wasm_browser))] + if self.dns_resolver.is_none() { + self.dns_resolver = Some(endpoint.dns_resolver()?.clone()); + } + let tls_config = endpoint.tls_config().clone(); + Ok(self.build(endpoint.secret_key().clone(), tls_config)) + } +} + +/// Publisher of address lookup information to a [pkarr] relay. +/// +/// This publisher uses HTTP to publish address lookup information to a pkarr relay +/// server, see the [module docs] for details. +/// +/// This implements the [`AddressLookup`] trait to be used as an address lookup service. Note +/// that it only publishes address lookup information, for the corresponding resolver use +/// the [`PkarrResolver`] together with [`AddressLookupServices`]. +/// +/// By default this publisher only publishes the [`RelayUrl`], to avoid leaking IP addresses to +/// the public pkarr server. Which addresses are published is controlled by the [`AddrFilter`] +/// set via [`PkarrPublisherBuilder::addr_filter`]. +/// +/// [pkarr]: https://pkarr.org +/// [module docs]: crate::address_lookup::pkarr +/// [`RelayUrl`]: crate::RelayUrl +/// [`AddressLookupServices`]: super::AddressLookupServices +#[derive(derive_more::Debug, Clone)] +pub struct PkarrPublisher { + endpoint_id: EndpointId, + watchable: Watchable>, + addr_filter: AddrFilter, + _drop_guard: Arc>, +} + +impl PkarrPublisher { + /// Returns a [`PkarrPublisherBuilder`] that publishes endpoint info to a [pkarr] relay at `pkarr_relay`. + /// + /// If no further options are set, the pkarr publisher will use [`DEFAULT_PKARR_TTL`] as the + /// time-to-live value for the published packets, and it will republish Address Lookup information + /// every [`DEFAULT_REPUBLISH_INTERVAL`], even if the information is unchanged. + /// + /// [`PkarrPublisherBuilder`] implements [`AddressLookupBuilder`], so it can be passed to [`address_lookup`]. + /// It will then use the endpoint's secret key to sign published packets. + /// + /// [`address_lookup`]: crate::endpoint::Builder::address_lookup + /// [pkarr]: https://pkarr.org + pub fn builder(pkarr_relay: Url) -> PkarrPublisherBuilder { + PkarrPublisherBuilder::new(pkarr_relay) + } + + /// Creates a new [`PkarrPublisher`] with a custom TTL and republish intervals. + /// + /// This allows creating the publisher with custom time-to-live values of the + /// [`SignedPacket`]s as well as a custom republish interval. + fn new( + secret_key: SecretKey, + pkarr_relay: Url, + ttl: u32, + republish_interval: Duration, + #[cfg(not(wasm_browser))] dns_resolver: DnsResolver, + tls_config: rustls::ClientConfig, + addr_filter: AddrFilter, + ) -> Self { + debug!("creating pkarr publisher that publishes to {pkarr_relay}"); + let endpoint_id = secret_key.public(); + + #[cfg(wasm_browser)] + let pkarr_client = PkarrRelayClient::new(pkarr_relay); + + #[cfg(not(wasm_browser))] + let pkarr_client = PkarrRelayClient::new(pkarr_relay, tls_config, dns_resolver); + + let watchable = Watchable::default(); + let service = PublisherService { + ttl, + watcher: watchable.watch(), + secret_key, + pkarr_client, + republish_interval, + }; + let join_handle = task::spawn(service.run().instrument(info_span!("pkarr_publish"))); + Self { + watchable, + endpoint_id, + addr_filter, + _drop_guard: Arc::new(AbortOnDropHandle::new(join_handle)), + } + } + + /// Creates a pkarr publisher which uses the [number 0] pkarr relay server. + /// + /// This uses the pkarr relay server operated by [number 0], at + /// [`N0_DNS_PKARR_RELAY_PROD`]. + /// + /// When running with the environment variable + /// `IROH_FORCE_STAGING_RELAYS` set to any non empty value [`N0_DNS_PKARR_RELAY_STAGING`] + /// server is used instead. + /// + /// [number 0]: https://n0.computer + pub fn n0_dns() -> PkarrPublisherBuilder { + PkarrPublisherBuilder::n0_dns() + } + + /// Publishes the addressing information about this endpoint to a pkarr relay. + /// + /// This is a nonblocking function, the actual update is performed in the background. + pub fn update_endpoint_data(&self, data: &EndpointData) { + let data = data.apply_filter(&self.addr_filter).into_owned(); + let info = EndpointInfo::from_parts(self.endpoint_id, data); + self.watchable.set(Some(info)).ok(); + } +} + +impl AddressLookup for PkarrPublisher { + fn publish(&self, data: &EndpointData) { + self.update_endpoint_data(data); + } +} + +/// Publish endpoint info to a pkarr relay. +#[derive(derive_more::Debug, Clone)] +struct PublisherService { + #[debug("SecretKey")] + secret_key: SecretKey, + #[debug("PkarrClient")] + pkarr_client: PkarrRelayClient, + watcher: n0_watcher::Direct>, + ttl: u32, + republish_interval: Duration, +} + +impl PublisherService { + async fn run(mut self) { + let mut failed_attempts = 0; + let republish = time::sleep(Duration::MAX); + tokio::pin!(republish); + loop { + if !self.watcher.is_connected() { + break; + } + if let Some(info) = self.watcher.get() { + match self.publish_current(info).await { + Err(err) => { + failed_attempts += 1; + // Retry after increasing timeout + let retry_after = Duration::from_secs(failed_attempts); + republish.as_mut().reset(Instant::now() + retry_after); + warn!( + err = %format!("{err:#}"), + url = %self.pkarr_client.pkarr_relay_url , + ?retry_after, + %failed_attempts, + "Failed to publish to pkarr", + ); + } + Ok(()) => { + failed_attempts = 0; + // Republish after fixed interval + republish + .as_mut() + .reset(Instant::now() + self.republish_interval); + } + } + } + // Wait until either the retry/republish timeout is reached, or the endpoint info changed. + tokio::select! { + res = self.watcher.updated() => match res { + Ok(_) => debug!("Publish endpoint info to pkarr (info changed)"), + Err(Disconnected { .. }) => break, + }, + _ = &mut republish => debug!("Publish endpoint info to pkarr (interval elapsed)"), + } + } + } + + async fn publish_current(&self, info: EndpointInfo) -> Result<(), PkarrError> { + debug!( + data = ?info.data, + pkarr_relay = %self.pkarr_client.pkarr_relay_url, + "Publishing endpoint info to pkarr" + ); + let signed_packet = info + .to_pkarr_signed_packet(&self.secret_key, self.ttl) + .map_err(|err| e!(PkarrError::Encoding, err))?; + self.pkarr_client.publish(&signed_packet).await?; + trace!( + data = ?info.data, + pkarr_relay = %self.pkarr_client.pkarr_relay_url, + "Published endpoint info to pkarr" + ); + Ok(()) + } +} + +/// Builder for [`PkarrResolver`]. +/// +/// See [`PkarrResolver::builder`]. +#[derive(Debug)] +pub struct PkarrResolverBuilder { + pkarr_relay: Url, + #[cfg(not(wasm_browser))] + dns_resolver: Option, +} + +impl PkarrResolverBuilder { + /// Sets the DNS resolver to use for resolving the pkarr relay URL. + #[cfg(not(wasm_browser))] + pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self { + self.dns_resolver = Some(dns_resolver); + self + } + + /// Creates a [`PkarrResolver`] from this builder. + pub fn build(self, tls_config: rustls::ClientConfig) -> PkarrResolver { + #[cfg(wasm_browser)] + let pkarr_client = PkarrRelayClient::new(self.pkarr_relay); + + #[cfg(not(wasm_browser))] + let pkarr_client = PkarrRelayClient::new( + self.pkarr_relay, + tls_config, + self.dns_resolver.unwrap_or_default(), + ); + + PkarrResolver { pkarr_client } + } +} + +impl AddressLookupBuilder for PkarrResolverBuilder { + fn into_address_lookup( + mut self, + endpoint: &Endpoint, + ) -> Result { + #[cfg(not(wasm_browser))] + if self.dns_resolver.is_none() { + self.dns_resolver = Some(endpoint.dns_resolver()?.clone()); + } + let tls_config = endpoint.tls_config().clone(); + Ok(self.build(tls_config)) + } +} + +/// Resolver of address lookup information from a [pkarr] relay. +/// +/// The resolver uses HTTP to query address lookup information from a pkarr relay server, +/// see the [module docs] for details. +/// +/// This implements the [`AddressLookup`] trait to be used as an address lookup service. Note +/// that it only resolves address lookup information, for the corresponding publisher use +/// the [`PkarrPublisher`] together with [`AddressLookupServices`]. +/// +/// [pkarr]: https://pkarr.org +/// [module docs]: crate::address_lookup::pkarr +/// [`AddressLookupServices`]: super::AddressLookupServices +#[derive(derive_more::Debug, Clone)] +pub struct PkarrResolver { + pkarr_client: PkarrRelayClient, +} + +impl PkarrResolver { + /// Creates a new resolver builder using the pkarr relay server at the URL. + /// + /// The builder implements [`AddressLookupBuilder`]. + pub fn builder(pkarr_relay: Url) -> PkarrResolverBuilder { + PkarrResolverBuilder { + pkarr_relay, + #[cfg(not(wasm_browser))] + dns_resolver: None, + } + } + + /// Creates a pkarr resolver builder which uses the [number 0] pkarr relay server. + /// + /// This uses the pkarr relay server operated by [number 0] at + /// [`N0_DNS_PKARR_RELAY_PROD`]. + /// + /// When running with the environment variable `IROH_FORCE_STAGING_RELAYS` + /// set to any non empty value [`N0_DNS_PKARR_RELAY_STAGING`] + /// server is used instead. + /// + /// [number 0]: https://n0.computer + pub fn n0_dns() -> PkarrResolverBuilder { + let pkarr_relay = match force_staging_infra() { + true => N0_DNS_PKARR_RELAY_STAGING, + false => N0_DNS_PKARR_RELAY_PROD, + }; + + let pkarr_relay: Url = pkarr_relay.parse().expect("url is valid"); + Self::builder(pkarr_relay) + } +} + +impl AddressLookup for PkarrResolver { + fn resolve( + &self, + endpoint_id: EndpointId, + ) -> Option>> { + let pkarr_client = self.pkarr_client.clone(); + let fut = async move { + let signed_packet = pkarr_client.resolve(endpoint_id).await?; + let info = EndpointInfo::from_pkarr_signed_packet(&signed_packet) + .map_err(|err| AddressLookupError::from_err_any("pkarr", err))?; + let item = AddressLookupItem::new(info, "pkarr", None); + Ok(item) + }; + let stream = n0_future::stream::once_future(fut); + Some(Box::pin(stream)) + } +} + +/// A [pkarr](https://pkarr.org) client to publish [`SignedPacket`]s to a pkarr relay. +/// +/// [pkarr]: https://pkarr.org +#[derive(Debug, Clone)] +pub struct PkarrRelayClient { + http_client: reqwest::Client, + pkarr_relay_url: Url, +} + +/// A builder for the [`PkarrRelayClient`] +#[derive(Debug, Clone)] +pub struct PkarrRelayClientBuilder { + pkarr_relay_url: Url, + #[cfg(not(wasm_browser))] + dns_resolver: DnsResolver, + #[cfg(not(wasm_browser))] + tls_config: rustls::ClientConfig, +} + +impl PkarrRelayClientBuilder { + /// Build a [`PkarrRelayClient`]. + pub fn build(self) -> PkarrRelayClient { + #[cfg(not(wasm_browser))] + let builder = reqwest_client_builder(self.tls_config, self.dns_resolver); + + #[cfg(wasm_browser)] + let builder = reqwest_client_builder(); + + let http_client = builder.build().expect("failed to create reqwest client"); + PkarrRelayClient { + http_client, + pkarr_relay_url: self.pkarr_relay_url, + } + } +} + +impl PkarrRelayClient { + /// Creates a [`PkarrRelayClient`]. + #[cfg(not(wasm_browser))] + pub fn new( + pkarr_relay_url: Url, + tls_config: rustls::ClientConfig, + dns_resolver: DnsResolver, + ) -> Self { + let http_client = reqwest_client_builder(tls_config, dns_resolver) + .build() + .expect("failed to create reqwest client"); + Self { + http_client, + pkarr_relay_url, + } + } + + /// Creates a [`PkarrRelayClient`]. + #[cfg(wasm_browser)] + pub fn new(pkarr_relay_url: Url) -> Self { + let http_client = reqwest_client_builder() + .build() + .expect("failed to create reqwest client"); + Self { + http_client, + pkarr_relay_url, + } + } + + /// Resolves a [`SignedPacket`] for the given [`EndpointId`]. + pub async fn resolve( + &self, + endpoint_id: EndpointId, + ) -> Result { + let mut url = self.pkarr_relay_url.clone(); + url.path_segments_mut() + .map_err(|_| { + e!(PkarrError::InvalidRelayUrl { + url: self.pkarr_relay_url.clone().into() + }) + })? + .push(&endpoint_id.to_z32()); + + let response = self + .http_client + .get(url) + .send() + .await + .map_err(|err| e!(PkarrError::HttpSend, anyerr!(err)))?; + + if !response.status().is_success() { + return Err(e!(PkarrError::HttpRequest { + status: response.status() + }) + .into()); + } + + let payload = response + .bytes() + .await + .map_err(|err| e!(PkarrError::HttpPayload, anyerr!(err)))?; + let packet = SignedPacket::from_relay_payload(&endpoint_id, &payload) + .map_err(|err| e!(PkarrError::Verify, err))?; + Ok(packet) + } + + /// Publishes a [`SignedPacket`]. + pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<(), PkarrError> { + let mut url = self.pkarr_relay_url.clone(); + url.path_segments_mut() + .map_err(|_| { + e!(PkarrError::InvalidRelayUrl { + url: self.pkarr_relay_url.clone().into() + }) + })? + .push(&signed_packet.public_key().to_z32()); + + let response = self + .http_client + .put(url) + .body(signed_packet.to_relay_payload()) + .send() + .await + .map_err(|err| e!(PkarrError::HttpSend, anyerr!(err)))?; + + if !response.status().is_success() { + return Err(e!(PkarrError::HttpRequest { + status: response.status() + })); + } + + Ok(()) + } +} diff --git a/vendor/iroh/src/defaults.rs b/vendor/iroh/src/defaults.rs new file mode 100644 index 0000000..7763cdb --- /dev/null +++ b/vendor/iroh/src/defaults.rs @@ -0,0 +1,156 @@ +//! Default values used in [`iroh`][`crate`] + +/// The default QUIC port used by the Relay server to accept QUIC connections +/// for QUIC address discovery +/// +/// The port is "QUIC" typed on a phone keypad. +pub use iroh_relay::defaults::DEFAULT_RELAY_QUIC_PORT; +use url::Url; + +/// The default HTTP port used by the Relay server. +pub const DEFAULT_HTTP_PORT: u16 = 80; + +/// The default HTTPS port used by the Relay server. +pub const DEFAULT_HTTPS_PORT: u16 = 443; + +/// The default metrics port used by the Relay server. +pub const DEFAULT_METRICS_PORT: u16 = 9090; + +/// Production configuration. +pub mod prod { + use iroh_relay::{RelayConfig, RelayMap}; + + use super::*; + use crate::RelayUrl; + + /// Hostname of the default NA east relay. + pub const NA_EAST_RELAY_HOSTNAME: &str = "use1-1.relay.n0.iroh.link."; + /// Hostname of the default NA west relay. + pub const NA_WEST_RELAY_HOSTNAME: &str = "usw1-1.relay.n0.iroh.link."; + /// Hostname of the default EU relay. + pub const EU_RELAY_HOSTNAME: &str = "euc1-1.relay.n0.iroh.link."; + /// Hostname of the default Asia-Pacific relay. + pub const AP_RELAY_HOSTNAME: &str = "aps1-1.relay.n0.iroh.link."; + + /// Get the default [`RelayMap`]. + pub fn default_relay_map() -> RelayMap { + RelayMap::from_iter([ + default_na_east_relay(), + default_na_west_relay(), + default_eu_relay(), + default_ap_relay(), + ]) + } + + /// Get the default [`RelayConfig`] for NA east. + pub fn default_na_east_relay() -> RelayConfig { + // The default NA east relay server run by number0. + let url: Url = format!("https://{NA_EAST_RELAY_HOSTNAME}") + .parse() + .expect("default url"); + RelayConfig::from(RelayUrl::from(url)) + } + + /// Get the default [`RelayConfig`] for NA west. + pub fn default_na_west_relay() -> RelayConfig { + // The default NA west relay server run by number0. + let url: Url = format!("https://{NA_WEST_RELAY_HOSTNAME}") + .parse() + .expect("default url"); + RelayConfig::from(RelayUrl::from(url)) + } + + /// Get the default [`RelayConfig`] for EU. + pub fn default_eu_relay() -> RelayConfig { + // The default EU relay server run by number0. + let url: Url = format!("https://{EU_RELAY_HOSTNAME}") + .parse() + .expect("default_url"); + RelayConfig::from(RelayUrl::from(url)) + } + + /// Get the default [`RelayConfig`] for Asia-Pacific. + pub fn default_ap_relay() -> RelayConfig { + // The default Asia-Pacific relay server run by number0. + let url: Url = format!("https://{AP_RELAY_HOSTNAME}") + .parse() + .expect("default_url"); + RelayConfig::from(RelayUrl::from(url)) + } +} + +/// Staging configuration. +/// +/// Used by tests and might have incompatible changes deployed +/// +/// Note: we have staging servers in EU and NA, but no corresponding staging server for AP at this time. +pub mod staging { + use iroh_relay::{RelayConfig, RelayMap}; + + use super::*; + use crate::RelayUrl; + + /// Hostname of the default NA relay. + pub const NA_EAST_RELAY_HOSTNAME: &str = "use1-1.staging-relay.n0.iroh.link."; + /// Hostname of the default EU relay. + pub const EU_RELAY_HOSTNAME: &str = "euc1-1.staging-relay.n0.iroh.link."; + + /// Get the default [`RelayMap`]. + pub fn default_relay_map() -> RelayMap { + RelayMap::from_iter([default_na_east_relay(), default_eu_relay()]) + } + + /// Get the default [`RelayConfig`] for NA east. + pub fn default_na_east_relay() -> RelayConfig { + // The default NA east relay server run by number0. + let url: Url = format!("https://{NA_EAST_RELAY_HOSTNAME}") + .parse() + .expect("default url"); + RelayConfig::from(RelayUrl::from(url)) + } + + /// Get the default [`RelayConfig`] for EU. + pub fn default_eu_relay() -> RelayConfig { + // The default EU relay server run by number0. + let url: Url = format!("https://{EU_RELAY_HOSTNAME}") + .parse() + .expect("default_url"); + RelayConfig::from(RelayUrl::from(url)) + } +} + +/// Contains all timeouts that we use in `iroh`. +pub(crate) mod timeouts { + use n0_future::time::Duration; + + // Timeouts for net_report + + /// Maximum duration to wait for a net_report. + pub(crate) const NET_REPORT_TIMEOUT: Duration = Duration::from_secs(10); +} + +#[cfg(test)] +pub(crate) mod tests { + use std::time::Duration; + + use n0_tracing_test::traced_test; + + use super::staging::NA_EAST_RELAY_HOSTNAME; + use crate::dns::DnsResolver; + + const TIMEOUT: Duration = Duration::from_secs(5); + const STAGGERING_DELAYS: &[u64] = &[200, 300]; + + #[tokio::test] + #[traced_test] + async fn test_dns_lookup_ipv4_ipv6() { + let resolver = DnsResolver::new(); + let res: Vec<_> = resolver + .lookup_ipv4_ipv6_staggered(NA_EAST_RELAY_HOSTNAME, TIMEOUT, STAGGERING_DELAYS) + .await + .unwrap() + .collect(); + assert!(!res.is_empty()); + dbg!(res); + } +} diff --git a/vendor/iroh/src/endpoint.rs b/vendor/iroh/src/endpoint.rs new file mode 100644 index 0000000..5f578f6 --- /dev/null +++ b/vendor/iroh/src/endpoint.rs @@ -0,0 +1,4232 @@ +//! The [`Endpoint`] allows establishing connections to other iroh endpoints. +//! +//! The [`Endpoint`] is the main API interface to manage a local iroh endpoint. It allows +//! connecting to and accepting connections from other endpoints. See the [module docs] for +//! more details on how iroh connections work. +//! +//! The main items in this module are: +//! +//! - [`Endpoint`] to establish iroh connections with other endpoints. +//! - [`Builder`] to create an [`Endpoint`]. +//! +//! [module docs]: crate + +use std::{collections::BTreeSet, net::SocketAddr, pin::Pin, sync::Arc}; + +#[cfg(not(wasm_browser))] +use ipnet::{Ipv4Net, Ipv6Net}; +use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr}; +use iroh_relay::{RelayConfig, RelayMap, tls::CaTlsConfig}; +#[cfg(not(wasm_browser))] +use n0_error::bail; +use n0_error::{AnyError, e, ensure, stack_error}; +use n0_watcher::Watcher; +use pin_project::pin_project; +use tokio_util::sync::WaitForCancellationFutureOwned; +use tracing::{Instrument, Span, debug, event, info_span, instrument, warn}; +use url::Url; + +#[cfg(feature = "unstable-custom-transports")] +pub mod transports { + //! Types for defining custom transports and path selectors. + //! + //!
+ //! + //! These items are unstable and gated behind the `unstable-custom-transport` feature. + //! They are not covered by semantic versioning guarantees and may change in any release + //! without a major version bump. + //! + //!
+ + pub use super::socket::{ + remote_map::{PathSelection, PathSelectionContext, PathSelectionData, PathSelector}, + transports::{ + Addr, AddrKind, FourTuple, RecvInfo, Transmit, + custom::{CustomEndpoint, CustomSender, CustomTransport}, + }, + }; +} + +use self::hooks::EndpointHooksList; +pub use super::socket::{ + BindError, DirectAddr, DirectAddrType, + remote_map::{ + Path, PathEvent, PathEventStream, PathList, PathListIter, PathListStream, RemoteInfo, + TransportAddrInfo, TransportAddrUsage, + }, + transports::LocalTransportAddr, +}; +#[cfg(wasm_browser)] +use crate::address_lookup::PkarrResolver; +#[cfg(not(wasm_browser))] +use crate::dns::DnsResolver; +#[cfg(feature = "unstable-custom-transports")] +use crate::endpoint::transports::CustomTransport; +#[cfg(feature = "unstable-net-report")] +use crate::net_report::Report as NetReport; +pub use crate::tls::TlsConfigError; +use crate::{ + address_lookup::{ + AddrFilter, AddressLookupBuilder, AddressLookupFailed, AddressLookupServices, + DynAddressLookupBuilder, UserData, + }, + endpoint::presets::Preset, + metrics::EndpointMetrics, + socket::{ + self, EndpointInner, RemoteStateActorStoppedError, StaticConfig, + biased_rtt_path_selector::BiasedRttPathSelector, + mapped_addrs::MappedAddr, + remote_map::PathSelector, + transports::{RelayConnectionFailure, RelayConnectionState}, + }, + tls::{self, DEFAULT_MAX_TLS_TICKETS, misc::RustlsTokenKey}, +}; + +#[cfg(not(wasm_browser))] +mod bind; +mod connection; +pub(crate) mod hooks; +pub mod presets; +pub(crate) mod quic; + +#[cfg(not(wasm_browser))] +pub use bind::{BindOpts, InvalidSocketAddr, ToSocketAddr}; +pub use hooks::{AfterHandshakeOutcome, BeforeConnectOutcome, EndpointHooks}; + +#[cfg(feature = "qlog")] +pub use self::quic::{QlogConfig, QlogFactory, QlogFileFactory}; +pub use self::{ + connection::{ + Accept, Accepting, AlpnError, AuthenticationError, Connecting, ConnectingError, Connection, + ConnectionState, HandshakeCompleted, Incoming, IncomingAddr, IncomingZeroRtt, + IncomingZeroRttConnection, OutgoingZeroRtt, OutgoingZeroRttConnection, + RemoteEndpointIdError, RetryError, WeakConnectionHandle, ZeroRttStatus, + }, + quic::{ + AcceptBi, AcceptUni, AckFrequencyConfig, ApplicationClose, Chunk, Closed, ClosedStream, + ConnectionClose, ConnectionError, ConnectionStats, Controller, ControllerFactory, + ControllerMetrics, CryptoError, DecryptedInitial, Dir, ExportKeyingMaterialError, + FrameStats, FrameType, HandshakeTokenKey, HeaderKey, IdleTimeout, IncomingAlpns, Keys, + MtuDiscoveryConfig, OpenBi, OpenUni, PacketKey, PathId, PathStats, QuicConnectError, + QuicTransportConfig, QuicTransportConfigBuilder, ReadDatagram, ReadError, ReadExactError, + ReadManyDatagrams, ReadToEndError, RecvStream, ResetError, RttEstimator, SendDatagram, + SendDatagramError, SendStream, ServerConfig, ServerConfigBuilder, Side, StoppedError, + StreamId, TimeSource, TokenLog, TokenReuseError, TransportError, TransportErrorCode, + TransportParameters, UdpStats, UnorderedRecvStream, UnsupportedVersion, + ValidationTokenConfig, VarInt, VarIntBoundsExceeded, WriteError, + }, +}; +#[cfg(not(wasm_browser))] +use crate::socket::transports::IpConfig; +use crate::socket::transports::TransportConfig; +pub use crate::{net_report::NetReportConfig, portmapper::PortmapperConfig}; + +/// Builder for [`Endpoint`]. +/// +/// By default the endpoint will generate a new random [`SecretKey`], which will result in a +/// new [`EndpointId`]. +/// +/// To create the [`Endpoint`] call [`Builder::bind`]. +#[derive(Debug)] +pub struct Builder { + secret_key: Option, + alpn_protocols: Vec>, + transport_config: QuicTransportConfig, + keylog: bool, + address_lookup: Vec>, + address_lookup_user_data: Option, + /// Default address filter applied to all address lookup services added via + /// [`Builder::address_lookup`]. + addr_filter: Option, + proxy_url: Option, + ca_tls_config: Option, + #[cfg(not(wasm_browser))] + dns_resolver: Option, + transports: Vec, + max_tls_tickets: usize, + hooks: EndpointHooksList, + path_selector: Arc, + portmapper_config: PortmapperConfig, + net_report_config: NetReportConfig, + crypto_provider: Option>, + configured_addrs: BTreeSet, +} + +impl From for Option { + fn from(mode: RelayMode) -> Self { + match mode { + RelayMode::Disabled => None, + RelayMode::Default => Some(TransportConfig::Relay { + relay_map: mode.relay_map(), + is_user_defined: true, + }), + RelayMode::Staging => Some(TransportConfig::Relay { + relay_map: mode.relay_map(), + is_user_defined: true, + }), + RelayMode::Custom(relay_map) => Some(TransportConfig::Relay { + relay_map, + is_user_defined: true, + }), + } + } +} + +impl Builder { + // The ordering of public methods is reflected directly in the documentation. This is + // roughly ordered by what is most commonly needed by users. + + /// Creates a new [`Builder`] using the given [`Preset`]. + /// + /// See [`presets`] for more. + pub fn new(preset: impl Preset) -> Self { + Self::empty().preset(preset) + } + + /// Applies the given [`Preset`]. + pub fn preset(mut self, preset: impl Preset) -> Self { + self = preset.apply(self); + self + } + + /// Creates an empty builder with no address lookup services, and [`RelayMode::Disabled`]. + pub fn empty() -> Self { + let transports = vec![ + #[cfg(not(wasm_browser))] + TransportConfig::default_ipv4(), + #[cfg(not(wasm_browser))] + TransportConfig::default_ipv6(), + ]; + + Self { + secret_key: Default::default(), + alpn_protocols: Default::default(), + transport_config: QuicTransportConfig::default(), + keylog: Default::default(), + address_lookup: Default::default(), + address_lookup_user_data: Default::default(), + addr_filter: None, + proxy_url: None, + ca_tls_config: None, + #[cfg(not(wasm_browser))] + dns_resolver: None, + max_tls_tickets: DEFAULT_MAX_TLS_TICKETS, + transports, + hooks: Default::default(), + path_selector: Arc::new(BiasedRttPathSelector::default()), + portmapper_config: Default::default(), + net_report_config: Default::default(), + crypto_provider: None, + configured_addrs: Default::default(), + } + } + + // # The final constructor that everyone needs. + + /// Binds the endpoint. + pub async fn bind(self) -> Result { + let secret_key = self.secret_key.unwrap_or_else(SecretKey::generate); + + let crypto_provider = self + .crypto_provider + .ok_or_else(|| e!(BindError::InvalidCryptoProvider))?; + + let token_key = Arc::new( + RustlsTokenKey::new(&mut rand::rng(), &crypto_provider) + .ok_or_else(|| e!(BindError::InvalidCryptoProvider))?, + ); + + let span = info_span!("endpoint", id = %secret_key.public().fmt_short()); + let _guard = span.enter(); + + let tls_config = tls::TlsConfig::new( + secret_key.clone(), + self.max_tls_tickets, + crypto_provider.clone(), + ); + let static_config = StaticConfig { + server_config: tls_config.make_server_config(self.keylog)?, + client_config: tls_config.make_client_config(self.keylog)?, + tls_config, + transport_config: self.transport_config.clone(), + token_key, + token_store: Arc::new(noq::TokenMemoryCache::default()), + }; + let server_config = static_config.create_server_config(self.alpn_protocols); + + let metrics = EndpointMetrics::default(); + + let tls_config = self + .ca_tls_config + .unwrap_or_default() + .client_config(crypto_provider) + .map_err(|err| e!(BindError::InvalidCaRootConfig, err))?; + + #[cfg(not(wasm_browser))] + let dns_resolver = self.dns_resolver.unwrap_or_else(|| { + DnsResolver::builder() + .with_system_defaults() + .tls_client_config(tls_config.clone()) + .build() + }); + + let sock_opts = socket::Options { + transports: self.transports, + secret_key, + address_lookup_user_data: self.address_lookup_user_data, + proxy_url: self.proxy_url, + #[cfg(not(wasm_browser))] + dns_resolver, + server_config, + tls_config, + metrics, + hooks: self.hooks, + path_selector: self.path_selector, + portmapper_config: self.portmapper_config, + net_report_config: self.net_report_config, + static_config, + configured_addrs: self.configured_addrs, + }; + + let inner = socket::EndpointInner::bind(sock_opts) + .instrument(Span::current()) + .await?; + debug!( + id = %inner.static_config.tls_config.secret_key.public(), + iroh_version = %env!("CARGO_PKG_VERSION"), + "iroh endpoint bound" + ); + + let ep = Endpoint { + inner: Arc::new(inner), + }; + + // Add Address Lookup mechanisms + let address_lookup = ep.address_lookup().expect("just created the endpoint"); + if let Some(filter) = self.addr_filter { + address_lookup.set_addr_filter(filter); + } + for create_service in self.address_lookup { + let service = create_service.into_address_lookup(&ep)?; + address_lookup.add_boxed(service); + } + + Ok(ep) + } + + // # The very common methods everyone basically needs. + + /// Binds an IP socket at the provided socket address. + /// + /// This is an advanced API to tightly control the sockets used by the endpoint. Most + /// uses do not need to explicitly bind sockets. + /// + /// # Warning + /// + /// - The builder always comes pre-configured with an IPv4 socket to be bound on the + /// *unspecified* address: `0.0.0.0`. This is the equivalent of using `INADDR_ANY` + /// special bind address and results in a socket listening on *all* interfaces + /// available. + /// + /// - Likewise the builder always comes pre-configured with an IPv6 socket to be bound + /// on the *unspecified* address: `[::]`. This bind is allowed to fail however. + /// + /// - Adding a bind address removes the pre-configured unspecified bind address for this + /// address family. Use [`Self::bind_addr_with_opts`] to bind additional addresses without + /// replacing the default bind address. + /// + /// - This should be called at most once for each address family: once for IPv4 and/or + /// once for IPv6. Calling it multiple times for the same address family will result + /// in undefined routing behaviour. To bind multiple sockets of the same address + /// family, use [`Self::bind_addr_with_opts`]. + /// + /// # Description + /// + /// Requests a socket to be bound on a specific address, with an implied netmask of + /// `/0`. This allows restricting binding to only one network interface for a given + /// address family. + /// + /// If the port specified is already in use, binding the endpoint will fail. Using + /// port `0` in the socket address assigns a random free port. + /// + /// # Example + /// + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # #[tokio::main] + /// # async fn main() -> n0_error::Result<()> { + /// # use iroh::{Endpoint, endpoint::presets}; + /// let endpoint = Endpoint::builder(presets::N0) + /// .clear_ip_transports() + /// .bind_addr("127.0.0.1:0")? + /// .bind_addr("[::1]:0")? + /// .bind() + /// .await?; + /// # Ok(()) } + /// # } + /// ``` + #[cfg(not(wasm_browser))] + pub fn bind_addr(self, addr: A) -> Result + where + A: ToSocketAddr, + ::Err: Into, + { + self.bind_addr_with_opts(addr, BindOpts::default()) + } + + /// Binds an IP socket at the provided socket address. + /// + /// This is an advanced API to tightly control the sockets used by the endpoint. Most + /// uses do not need to explicitly bind sockets. + /// + /// # Warning + /// + /// - The builder always comes pre-configured with an IPv4 socket to be bound on the + /// *unspecified* address: `0.0.0.0`. This is the equivalent of using `INADDR_ANY` + /// special bind address and results in a socket listening on *all* interfaces + /// available. + /// + /// - Likewise the builder always comes pre-configured with an IPv6 socket to be bound + /// on the *unspecified* address: `[::]`. This bind is allowed to fail however. + /// + /// # Description + /// + /// Requests a socket to be bound on a specific address. This allows restricting binding + /// to only one network interface for a given address family. + /// + /// [`BindOpts::set_prefix_len`] **should** be used to configure the netmask of the + /// network interface. This allows outgoing datagrams that start a new network flow to + /// be sent over the socket which is attached to the subnet of the destination + /// address. If multiple sockets are bound the standard routing-table semantics are + /// used: the socket attached to the subnet with the longest prefix matching the + /// destination is used. Practically this means the smallest subnets are at the top of + /// the routing table, and the first subnet containing the destination address is + /// chosen. + /// + /// If no socket is bound to a subnet that contains the destination address, the notion + /// of "default route" is used. At most one socket per address family may be marked as + /// the default route using [`BindOpts::set_is_default_route`], and this will be used + /// for destinations not contained by the subnets of the bound sockets. This network is + /// expected to have a default gateway configured. A socket with a prefix length of `/0` + /// will be set as a "default route" implicitly, unless [`BindOpts::set_is_default_route`] + /// is set to `false` explicitly. + /// + /// Be aware that using a subnet with a prefix length of `/0` will always contain all + /// destination addresses. It is valid to configure this, but no more than one such + /// socket should be bound or the routing will be non-deterministic. + /// + /// To use a subnet with a non-zero prefix length as the default route in addition to + /// being routed when its prefix matches, use [`BindOpts::set_is_default_route`]. + /// Subnets with a prefix length of zero are always marked as default routes. + /// + /// Finally note that most outgoing datagrams are part of an existing network flow. That + /// is, they are in response to an incoming datagram. In this case the outgoing datagram + /// will be sent over the same socket as the incoming datagram was received on, and the + /// routing with the prefix length and default route as described above does not apply. + /// + /// Using port `0` in the socket address assigns a random free port. + /// + /// If the port specified is already in use, binding the endpoint will fail, unless + /// [`BindOpts::set_is_required`] is set to `false` + /// + /// # Example + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # #[tokio::main] + /// # async fn main() -> n0_error::Result<()> { + /// # use iroh::{Endpoint, endpoint::{BindOpts, presets}}; + /// let endpoint = Endpoint::builder(presets::N0) + /// .clear_ip_transports() + /// .bind_addr_with_opts("127.0.0.1:0", BindOpts::default().set_prefix_len(24))? + /// .bind_addr_with_opts("[::1]:0", BindOpts::default().set_prefix_len(48))? + /// .bind() + /// .await?; + /// # Ok(()) } + /// # } + /// ``` + #[cfg(not(wasm_browser))] + pub fn bind_addr_with_opts( + mut self, + addr: A, + opts: BindOpts, + ) -> Result + where + A: ToSocketAddr, + ::Err: Into, + { + let addr = addr.to_socket_addr().map_err(Into::into)?; + match addr { + SocketAddr::V4(addr) => { + if self + .transports + .iter() + .any(|t| t.is_ipv4_default() && t.is_user_defined()) + { + bail!(InvalidSocketAddr::DuplicateDefaultAddr); + } + + let ip_net = Ipv4Net::new(*addr.ip(), opts.prefix_len()) + .map_err(|_| e!(InvalidSocketAddr::InvalidPrefixLength))?; + self.transports.push(TransportConfig::Ip { + config: IpConfig::V4 { + ip_net, + port: addr.port(), + is_required: opts.is_required(), + is_default: opts.is_default_route(), + }, + is_user_defined: true, + }); + } + SocketAddr::V6(addr) => { + if self + .transports + .iter() + .any(|t| t.is_ipv6_default() && t.is_user_defined()) + { + bail!(InvalidSocketAddr::DuplicateDefaultAddr); + } + + let ip_net = Ipv6Net::new(*addr.ip(), opts.prefix_len()) + .map_err(|_| e!(InvalidSocketAddr::InvalidPrefixLength))?; + self.transports.push(TransportConfig::Ip { + config: IpConfig::V6 { + ip_net, + scope_id: addr.scope_id(), + port: addr.port(), + is_required: opts.is_required(), + is_default: opts.is_default_route(), + }, + is_user_defined: true, + }); + } + } + Ok(self) + } + + /// Removes all IP based transports. + #[cfg(not(wasm_browser))] + pub fn clear_ip_transports(mut self) -> Self { + self.transports + .retain(|t| !matches!(t, TransportConfig::Ip { .. })); + self + } + + /// Removes all relay based transports. + pub fn clear_relay_transports(mut self) -> Self { + self.transports + .retain(|t| !matches!(t, TransportConfig::Relay { .. })); + self + } + + /// Sets a secret key to authenticate with other peers. + /// + /// This secret key's public key will be the [`PublicKey`] of this endpoint and thus + /// also its [`EndpointId`] + /// + /// If not set, a new secret key will be generated. + /// + /// [`PublicKey`]: iroh_base::PublicKey + pub fn secret_key(mut self, secret_key: SecretKey) -> Self { + self.secret_key = Some(secret_key); + self + } + + /// Sets the [ALPN] protocols that this endpoint will accept on incoming connections. + /// + /// Not setting this will still allow creating connections, but to accept incoming + /// connections at least one [ALPN] must be set. + /// + /// Ordering matters for protocol negotiation. When an incoming connection offers multiple ALPNs, + /// the first matching ALPN will be chosen. This means that `alpns` should be ordered such + /// that the preferred protocols come first. + /// + /// [ALPN]: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation + pub fn alpns(mut self, alpn_protocols: Vec>) -> Self { + self.alpn_protocols = alpn_protocols; + self + } + + // # Methods for common customisation items. + + /// Sets the relay servers to assist in establishing connectivity. + /// + /// Relay servers are used to establish initial connection with another iroh endpoint. + /// They also perform various functions related to hole punching, see the [crate docs] + /// for more details. + /// + /// By default the [number 0] relay servers are used, see [`RelayMode::Default`]. + /// + /// When using [RelayMode::Custom], the provided `relay_map` must contain at least one + /// configured relay endpoint. If an invalid RelayMap is provided [`bind`] + /// will result in an error. + /// + /// [`bind`]: Builder::bind + /// [crate docs]: crate + /// [number 0]: https://n0.computer + pub fn relay_mode(mut self, relay_mode: RelayMode) -> Self { + let transport: Option<_> = relay_mode.into(); + match transport { + Some(transport) => { + if let Some(og) = self + .transports + .iter_mut() + .find(|t| matches!(t, TransportConfig::Relay { .. })) + { + *og = transport; + } else { + self.transports.push(transport); + } + } + None => { + self.transports + .retain(|t| !matches!(t, TransportConfig::Relay { .. })); + } + } + self + } + + /// Removes all Address Lookup services from the builder. + /// + /// If no Address Lookup is set, connecting to an endpoint without providing its + /// direct addresses or relay URLs will fail. + /// + /// See the documentation of the [`crate::address_lookup::AddressLookup`] trait for details. + pub fn clear_address_lookup(mut self) -> Self { + self.address_lookup.clear(); + self + } + + /// Adds an additional Address Lookup for this endpoint. + /// + /// Once the endpoint is created the provided [`AddressLookupBuilder::into_address_lookup`] will be + /// called. This allows Address Lookup's to finalize their configuration by e.g. using + /// the secret key from the endpoint which can be needed to sign published information. + /// + /// This method can be called multiple times and all the Address Lookup's passed in + /// will be combined using an internal instance of the + /// [`crate::address_lookup::AddressLookupServices`]. To clear all Address Lookup's, use + /// [`Self::clear_address_lookup`]. + /// + /// If no Address Lookup is set, connecting to an endpoint without providing its + /// direct addresses or relay URLs will fail. + /// + /// See the documentation of the [`crate::address_lookup::AddressLookup`] trait for details. + pub fn address_lookup(mut self, address_lookup: impl AddressLookupBuilder) -> Self { + self.address_lookup.push(Box::new(address_lookup)); + self + } + + /// Sets the address filter applied to all address data before publishing. + /// + /// This filter is applied once, at the [`AddressLookupServices`] level, before + /// distributing data to any individual address lookup service. This ensures + /// consistent filtering regardless of how the services are configured. + /// + /// [`AddressLookupServices`]: crate::address_lookup::AddressLookupServices + pub fn addr_filter(mut self, filter: AddrFilter) -> Self { + self.addr_filter = Some(filter); + self + } + + /// Clears the address filter, allowing all addresses to be published. + /// + /// This removes any filter previously set via [`Self::addr_filter`], including + /// filters set by presets. + pub fn clear_addr_filter(mut self) -> Self { + self.addr_filter = None; + self + } + + /// Sets the initial user-defined data to be published in Address Lookup's for this node. + /// + /// When using Address Lookup's, this string of [`UserData`] will be published together + /// with the endpoint's addresses and relay URL. When other endpoints discover this endpoint, + /// they retrieve the [`UserData`] in addition to the addressing info. + /// + /// Iroh itself does not interpret the user-defined data in any way, it is purely left + /// for applications to parse and use. + pub fn user_data_for_address_lookup(mut self, user_data: UserData) -> Self { + self.address_lookup_user_data = Some(user_data); + self + } + + /// Adds an external address on which this endpoint is directly reachable. + /// + /// This address will be advertised to peers together with any discovered external addresses + /// and will be used in NAT traversal and to establish direct connections. + /// + /// Can be called multiple times. See also [`Endpoint::add_external_addr`] for + /// adding addresses at runtime. + pub fn external_addr(mut self, addr: SocketAddr) -> Self { + self.configured_addrs.insert(addr); + self + } + + // # Methods for more specialist customisation. + + /// Sets a custom [`QuicTransportConfig`] for this endpoint. + /// + /// The transport config contains parameters governing the QUIC state machine. + /// + /// If unset, the default config is used. Default values should be suitable for most + /// internet applications. Applications protocols which forbid remotely-initiated + /// streams should set `max_concurrent_bidi_streams` and `max_concurrent_uni_streams` to + /// zero. + /// + /// Please be aware that changing some settings may have adverse effects on establishing + /// and maintaining direct connections. + pub fn transport_config(mut self, transport_config: QuicTransportConfig) -> Self { + self.transport_config = transport_config; + self + } + + /// Optionally sets a custom DNS resolver to use for this endpoint. + /// + /// The DNS resolver is used to resolve relay hostnames, and endpoint addresses if + /// [`crate::address_lookup::DnsAddressLookup`] is configured. + /// + /// By default, a new DNS resolver is created which is configured to use the + /// host system's DNS configuration. You can pass a custom instance of [`DnsResolver`] + /// here to use a differently configured DNS resolver for this endpoint, or to share + /// a [`DnsResolver`] between multiple endpoints. + #[cfg(not(wasm_browser))] + pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self { + self.dns_resolver = Some(dns_resolver); + self + } + + /// Sets an explicit proxy url to proxy all HTTP(S) traffic through. + pub fn proxy_url(mut self, url: Url) -> Self { + self.proxy_url.replace(url); + self + } + + /// Sets the proxy url from the environment, in this order: + /// + /// - `HTTP_PROXY` + /// - `http_proxy` + /// - `HTTPS_PROXY` + /// - `https_proxy` + pub fn proxy_from_env(mut self) -> Self { + self.proxy_url = proxy_url_from_env(); + self + } + + /// Sets the trusted CA root certificates for non-iroh TLS connections. + /// + /// These Certificate Authority roots are used as trust anchors for verifying + /// the validity of TLS certificates presented by external services, such as + /// iroh relays, pkarr servers, or DNS-over-HTTPS resolvers. + /// They don't need to be trusted for the integrity or authenticity of native + /// iroh connections, which rely on iroh's own cryptographic authentication mechanisms. + pub fn ca_tls_config(mut self, ca_tls_config: CaTlsConfig) -> Self { + self.ca_tls_config = Some(ca_tls_config); + self + } + + /// Renamed to [`Builder::ca_tls_config`]. + #[deprecated(since = "1.0.0", note = "Renamed to `ca_tls_config`")] + pub fn ca_roots_config(self, ca_roots_config: CaTlsConfig) -> Self { + self.ca_tls_config(ca_roots_config) + } + + /// Enables saving the TLS pre-master key for connections. + /// + /// This key should normally remain secret but can be useful to debug networking issues + /// by decrypting captured traffic. + /// + /// If *keylog* is `true` then setting the `SSLKEYLOGFILE` environment variable to a + /// filename will result in this file being used to log the TLS pre-master keys. + pub fn keylog(mut self, keylog: bool) -> Self { + self.keylog = keylog; + self + } + + /// Set the maximum number of TLS tickets to cache. + /// + /// Set this to a larger value if you want to do 0rtt connections to a large + /// number of clients. + /// + /// The default is 256, taking about 150 KiB in memory. + pub fn max_tls_tickets(mut self, n: usize) -> Self { + self.max_tls_tickets = n; + self + } + + /// Specify the rustls cryptography to use for all TLS operations. + /// + /// This includes + /// - TLS for encryption and authentication of iroh connections themselves, but also + /// - HTTPS connections to relays + /// - Pkarr relay publishing HTTPS connections + /// - and any other Address Lookup services that decide to use [`Endpoint::tls_config`]. + /// + /// The two most common crypto providers in use today are `ring` as well as `aws-lc-rs`. + /// + /// If either the `tls-ring` or `tls-aws-lc-rs` feature is set in iroh, this function doesn't + /// need to be called. + /// + /// If none of these features are set, then calling this function in the builder is mandatory. + pub fn crypto_provider(mut self, crypto_provider: Arc) -> Self { + self.crypto_provider = Some(crypto_provider); + self + } + + /// Install hooks onto the endpoint. + /// + /// Endpoint hooks intercept the connection establishment process of an [`Endpoint`]. + /// + /// You can install multiple [`EndpointHooks`] by calling this function multiple times. + /// Order matters: hooks are invoked in the order they were installed onto the endpoint + /// builder. Once a hook returns reject, further processing + /// is aborted and other hooks won't be invoked. + /// + /// See [`EndpointHooks`] for details on the possible interception points in the connection lifecycle. + pub fn hooks(mut self, hooks: impl EndpointHooks + 'static) -> Self { + self.hooks.push(hooks); + self + } + + /// Configures the portmapper service (UPnP, PCP, NAT-PMP). + /// + /// Defaults to [`PortmapperConfig::Enabled`]. Pass + /// [`PortmapperConfig::Disabled`] to avoid gateway probing (e.g. if it + /// triggers firewall prompts). + pub fn portmapper_config(mut self, config: PortmapperConfig) -> Self { + self.portmapper_config = config; + self + } + + /// Configures the net report. + /// + /// The net report component is responsible for figuring out if and how the endpoint is connected to the internet. + /// It does this by doing various probes to the configured relay servers to get public addresses, NAT behaviour, and + /// relay latencies. In addition it tries to detect captive portals. + /// + /// Some non-essential features of the net report component can be disabled via this configuration. + pub fn net_report_config(mut self, config: NetReportConfig) -> Self { + self.net_report_config = config; + self + } + + /// Adds a custom transport to the endpoint. + /// + ///
+ /// + /// This API is unstable and gated behind the `unstable-custom-transport` feature. + /// It is not covered by semantic versioning guarantees and may change in any release + /// without a major version bump. + /// + ///
+ #[cfg(feature = "unstable-custom-transports")] + pub fn add_custom_transport(mut self, factory: Arc) -> Self { + self.transports.push(TransportConfig::Custom(factory)); + self + } + + /// Sets a custom [`PathSelector`] for this endpoint. + /// + /// The path selector decides which path to use among the candidate paths to a + /// remote endpoint. By default iroh uses a built-in selector that sorts paths by + /// biased RTT (with IPv6 preferred over IPv4 and relay treated as backup) and is + /// sticky to avoid flapping. Pass a custom [`PathSelector`] here to override that + /// policy — for example, to make a custom transport always win over IP. + /// + /// Takes an `Arc` so the same selector instance can be shared + /// across multiple endpoints if desired. See `examples/custom-transport.rs` for + /// an example implementation. + /// + ///
+ /// + /// This API is unstable and gated behind the `unstable-custom-transport` feature. + /// It is not covered by semantic versioning guarantees and may change in any release + /// without a major version bump. + /// + ///
+ /// + /// [`PathSelector`]: socket::remote_map::PathSelector + #[cfg(feature = "unstable-custom-transports")] + pub fn path_selector(mut self, selector: Arc) -> Self { + self.path_selector = selector; + self + } +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub enum EndpointError { + #[error("Endpoint is closed")] + Closed, +} + +/// Controls an iroh endpoint, establishing connections with other endpoints. +/// +/// This is the main API interface to create connections to, and accept connections from +/// other iroh endpoints. The connections are peer-to-peer and encrypted, a Relay server is +/// used to make the connections reliable. See the [crate docs] for a more detailed +/// overview of iroh. +/// +/// It is recommended to only create a single instance per application. This ensures all +/// the connections made share the same peer-to-peer connections to other iroh endpoints, +/// while still remaining independent connections. This will result in more optimal network +/// behaviour. +/// +/// The endpoint is created using the [`Builder`], which can be created using +/// [`Endpoint::builder`]. +/// +/// Once an endpoint exists, new connections are typically created using the +/// [`Endpoint::connect`] and [`Endpoint::accept`] methods. Once established, the +/// [`Connection`] gives access to most [QUIC] features. Individual streams to send data to +/// the peer are created using the [`Connection::open_bi`], [`Connection::accept_bi`], +/// [`Connection::open_uni`] and [`Connection::accept_uni`] functions. +/// +/// Note that due to the light-weight properties of streams a stream will only be accepted +/// once the initiating peer has sent some data on it. +/// +/// # Usage on Android +/// +/// The endpoint's default [`DnsResolver`] reads the system DNS configuration +/// through JNI, which needs a JVM context published to [`ndk_context`]. Apps +/// should initialize that context before constructing the endpoint. See +/// [`iroh_dns::install_android_jni_context`] for details (the function is also +/// exported as `iroh::dns::install_android_jni_context`). +/// +/// If no JNI context is installed, iroh relies on panic unwinding to detect +/// the error, and will then use the fallback nameservers instead, subject to the +/// resolver's [`FallbackMode`]. Note that if your compilation profile sets +/// `panic = "abort"`, this can't work, and thus your app will panic if using a +/// default `DnsResolver` without first initializing the JNI context. +/// +/// [QUIC]: https://quicwg.org +/// [`DnsResolver`]: crate::dns::DnsResolver +/// [`FallbackMode`]: crate::dns::FallbackMode +/// [`ndk_context`]: https://docs.rs/ndk-context +/// [`iroh_dns::install_android_jni_context`]: https://docs.rs/iroh-dns/latest/iroh_dns/fn.install_android_jni_context.html +// The last link can't be a normal doclink, because #[cfg(doc)] can't cross crate boundaries unfortunately. +#[derive(Clone, Debug)] +pub struct Endpoint { + inner: Arc, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta, from_sources)] +#[non_exhaustive] +#[allow(private_interfaces)] +pub enum ConnectWithOptsError { + #[error("Connecting to ourself is not supported")] + SelfConnect, + #[error("No addressing information available")] + NoAddress { source: AddressLookupFailed }, + #[error("Unable to connect to remote")] + Noq { + #[error(std_err)] + source: QuicConnectError, + }, + #[error("Internal consistency error")] + InternalConsistencyError { + /// Private source type, cannot be created publicly. + source: RemoteStateActorStoppedError, + }, + #[error("Connection was rejected locally")] + LocallyRejected, + #[error("Endpoint is closed")] + EndpointClosed, + #[error("Invalid ALPN")] + InvalidAlpn, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta, from_sources)] +#[non_exhaustive] +pub enum ConnectError { + #[error(transparent)] + Connect { source: ConnectWithOptsError }, + #[error(transparent)] + Connecting { source: ConnectingError }, + #[error(transparent)] + Connection { + #[error(std_err)] + source: ConnectionError, + }, +} + +impl Endpoint { + // The ordering of public methods is reflected directly in the documentation. This is + // roughly ordered by what is most commonly needed by users, but grouped in similar + // items. + + // # Methods relating to construction. + + /// Returns the builder for an [`Endpoint`], with the given [`Preset`] configuration. + pub fn builder(preset: impl Preset) -> Builder { + Builder::new(preset) + } + + /// Constructs a default [`Endpoint`] using the provided [`Preset`] and binds it immediately. + pub async fn bind(preset: impl Preset) -> Result { + Self::builder(preset).bind().await + } + + /// Sets the list of accepted ALPN protocols. + /// + /// Ordering matters for protocol negotiation. When an incoming connection offers multiple ALPNs, + /// the first matching ALPN will be chosen. This means that `alpns` should be ordered such + /// that the preferred protocols come first. + /// + /// This will only affect new incoming connections. + /// Note that this *overrides* the current list of ALPNs. + /// + /// If the endpoint is closed, this method will log a warning and ignore + /// the request to set new ALPNs. + pub fn set_alpns(&self, alpns: Vec>) { + if self.is_closed() { + warn!("Attempting to set ALPNs for a closed endpoint. Ignoring."); + return; + } + let server_config = self.inner.static_config.create_server_config(alpns); + self.inner + .noq_endpoint() + .set_server_config(Some(server_config)); + } + + /// Adds the provided configuration to the [`RelayMap`]. + /// + /// Replacing and returning any existing configuration for [`RelayUrl`]. + /// + /// Will also return `None` if the endpoint is closed. + pub async fn insert_relay( + &self, + relay: RelayUrl, + config: Arc, + ) -> Option> { + if self.is_closed() { + return None; + } + self.inner.insert_relay(relay, config).await + } + + /// Removes the configuration from the [`RelayMap`] for the provided [`RelayUrl`]. + /// + /// Returns any existing configuration if it exists. Will also return `None` if the endpoint is closed. + pub async fn remove_relay(&self, relay: &RelayUrl) -> Option> { + if self.is_closed() { + return None; + } + self.inner.remove_relay(relay).await + } + + /// Adds an external address on which this endpoint is directly reachable. + /// + /// This address will be advertised to peers together with any discovered external addresses + /// and will be used in NAT traversal and to establish direct connections. + /// + /// See also [`Builder::external_addr`] for setting addresses at build time. + pub async fn add_external_addr(&self, addr: SocketAddr) { + if self.is_closed() { + warn!("Attempting to add external addr for a closed endpoint. Ignoring."); + return; + } + self.inner.add_external_addr(addr).await; + } + + /// Removes a configured external address. Returns `true` if it was present. + pub async fn remove_external_addr(&self, addr: &SocketAddr) -> bool { + if self.is_closed() { + return false; + } + self.inner.remove_external_addr(addr).await + } + + // # Methods for establishing connectivity. + + /// Connects to a remote [`Endpoint`]. + /// + /// A value that can be converted into an [`EndpointAddr`] is required. This can be either an + /// [`EndpointAddr`] or an [`EndpointId`]. + /// + /// The [`EndpointAddr`] must contain the [`EndpointId`] to dial and may also contain a [`RelayUrl`] + /// and direct addresses. If direct addresses are provided, they will be used to try and + /// establish a direct connection without involving a relay server. + /// + /// If neither a [`RelayUrl`] or direct addresses are configured in the [`EndpointAddr`] it + /// may still be possible a connection can be established. This depends on which, if any, + /// [`crate::address_lookup::AddressLookup`]s were configured using [`Builder::address_lookup`]. The Address Lookup + /// service will also be used if the remote endpoint is not reachable on the provided direct + /// addresses and there is no [`RelayUrl`]. + /// + /// If addresses or relay servers are neither provided nor can be discovered, the + /// connection attempt will fail with an error. + /// + /// The `alpn`, or application-level protocol identifier, is also required. The remote + /// endpoint must support this `alpn`, otherwise the connection attempt will fail with + /// an error. + /// + /// [`RelayUrl`]: crate::RelayUrl + pub async fn connect( + &self, + endpoint_addr: impl Into, + alpn: &[u8], + ) -> Result { + let endpoint_addr = endpoint_addr.into(); + let remote = endpoint_addr.id; + let connecting = self + .connect_with_opts(endpoint_addr, alpn, Default::default()) + .await?; + let conn = connecting.await?; + + debug!( + me = %self.id().fmt_short(), + remote = %remote.fmt_short(), + alpn = %String::from_utf8_lossy(alpn), + "Connection established." + ); + Ok(conn) + } + + /// Starts a connection attempt with a remote [`Endpoint`]. + /// + /// Like [`Endpoint::connect`] (see also its docs for general details), but allows for a more + /// advanced connection setup with more customization in two aspects: + /// 1. The returned future resolves to a [`Connecting`], which can be further processed into + /// a [`Connection`] by awaiting, or alternatively allows connecting with 0-RTT via + /// [`Connecting::into_0rtt`]. + /// **Note:** Please read the documentation for `into_0rtt` carefully to assess + /// security concerns. + /// 2. The [`QuicTransportConfig`] for the connection can be modified via the provided + /// [`ConnectOptions`]. + /// **Note:** Please be aware that changing transport config settings may have adverse effects on + /// establishing and maintaining direct connections. Carefully test settings you use and + /// consider this currently as still rather experimental. + #[instrument(name = "connect", skip_all, fields( + me = %self.id().fmt_short(), + remote = tracing::field::Empty, + alpn = %String::from_utf8_lossy(alpn).to_string(), + ))] + pub async fn connect_with_opts( + &self, + endpoint_addr: impl Into, + alpn: &[u8], + options: ConnectOptions, + ) -> Result { + if self.is_closed() { + return Err(e!(ConnectWithOptsError::EndpointClosed)); + } + + let endpoint_addr: EndpointAddr = endpoint_addr.into(); + let endpoint_id = endpoint_addr.id; + + Span::current().record("remote", tracing::field::display(endpoint_id.fmt_short())); + + if let BeforeConnectOutcome::Reject = + self.inner.hooks.before_connect(&endpoint_addr, alpn).await + { + return Err(e!(ConnectWithOptsError::LocallyRejected)); + } + + // Connecting to ourselves is not supported. + ensure!(endpoint_id != self.id(), ConnectWithOptsError::SelfConnect); + ensure!(!alpn.is_empty(), ConnectWithOptsError::InvalidAlpn); + + event!( + target: "iroh::_events::conn::connecting", + tracing::Level::DEBUG, + remote_id = %endpoint_id.fmt_short(), + alpn = %String::from_utf8_lossy(alpn), + ); + + debug!( + relay_url = ?endpoint_addr.relay_urls().next().cloned(), + ip_addresses = ?endpoint_addr.ip_addrs().cloned().collect::>(), + "connecting", + ); + + let mapped_addr = self.inner.resolve_remote(endpoint_addr).await??; + + let transport_config = options + .transport_config + .map(|cfg| cfg.to_inner_arc()) + .unwrap_or(self.inner.static_config.transport_config.to_inner_arc()); + + // Start connecting via noq. This will time out after 10 seconds if no reachable + // address is available. + + let mut alpn_protocols = vec![alpn.to_vec()]; + alpn_protocols.extend(options.additional_alpns); + let client_config = self + .inner + .static_config + .create_client_config(alpn_protocols, transport_config.clone()); + + let dest_addr = mapped_addr.private_socket_addr(); + let server_name = &tls::name::encode(endpoint_id); + let connect = + self.inner + .noq_endpoint() + .connect_with(client_config, dest_addr, server_name)?; + + Ok(Connecting::new(connect, self.clone(), endpoint_id)) + } + + /// Accepts an incoming connection on the endpoint. + /// + /// Only connections with the ALPNs configured in [`Builder::alpns`] will be accepted. + /// If multiple ALPNs have been configured the ALPN can be inspected before accepting + /// the connection using [`Connecting::alpn`]. + /// + /// The returned future will yield `None` if the endpoint is closed by calling + /// [`Endpoint::close`]. + pub fn accept(&self) -> Accept<'_> { + Accept { + inner: self.inner.noq_endpoint().accept(), + ep: self.clone(), + } + } + + // # Getter methods for properties of this Endpoint itself. + + /// Returns the secret_key of this endpoint. + pub fn secret_key(&self) -> &SecretKey { + &self.inner.static_config.tls_config.secret_key + } + + /// Returns the endpoint id of this endpoint. + /// + /// This ID is the unique addressing information of this endpoint and other peers must know + /// it to be able to connect to this endpoint. + pub fn id(&self) -> EndpointId { + self.inner.static_config.tls_config.secret_key.public() + } + + /// Returns the current [`EndpointAddr`]. + /// As long as the endpoint was able to bind to a network interface, some + /// local addresses will be available. + /// + /// The state of other fields depends on the state of networking and connectivity. + /// Use the [`Endpoint::online`] method to ensure that the endpoint is considered + /// "online" (has contacted a relay server) before calling this method, if you want + /// to ensure that the `EndpointAddr` will contain enough information to allow this endpoint + /// to be dialable by a remote endpoint over the internet. + /// + /// You can use the [`Endpoint::watch_addr`] method to get updates when the `EndpointAddr` + /// changes. + pub fn addr(&self) -> EndpointAddr { + self.watch_addr().get() + } + + /// Returns a [`Watcher`] for the current [`EndpointAddr`] for this endpoint. + /// + /// The observed [`EndpointAddr`] will have the current [`RelayUrl`] and direct addresses. + /// + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # async fn wrapper() -> n0_error::Result<()> { + /// use iroh::{Endpoint, Watcher, endpoint::presets}; + /// + /// let endpoint = Endpoint::builder(presets::N0) + /// .alpns(vec![b"my-alpn".to_vec()]) + /// .bind() + /// .await?; + /// let endpoint_addr = endpoint.watch_addr().get(); + /// # let _ = endpoint_addr; + /// # Ok(()) + /// # } + /// # } + /// ``` + /// + /// The [`Endpoint::online`] method can be used as a convenience method to + /// understand if the endpoint has ever been considered "online". But after + /// that initial call to [`Endpoint::online`], to understand if your + /// endpoint is no longer able to be connected to by endpoints outside + /// of the private or local network, watch for changes in its [`EndpointAddr`]. + /// If there are no `addrs` in the [`EndpointAddr`], you may not be dialable by other endpoints + /// on the internet. + /// + /// The `EndpointAddr` will change as: + /// - network conditions change + /// - the endpoint connects to a relay server + /// - the endpoint changes its preferred relay server + /// - more addresses are discovered for this endpoint + /// + /// ## Closing behavior + /// + /// The returned watcher only becomes disconnected once the last clone of the [`Endpoint`] + /// is dropped. Closing the endpoint does not disconnect the watcher. Thus, a stream created + /// via [`Watcher::stream`] only terminates once the endpoint is fully dropped. To stop a task + /// that loops over a watcher stream once the endpoint stops, combine with [`Self::closed`]: + /// + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # use iroh::{Watcher, Endpoint, endpoint::presets}; + /// # use n0_future::StreamExt; + /// # use tracing::info; + /// # async fn wrapper() -> n0_error::Result<()> { + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// // We want to watch address changes in a different task, and stop our task + /// // once the endpoint stops. + /// let mut addr_stream = endpoint.watch_addr().stream(); + /// let endpoint_closed = endpoint.closed(); + /// tokio::spawn(endpoint_closed.run_until(async move { + /// while let Some(addr) = addr_stream.next().await { + /// info!("our address changed: {addr:?}"); + /// } + /// info!("endpoint closed"); + /// })); + /// // Do fancy things, then close the endpoint. + /// // Our task above will stop even if there are still clones of `Endpoint` alive somewhere. + /// endpoint.close().await; + /// # Ok(()) + /// # } + /// # } + /// ``` + /// + /// [`RelayUrl`]: crate::RelayUrl + #[cfg(not(wasm_browser))] + pub fn watch_addr(&self) -> impl n0_watcher::Watcher + use<> { + let watch_addrs = self.inner.ip_addrs(); + let watch_relay = self.inner.home_relay(); + let endpoint_id = self.id(); + + watch_addrs.or(watch_relay).map(move |(addrs, relays)| { + EndpointAddr::from_parts( + endpoint_id, + relays + .into_iter() + .map(TransportAddr::Relay) + .chain(addrs.into_iter().map(|x| TransportAddr::Ip(x.addr))), + ) + }) + } + + /// Returns a [`Watcher`] for the current [`EndpointAddr`] for this endpoint. + /// + /// When compiled to Wasm, this function returns a watcher that initializes + /// with an [`EndpointAddr`] that only contains a relay URL, but no direct addresses, + /// as there are no APIs for directly using sockets in browsers. + /// + /// The returned watcher only becomes disconnected once the last clone of the [`Endpoint`] + /// is dropped. Closing the endpoint does not disconnect the watcher. Thus, a stream created + /// via [`Watcher::stream`] only terminates once the endpoint stops. If you want to stop a + /// task once the endpoint stops combine with [`Self::closed`]. + #[cfg(wasm_browser)] + pub fn watch_addr(&self) -> impl n0_watcher::Watcher + use<> { + // In browsers, there will never be any direct addresses, so we wait + // for the home relay instead. This makes the `EndpointAddr` have *some* way + // of connecting to us. + let watch_relay = self.inner.home_relay(); + let endpoint_id = self.id(); + watch_relay.map(move |mut relays| { + EndpointAddr::from_parts(endpoint_id, relays.into_iter().map(TransportAddr::Relay)) + }) + } + + /// A convenience method that waits for the endpoint to be considered "online". + /// + /// This currently means at least one relay server has completed its + /// connection handshake (i.e. the endpoint is registered and reachable + /// via that relay). Merely selecting a relay URL is not sufficient. + /// + /// If no relays are configured, this will pend forever. + /// + /// This has no timeout, so if that is needed, you need to wrap it in a + /// timeout. We recommend using a timeout close to + /// [`crate::NET_REPORT_TIMEOUT`]s, so you can be sure that at least one + /// net report has been attempted. + /// + /// To understand if the endpoint has gone back "offline", + /// you must use the [`Endpoint::watch_addr`] method, to + /// get information on the current relay and direct address information. + /// + /// In the common case where the endpoint's configured relay servers are + /// only accessible via a wide area network (WAN) connection, this method + /// will await indefinitely when the endpoint has no WAN connection. If you're + /// writing an app that's designed to work without a WAN connection, defer + /// any calls to `online` as long as possible, or avoid calling `online` + /// entirely. + /// + /// The online method does not interact with [`crate::address_lookup::AddressLookup`] + /// services, which means that any Address Lookup that relies on a WAN + /// connection is independent of the endpoint's online status. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # #[tokio::main] + /// # async fn main() -> n0_error::Result<()> { + /// # use iroh::{Endpoint, endpoint::presets}; + /// // After this await returns, the endpoint is bound to a local socket. + /// // It can be dialed, but almost certainly hasn't finished picking a + /// // relay. + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// + /// // After this await returns we have a connection to at least one relay + /// // and holepunching should work as expected. + /// endpoint.online().await; + /// # Ok(()) } + /// # } + /// ``` + pub async fn online(&self) { + let mut watcher = self.inner.home_relay_status(); + let mut value = watcher.get(); + loop { + if value.into_iter().any(|status| status.is_connected()) { + return; + } + value = match watcher.updated().await { + Ok(value) => value, + Err(_disconnected) => { + std::future::pending::<()>().await; + break; + } + } + } + } + + /// Returns a [`Watcher`] over the connection status of the endpoint's home relays. + /// + /// The watched value has one entry per home relay whose URL is known, + /// and is empty when no relays are configured or before the endpoint has + /// selected a home relay from the list of configured relays. + /// The watcher updates whenever any home relay's connection status changes. + /// See [`RelayStatus`] for the information available on each entry. + /// + /// This may be used to observe connection failures to the home relay: + /// [`RelayStatus::last_error`] reports the most recent error, and + /// [`RelayStatus::auth_denied_reason`] singles out the case of the relay + /// server denying the endpoint's authentication. + /// + /// The returned watcher only becomes disconnected once the last clone of + /// the [`Endpoint`] is dropped. Closing the endpoint does not disconnect + /// the watcher. To stop a task once the endpoint stops, combine with + /// [`Self::closed`]. + pub fn home_relay_status(&self) -> impl Watcher> + use<> { + self.inner.home_relay_status() + } + + /// Returns a [`Watcher`] for any net report runs from this [`Endpoint`]. + /// + ///
+ /// + /// This API is unstable and gated behind the `unstable-net-report` feature. + /// It is not covered by semantic versioning guarantees and may change in any release + /// without a major version bump. + /// + ///
+ /// + /// A net report checks the network conditions of the [`Endpoint`], such as + /// whether it is connected to the internet via IPv4 and/or IPv6, its NAT + /// status, its latency to the relay servers, and its public addresses. + /// + /// The [`Endpoint`] continuously runs net reports to monitor if network + /// conditions have changed. This [`Watcher`] will return the latest + /// net report. + /// + /// When issuing the first call to this method the first report might + /// still be underway, in this case the [`Watcher`] might not be initialized + /// with [`Some`] value yet. Once the net report has been successfully + /// run, the [`Watcher`] will always return [`Some`] immediately, which + /// is the most recently run net report. + /// + /// The returned watcher only becomes disconnected once the last clone of the [`Endpoint`] + /// is dropped. Closing the endpoint does not disconnect the watcher. Thus, a stream created + /// via [`Watcher::stream`] only terminates once the endpoint stops. If you want to stop a + /// task once the endpoint stops combine with [`Self::closed`]. + /// + /// # Examples + /// + /// To get the first report use [`Watcher::initialized`]: + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// use iroh::{Endpoint, Watcher as _, endpoint::presets}; + /// + /// # let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + /// # rt.block_on(async move { + /// let ep = Endpoint::bind(presets::N0).await.unwrap(); + /// let _report = ep.net_report().initialized().await; + /// # }); + /// # } + /// ``` + #[cfg(feature = "unstable-net-report")] + pub fn net_report(&self) -> impl Watcher> + use<> { + self.inner.net_report() + } + + /// Returns the local socket addresses on which the underlying sockets are bound. + /// + /// The [`Endpoint`] always binds on an IPv4 address and also tries to bind on an IPv6 + /// address if available. + #[cfg(not(wasm_browser))] + pub fn bound_sockets(&self) -> Vec { + self.inner + .local_addr() + .into_iter() + .filter_map(|addr| addr.into_socket_addr()) + .collect() + } + + // # Methods for less common getters. + // + // Partially they return things passed into the builder. + + /// Returns the DNS resolver used in this [`Endpoint`]. + /// + /// # Errors + /// + /// Returns an `EndpointError::Closed` error if the endpoint is closed. + /// + /// See [`Builder::dns_resolver`]. + #[cfg(not(wasm_browser))] + pub fn dns_resolver(&self) -> Result<&DnsResolver, EndpointError> { + if self.is_closed() { + return Err(e!(EndpointError::Closed)); + } + Ok(self.inner.dns_resolver()) + } + + /// Returns the [`rustls::ClientConfig`] used by the endpoint for connecting to external services. + /// + /// This might be useful for address lookup services or other functions + /// that want to use the same trust anchors as iroh does for verifying the + /// validity of TLS certificates presented by external services. + /// + /// Note that this TLS config is unrelated to how iroh validates the authenticity + /// of iroh connections itself. + /// + /// The config is based on the trust anchors set via [`Builder::ca_tls_config`]. + pub fn tls_config(&self) -> &rustls::ClientConfig { + &self.inner.tls_config + } + + /// Returns the Address Lookup service, if configured. + /// + /// # Errors + /// + /// Returns a `EndpointError::Closed` error if the endpoint is closed. + /// + /// See [`Builder::address_lookup`]. + pub fn address_lookup(&self) -> Result<&AddressLookupServices, EndpointError> { + if self.is_closed() { + return Err(e!(EndpointError::Closed)); + } + Ok(self.inner.address_lookup()) + } + + /// Returns metrics collected for this endpoint. + /// + /// The endpoint internally collects various metrics about its operation. + /// The returned [`EndpointMetrics`] struct contains all of these metrics. + /// + /// You can access individual metrics directly by using the public fields: + /// ```rust + /// # use std::collections::BTreeMap; + /// # use iroh::endpoint::{Endpoint, presets}; + /// # async fn wrapper() -> n0_error::Result<()> { + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// assert_eq!(endpoint.metrics().socket.recv_datagrams.get(), 0); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`EndpointMetrics`] implements [`MetricsGroupSet`], and each field + /// implements [`MetricsGroup`]. These traits provide methods to iterate over + /// the groups in the set, and over the individual metrics in each group, without having + /// to access each field manually. With these methods, it is straightforward to collect + /// all metrics into a map or push their values to a metrics collector. + /// + /// For example, the following snippet collects all metrics into a map: + /// ```rust + /// # use std::collections::BTreeMap; + /// # use iroh_metrics::{Metric, MetricsGroup, MetricValue, MetricsGroupSet}; + /// # use iroh::endpoint::{Endpoint, presets}; + /// # async fn wrapper() -> n0_error::Result<()> { + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// let metrics: BTreeMap = endpoint + /// .metrics() + /// .iter() + /// .map(|(group, metric)| { + /// let name = [group, metric.name()].join(":"); + /// (name, metric.value()) + /// }) + /// .collect(); + /// + /// assert_eq!(metrics["socket:recv_datagrams"], MetricValue::Counter(0)); + /// # Ok(()) + /// # } + /// ``` + /// + /// The metrics can also be encoded into the OpenMetrics text format, as used by Prometheus. + /// To do so, use the [`iroh_metrics::Registry`], add the endpoint metrics to the + /// registry with [`Registry::register_all`], and encode the metrics to a string with + /// [`encode_openmetrics_to_string`]: + /// ```rust + /// # use iroh_metrics::{Registry, MetricsSource}; + /// # use iroh::endpoint::{Endpoint, presets}; + /// # async fn wrapper() -> n0_error::Result<()> { + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// let mut registry = Registry::default(); + /// registry.register_all(endpoint.metrics()); + /// let s = registry.encode_openmetrics_to_string()?; + /// assert!(s.contains(r#"TYPE socket_recv_datagrams counter"#)); + /// assert!(s.contains(r#"socket_recv_datagrams_total 0"#)); + /// # Ok(()) + /// # } + /// ``` + /// + /// Through a registry, you can also add labels or prefixes to metrics with + /// [`Registry::sub_registry_with_label`] or [`Registry::sub_registry_with_prefix`]. + /// Furthermore, [`iroh_metrics::service`] provides functions to easily start services + /// to serve the metrics with a HTTP server, dump them to a file, or push them + /// to a Prometheus gateway. + /// + /// For example, the following snippet launches an HTTP server that serves the metrics in the + /// OpenMetrics text format: + /// ```no_run + /// # use std::sync::{Arc, RwLock}; + /// # use iroh_metrics::{Registry, MetricsSource}; + /// # use iroh::endpoint::{Endpoint, presets}; + /// # use n0_error::{StackResultExt, StdResultExt}; + /// # async fn wrapper() -> n0_error::Result<()> { + /// // Create a registry, wrapped in a read-write lock so that we can register and serve + /// // the metrics independently. + /// let registry = Arc::new(RwLock::new(Registry::default())); + /// // Spawn an OpenMetrics HTTP server backed by the registry. + /// let addr = "0.0.0.0:9100".parse().unwrap(); + /// let metrics_server = iroh_metrics::service::MetricsServer::spawn(addr, registry.clone()) + /// .await + /// .std_context("spawn metrics server")?; + /// + /// // Spawn an endpoint and add the metrics to the registry. + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// registry.write().unwrap().register_all(endpoint.metrics()); + /// + /// // Fetch the metrics via HTTP. + /// let res = reqwest::get("http://localhost:9100/metrics") + /// .await + /// .std_context("get")? + /// .text() + /// .await + /// .std_context("text")?; + /// + /// assert!(res.contains(r#"TYPE socket_recv_datagrams counter"#)); + /// assert!(res.contains(r#"socket_recv_datagrams_total 0"#)); + /// # metrics_server.shutdown().await; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Registry`]: iroh_metrics::Registry + /// [`Registry::register_all`]: iroh_metrics::Registry::register_all + /// [`Registry::sub_registry_with_label`]: iroh_metrics::Registry::sub_registry_with_label + /// [`Registry::sub_registry_with_prefix`]: iroh_metrics::Registry::sub_registry_with_prefix + /// [`encode_openmetrics_to_string`]: iroh_metrics::MetricsSource::encode_openmetrics_to_string + /// [`MetricsGroup`]: iroh_metrics::MetricsGroup + /// [`MetricsGroupSet`]: iroh_metrics::MetricsGroupSet + #[cfg(feature = "metrics")] + pub fn metrics(&self) -> &EndpointMetrics { + &self.inner.metrics + } + + /// Returns addressing information about a recently used remote endpoint. + /// + /// The returned [`RemoteInfo`] contains a list of all transport addresses for the remote + /// that we know about. This is a snapshot in time and not a watcher. + /// + /// Returns `None` if the endpoint doesn't have information about the remote or if the endpoint is closed. + /// When remote endpoints are no longer used, our endpoint will keep information around + /// for a little while, and then drop it. Afterwards, this will return `None`. + pub async fn remote_info(&self, endpoint_id: EndpointId) -> Option { + if self.is_closed() { + return None; + } + self.inner.remote_info(endpoint_id).await + } + + // # Methods for less common state updates. + + /// Notifies the system of potential network changes. + /// + /// On many systems iroh is able to detect network changes by itself, however + /// some systems like android do not expose this functionality to native code. + /// Android does however provide this functionality to Java code. This + /// function allows for notifying iroh of any potential network changes like + /// this. + /// + /// Even when the network did not change, or iroh was already able to detect + /// the network change itself, there is no harm in calling this function. + /// + /// If the endpoint is closed, this method will log a warning and ignore the request. + pub async fn network_change(&self) { + if self.is_closed() { + debug!("Attempting to notify a closed endpoint about a network change. Ignoring."); + return; + } + self.inner.network_change().await; + } + + // # Methods to update internal state. + + /// Sets the initial user-defined data to be published in Address Lookups for this endpoint. + /// + /// If the user-defined data passed to this function is different to the previous one, + /// the endpoint will republish its endpoint info to the configured Address Lookups. + /// + /// See also [`Builder::user_data_for_address_lookup`] for setting an initial value when + /// building the endpoint. + /// + /// If the endpoint is closed, this method will log a warning and ignore the + /// request. + pub fn set_user_data_for_address_lookup(&self, user_data: Option) { + if self.is_closed() { + warn!("Attempting to set user data for a closed endpoint. Ignoring."); + return; + } + self.inner.set_user_data_for_address_lookup(user_data); + } + + // # Methods for terminating the endpoint. + + /// Closes the QUIC endpoint and the socket. + /// + /// This will close any remaining open [`Connection`]s with an error code + /// of `0` and an empty reason. Though it is best practice to close those + /// explicitly before with a custom error code and reason. + /// + /// It will then make a best effort to wait for all close notifications to be + /// acknowledged by the peers, re-transmitting them if needed. This ensures the + /// peers are aware of the closed connections instead of having to wait for a timeout + /// on the connection. Once all connections are closed or timed out, the future + /// finishes. + /// + /// The maximum time-out that this future will wait for depends on QUIC transport + /// configurations of non-drained connections at the time of calling, and their current + /// estimates of round trip time. With default parameters and a conservative estimate + /// of round trip time, this call's future should take 3 seconds to resolve in cases of + /// bad connectivity or failed connections. In the usual case, this call's future should + /// return much more quickly. + /// + /// It is highly recommended you *do* wait for this close call to finish, if possible. + /// Not doing so will make connections that were still open while closing the endpoint + /// time out on the remote end. Thus remote ends will assume connections to have failed + /// even if all application data was transmitted successfully. + /// + /// Note: Someone used to closing TCP sockets might wonder why it is necessary to wait + /// for timeouts when closing QUIC endpoints, while they don't have to do this for TCP + /// sockets. This is due to QUIC and its acknowledgments being implemented in user-land, + /// while TCP sockets usually get closed and drained by the operating system in the + /// kernel during the "Time-Wait" period of the TCP socket. + /// + /// Be aware however that the underlying UDP sockets are only closed once all clones of + /// the respective [`Endpoint`] are dropped. + pub async fn close(&self) { + self.inner.close().await; + } + + /// Check if this endpoint is still alive, or already closed. + pub fn is_closed(&self) -> bool { + self.inner.is_closed() + } + + /// Returns a future that resolves once the endpoint closes. + /// + /// The returned future does not contain a clone or reference to the [`Endpoint`], + /// so keeping the returned future alive does not prevent the endpoint from being dropped. + /// + /// To run a task and stop it once the endpoint closes, you can use + /// [`EndpointClosed::run_until`]: + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # use iroh::endpoint::{Endpoint, presets}; + /// # async fn wrapper() -> n0_error::Result<()> { + /// let endpoint = Endpoint::bind(presets::N0).await?; + /// tokio::spawn(endpoint.closed().run_until(async move { + /// // the future will be aborted once the endpoint closes. + /// })); + /// # Ok(()) + /// # } + /// # } + /// ``` + pub fn closed(&self) -> EndpointClosed { + EndpointClosed { + inner: self.inner.closed(), + } + } + + /// Create a [`ServerConfigBuilder`] for this endpoint that includes the given alpns. + /// + /// Use the [`ServerConfigBuilder`] to customize the [`ServerConfig`] connection configuration + /// for a connection accepted using the [`Incoming::accept_with`] method. + pub fn create_server_config_builder(&self, alpns: Vec>) -> ServerConfigBuilder { + let inner = self.inner.static_config.create_server_config(alpns); + ServerConfigBuilder::new(inner, self.inner.static_config.transport_config.clone()) + } + + // # Remaining private methods + + /// Translates a possible IP-mapped [`SocketAddr`] into a transport address. + pub(crate) fn to_transport_addr( + &self, + addr: SocketAddr, + ) -> Option { + self.inner.to_transport_addr(addr) + } + + #[cfg(all(test, with_crypto_provider))] + pub(crate) fn inner(&self) -> Result, EndpointError> { + if self.is_closed() { + return Err(e!(EndpointError::Closed)); + } + Ok(self.inner.clone()) + } +} + +/// Options for the [`Endpoint::connect_with_opts`] function. +#[derive(Default, Debug, Clone)] +pub struct ConnectOptions { + transport_config: Option, + additional_alpns: Vec>, +} + +impl ConnectOptions { + /// Initializes new connection options. + /// + /// By default, the connection will use the same options + /// as [`Endpoint::connect`], e.g. a default [`QuicTransportConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Sets the QUIC transport config options for this connection. + pub fn with_transport_config(mut self, transport_config: QuicTransportConfig) -> Self { + self.transport_config = Some(transport_config); + self + } + + /// Sets [ALPN] identifiers that should be signaled as supported on connection, *in + /// addition* to the main [ALPN] identifier used in [`Endpoint::connect_with_opts`]. + /// + /// This allows connecting to servers that may only support older versions of your + /// protocol. In this case, you would add the older [ALPN] identifiers with this + /// function. + /// + /// You'll know the final negotiated [ALPN] identifier once your connection was + /// established using [`Connection::alpn`], or even slightly earlier in the + /// handshake by using [`Connecting::alpn`]. + /// The negotiated [ALPN] identifier may be any of the [ALPN] identifiers in this + /// list or the main [ALPN] used in [`Endpoint::connect_with_opts`]. + /// + /// The [ALPN] identifier order on the connect side doesn't matter, since it's the + /// accept side that determines the protocol. + /// + /// For setting the supported [ALPN] identifiers on the accept side, see the endpoint + /// builder's [`Builder::alpns`] function. + /// + /// [ALPN]: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation + pub fn with_additional_alpns(mut self, alpns: Vec>) -> Self { + self.additional_alpns = alpns; + self + } +} + +/// Future returned from [`Endpoint::closed`]. +#[derive(derive_more::Debug)] +#[pin_project] +#[debug("EndpointClosed")] +pub struct EndpointClosed { + #[pin] + inner: WaitForCancellationFutureOwned, +} + +impl Future for EndpointClosed { + type Output = (); + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let this = self.project(); + this.inner.poll(cx) + } +} + +impl EndpointClosed { + /// Runs a future to completion, or until the [`Endpoint`] is closed. + /// + /// Returns the output of `fut` if it completes before the endpoint closes, + /// or `None` otherwise. + pub async fn run_until(self, fut: F) -> Option { + n0_future::future::or(async { Some(fut.await) }, async { + self.await; + None + }) + .await + } +} + +/// Read a proxy url from the environment, in this order +/// +/// - `HTTP_PROXY` +/// - `http_proxy` +/// - `HTTPS_PROXY` +/// - `https_proxy` +fn proxy_url_from_env() -> Option { + if let Some(url) = std::env::var("HTTP_PROXY") + .ok() + .and_then(|s| s.parse::().ok()) + { + if is_cgi() { + warn!("HTTP_PROXY environment variable ignored in CGI"); + } else { + return Some(url); + } + } + if let Some(url) = std::env::var("http_proxy") + .ok() + .and_then(|s| s.parse::().ok()) + { + return Some(url); + } + if let Some(url) = std::env::var("HTTPS_PROXY") + .ok() + .and_then(|s| s.parse::().ok()) + { + return Some(url); + } + if let Some(url) = std::env::var("https_proxy") + .ok() + .and_then(|s| s.parse::().ok()) + { + return Some(url); + } + + None +} + +/// Connection status of a single home relay. +/// +/// Observed via [`Endpoint::home_relay_status`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RelayStatus { + url: RelayUrl, + state: RelayConnectionState, +} + +impl RelayStatus { + pub(crate) fn new(url: RelayUrl, state: RelayConnectionState) -> Self { + Self { url, state } + } + + /// Returns the URL of the home relay. + pub fn url(&self) -> &RelayUrl { + &self.url + } + + /// Returns `true` if the endpoint is connected to the relay. + pub fn is_connected(&self) -> bool { + self.state.is_connected() + } + + /// Returns the most recent connection error. + /// + /// Returns `None` when the relay is connected, or when the endpoint has + /// not yet observed a failed connection attempt. + /// + /// The error is meant to be logged or displayed, not matched on: it is an + /// [`AnyError`] wrapping a chain of private error types, none of which are + /// covered by semver guarantees. Use [`Self::auth_denied_reason`] to + /// distinguish the one failure that usually calls for a different reaction + /// than retrying. + pub fn last_error(&self) -> Option<&AnyError> { + self.state.last_failure().map(RelayConnectionFailure::error) + } + + /// Returns the reason if the relay server denied our authentication. + /// + /// Unlike most connection failures, this one will not usually resolve + /// itself. The endpoint keeps retrying with a backoff, but it presents the + /// same credentials every time, so unless the relay's access policy + /// changes it will keep being denied and [`Endpoint::online`] will never + /// resolve. An application that configures a relay auth token should + /// surface this to the user rather than wait to come online. + /// + /// The returned string is the reason reported by the relay server. It is + /// meant to be human-readable, don't attempt to match on it. + /// + /// Returns `None` when the relay is connected, when no connection attempt + /// has failed yet, or when the last failure had another cause. + /// + /// ```no_run + /// # async fn wrapper() -> n0_error::Result<()> { + /// # #[cfg(with_crypto_provider)] + /// # { + /// use iroh::{Endpoint, Watcher, endpoint::presets}; + /// use n0_future::StreamExt; + /// + /// let endpoint = Endpoint::builder(presets::Minimal).bind().await?; + /// let mut status = endpoint.home_relay_status().stream(); + /// while let Some(relays) = status.next().await { + /// for relay in relays { + /// if let Some(reason) = relay.auth_denied_reason() { + /// println!("{}: authentication denied ({reason})", relay.url()); + /// } + /// } + /// } + /// # } + /// # Ok(()) } + /// ``` + pub fn auth_denied_reason(&self) -> Option<&str> { + self.state + .last_failure() + .and_then(RelayConnectionFailure::auth_denied_reason) + } +} + +/// Configuration of the relay servers for an [`Endpoint`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RelayMode { + /// Disable relay servers completely. + /// This means that neither listening nor dialing relays will be available. + Disabled, + /// Use the default relay map, with production relay servers from n0. + /// + /// See [`crate::defaults::prod`] for the severs used. + Default, + /// Use the staging relay servers from n0. + Staging, + /// Use a custom relay map. + Custom(RelayMap), +} + +impl RelayMode { + /// Returns the relay map for this mode. + pub fn relay_map(&self) -> RelayMap { + match self { + RelayMode::Disabled => RelayMap::empty(), + RelayMode::Default => crate::defaults::prod::default_relay_map(), + RelayMode::Staging => crate::defaults::staging::default_relay_map(), + RelayMode::Custom(relay_map) => relay_map.clone(), + } + } + + /// Create a custom relay mode from a list of [`RelayUrl`]s. + /// + /// # Example + /// + /// ``` + /// # fn main() -> n0_error::Result<()> { + /// # use iroh::RelayMode; + /// RelayMode::custom([ + /// "https://use1-1.relay.n0.iroh.link.".parse()?, + /// "https://euw-1.relay.n0.iroh.link.".parse()?, + /// ]); + /// # Ok(()) } + /// ``` + pub fn custom(map: impl IntoIterator) -> Self { + let m = RelayMap::from_iter(map); + Self::Custom(m) + } +} + +/// Environment variable to force the use of staging relays. +pub const ENV_FORCE_STAGING_RELAYS: &str = "IROH_FORCE_STAGING_RELAYS"; + +/// Returns `true` if the use of staging relays is forced. +pub fn force_staging_infra() -> bool { + matches!(std::env::var(ENV_FORCE_STAGING_RELAYS), Ok(value) if !value.is_empty()) +} + +/// Returns the default relay mode. +/// +/// If the `IROH_FORCE_STAGING_RELAYS` environment variable is non empty, it will return `RelayMode::Staging`. +/// Otherwise, it will return `RelayMode::Default`. +pub fn default_relay_mode() -> RelayMode { + // Use staging in testing + match force_staging_infra() { + true => RelayMode::Staging, + false => RelayMode::Default, + } +} + +/// Check if we are being executed in a CGI context. +/// +/// If so, a malicious client can send the `Proxy:` header, and it will +/// be in the `HTTP_PROXY` env var. So we don't use it :) +fn is_cgi() -> bool { + std::env::var_os("REQUEST_METHOD").is_some() +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::{ + collections::BTreeMap, + io, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4}, + str::FromStr, + sync::Arc, + time::{Duration, Instant}, + }; + + use assert_matches::assert_matches; + use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr}; + use iroh_dns::endpoint_info::UserData; + use iroh_relay::{RelayConfig, RelayQuicConfig, server::Access, tls::CaTlsConfig}; + use n0_error::{AnyError as Error, Result, StdResultExt}; + use n0_future::{BufferedStreamExt, StreamExt, future::now_or_never, stream, time}; + use n0_tracing_test::traced_test; + use n0_watcher::Watcher; + use noq::PathStats; + use rand::{RngExt, SeedableRng}; + use rand_chacha::ChaCha8Rng; + use tokio::sync::oneshot; + use tracing::{Instrument, debug_span, error_span, info, info_span, instrument}; + + use super::Endpoint; + use crate::{ + RelayMap, RelayMode, + address_lookup::memory::MemoryLookup, + endpoint::{ + ApplicationClose, BindError, BindOpts, ConnectError, ConnectOptions, + ConnectWithOptsError, Connection, ConnectionError, PathEvent, PathEventStream, presets, + }, + protocol::{AcceptError, ProtocolHandler, Router}, + test_utils::{ + QlogFileGroup, run_relay_server, run_relay_server_with, run_relay_server_with_access, + }, + }; + + const TEST_ALPN: &[u8] = b"n0/iroh/test"; + + #[tokio::test] + #[traced_test] + async fn test_connect_self() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await + .unwrap(); + let my_addr = ep.addr(); + let res = ep.connect(my_addr.clone(), TEST_ALPN).await; + assert!(res.is_err()); + let err = res.err().unwrap(); + assert!(err.to_string().starts_with("Connecting to ourself")); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_connect_empty_alpn() -> Result { + let server = Endpoint::builder(presets::Minimal) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await + .unwrap(); + let server_addr = server.addr(); + + let client = Endpoint::builder(presets::Minimal).bind().await.unwrap(); + let res = client.connect(server_addr, b"").await; + assert!(res.is_err()); + let err = res.err().unwrap(); + assert_matches!( + err, + ConnectError::Connect { + source: ConnectWithOptsError::InvalidAlpn { .. }, + .. + } + ); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_connect_close() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let (relay_map, relay_url, _guard) = run_relay_server().await?; + let server_secret_key = SecretKey::from_bytes(&rng.random()); + let server_peer_id = server_secret_key.public(); + + let qlog = QlogFileGroup::from_env("endpoint_connect_close"); + + // Wait for the endpoint to be started to make sure it's up before clients try to connect + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .secret_key(server_secret_key) + .transport_config(qlog.create("server")?) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + // Wait for the endpoint to be reachable via relay + ep.online().await; + + let server = tokio::spawn( + async move { + info!("accepting connection"); + let incoming = ep.accept().await.anyerr()?; + let conn = incoming.await.anyerr()?; + let mut stream = conn.accept_uni().await.anyerr()?; + let mut buf = [0u8; 5]; + stream.read_exact(&mut buf).await.anyerr()?; + info!("Accepted 1 stream, received {buf:?}. Closing now."); + // close the connection + conn.close(7u8.into(), b"bye"); + + let res = conn.accept_uni().await; + assert_eq!(res.unwrap_err(), ConnectionError::LocallyClosed); + + let res = stream.read_to_end(10).await; + assert_eq!( + res.unwrap_err(), + noq::ReadToEndError::Read(noq::ReadError::ConnectionLost( + ConnectionError::LocallyClosed + )) + ); + info!("Closing the endpoint"); + ep.close().await; + info!("server test completed"); + Ok::<_, Error>(()) + } + .instrument(info_span!("test-server")), + ); + + let client = tokio::spawn( + async move { + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .transport_config(qlog.create("client")?) + .bind() + .await?; + info!("client connecting"); + let endpoint_addr = EndpointAddr::new(server_peer_id).with_relay_url(relay_url); + let conn = ep.connect(endpoint_addr, TEST_ALPN).await?; + let mut stream = conn.open_uni().await.anyerr()?; + + // First write is accepted by server. We need this bit of synchronisation + // because if the server closes after simply accepting the connection we can + // not be sure our .open_uni() call would succeed as it may already receive + // the error. + stream.write_all(b"hello").await.anyerr()?; + + info!("waiting for closed"); + // Remote now closes the connection, we should see an error sometime soon. + let err = conn.closed().await; + let expected_err = ConnectionError::ApplicationClosed(ApplicationClose { + error_code: 7u8.into(), + reason: b"bye".to_vec().into(), + }); + assert_eq!(err, expected_err); + + info!("opening new - expect it to fail"); + let res = conn.open_uni().await; + assert_eq!(res.unwrap_err(), expected_err); + info!("Closing the client"); + ep.close().await; + info!("client test completed"); + Ok::<_, Error>(()) + } + .instrument(info_span!("test-client")), + ); + + let (server, client) = tokio::time::timeout( + Duration::from_secs(30), + n0_future::future::zip(server, client), + ) + .await + .anyerr()?; + server.anyerr()??; + client.anyerr()??; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_relay_connect_loop() -> Result { + let test_start = Instant::now(); + let n_clients = 5; + let n_chunks_per_client = 2; + let chunk_size = 100; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); + let (relay_map, relay_url, _relay_guard) = run_relay_server().await.unwrap(); + let server_secret_key = SecretKey::from_bytes(&rng.random()); + let server_endpoint_id = server_secret_key.public(); + + // Make sure the server is bound before having clients connect to it: + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .secret_key(server_secret_key) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + // Also make sure the server has a working relay connection + ep.online().await; + + info!(time = ?test_start.elapsed(), "test setup done"); + + // The server accepts the connections of the clients sequentially. + let server = tokio::spawn( + async move { + let eps = ep.bound_sockets(); + + info!(me = %ep.id().fmt_short(), eps = ?eps, "server listening on"); + for i in 0..n_clients { + let res = tokio::time::timeout(Duration::from_secs(5), async { + let round_start = Instant::now(); + info!("[server] round {i}"); + let incoming = ep.accept().await.anyerr()?; + let conn = incoming.await.anyerr()?; + let endpoint_id = conn.remote_id(); + info!(%i, peer = %endpoint_id.fmt_short(), "accepted connection"); + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let mut buf = vec![0u8; chunk_size]; + for _i in 0..n_chunks_per_client { + recv.read_exact(&mut buf).await.anyerr()?; + send.write_all(&buf).await.anyerr()?; + } + info!(%i, peer = %endpoint_id.fmt_short(), "finishing"); + send.finish().anyerr()?; + conn.closed().await; // we're the last to send data, so we wait for the other side to close + info!(%i, peer = %endpoint_id.fmt_short(), "finished"); + info!("[server] round {i} done in {:?}", round_start.elapsed()); + Ok::<_, Error>(()) + }) + .await + .std_context("timeout"); + match res { + Err(err) | Ok(Err(err)) => { + // ensure we close the endpoint before returning early + // on error + ep.close().await; + return Err(err); + } + _ => { + // if this round went `Ok` don't close the endpoint yet + } + } + } + // close the endpoint before dropping the server task + ep.close().await; + Ok::<_, Error>(()) + } + .instrument(debug_span!("server")), + ); + + let client = tokio::spawn(async move { + for i in 0..n_clients { + let round_start = Instant::now(); + info!("[client] round {i}"); + let client_secret_key = SecretKey::from_bytes(&rng.random()); + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .secret_key(client_secret_key) + .bind() + .await?; + let ep_1 = ep.clone(); + let res = tokio::time::timeout( + Duration::from_secs(5), + async { + info!("client binding"); + let eps = ep.bound_sockets(); + + info!(me = %ep.id().fmt_short(), eps=?eps, "client bound"); + let endpoint_addr = + EndpointAddr::new(server_endpoint_id).with_relay_url(relay_url.clone()); + info!(to = ?endpoint_addr, "client connecting"); + let conn = ep.connect(endpoint_addr, TEST_ALPN).await.anyerr()?; + info!("client connected"); + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + + for i in 0..n_chunks_per_client { + let mut buf = vec![i; chunk_size]; + send.write_all(&buf).await.anyerr()?; + recv.read_exact(&mut buf).await.anyerr()?; + assert_eq!(buf, vec![i; chunk_size]); + } + // we're the last to receive data, so we close + conn.close(0u32.into(), b"bye!"); + info!("client finished"); + Ok::<_, Error>(()) + } + .instrument(debug_span!("client", %i)), + ) + .await + .std_context("timeout"); + ep_1.close().await; + info!("client endpoint closed"); + res??; + info!("[client] round {i} done in {:?}", round_start.elapsed()); + } + Ok::<_, Error>(()) + }); + + server.await.anyerr()??; + client.await.anyerr()??; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_send_relay() -> Result { + let (relay_map, _relay_url, _guard) = run_relay_server().await?; + let client = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + let server = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + + let task = tokio::spawn({ + let server = server.clone(); + async move { + let Some(conn) = server.accept().await else { + n0_error::bail_any!("Expected an incoming connection"); + }; + let conn = conn.await.anyerr()?; + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let data = recv.read_to_end(1000).await.anyerr()?; + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + conn.closed().await; + + Ok::<_, Error>(()) + } + }); + + let addr = server.addr(); + let conn = client.connect(addr, TEST_ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"Hello, world!").await.anyerr()?; + send.finish().anyerr()?; + let data = recv.read_to_end(1000).await.anyerr()?; + conn.close(0u32.into(), b"bye!"); + + task.await.anyerr()??; + + client.close().await; + server.close().await; + + assert_eq!(&data, b"Hello, world!"); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_two_direct_only() -> Result { + // Connect two endpoints on the same network, without a relay server, without + // Address Lookup. + let ep1 = { + let span = info_span!("server"); + let _guard = span.enter(); + Endpoint::builder(presets::N0) + .alpns(vec![TEST_ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + .bind() + .await? + }; + let ep2 = { + let span = info_span!("client"); + let _guard = span.enter(); + Endpoint::builder(presets::N0) + .alpns(vec![TEST_ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + .bind() + .await? + }; + let ep1_nodeaddr = ep1.addr(); + + #[instrument(name = "client", skip_all)] + async fn connect(ep: Endpoint, dst: EndpointAddr) -> Result { + info!(me = %ep.id().fmt_short(), "client starting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + send.finish().anyerr()?; + Ok(conn.closed().await) + } + + #[instrument(name = "server", skip_all)] + async fn accept(ep: Endpoint, src: EndpointId) -> Result { + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.anyerr()?.await.anyerr()?; + let node_id = conn.remote_id(); + assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.anyerr()?; + let msg = recv.read_to_end(100).await.anyerr()?; + assert_eq!(msg, b"hello"); + // Dropping the connection closes it just fine. + Ok(()) + } + + let ep1_accept = tokio::spawn(accept(ep1.clone(), ep2.id())); + let ep2_connect = tokio::spawn(connect(ep2.clone(), ep1_nodeaddr)); + + ep1_accept.await.anyerr()??; + let conn_closed = dbg!(ep2_connect.await.anyerr()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(ApplicationClose { .. }) + )); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_two_relay_only_becomes_direct() -> Result { + // Connect two endpoints on the same network, via a relay server, without + // Address Lookup. Wait until there is a direct connection. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + let qlog = Arc::new(QlogFileGroup::from_env("two_relay_only_becomes_direct")); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + qlog: Arc, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let secret = SecretKey::from_bytes(&rng.random()); + let ep = Endpoint::builder(presets::N0) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .relay_mode(RelayMode::Custom(relay_map)) + .transport_config(qlog.create("client")?) + .bind() + .await?; + info!(me = %ep.id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.anyerr()?; + + info!(me = %ep.id().fmt_short(), "client connecting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + let mut paths = conn.paths_stream(); + info!("Waiting for direct connection"); + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + if infos.iter().any(|info| info.is_ip()) { + break; + } + } + info!("Have direct connection"); + #[cfg(feature = "metrics")] + { + // Validate holepunch metrics. + assert_eq!(ep.metrics().socket.num_conns_opened.get(), 1); + assert_eq!(ep.metrics().socket.num_conns_direct.get(), 1); + } + + send.write_all(b"close please").await.anyerr()?; + send.finish().anyerr()?; + + let res = conn.closed().await; + ep.close().await; + Ok(res) + } + + #[instrument(name = "server", skip_all)] + async fn accept( + relay_map: RelayMap, + node_addr_tx: oneshot::Sender, + qlog: Arc, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); + let secret = SecretKey::from_bytes(&rng.random()); + let ep = Endpoint::builder(presets::N0) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .transport_config(qlog.create("server")?) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + ep.online().await; + let mut node_addr = ep.addr(); + node_addr.addrs.retain(|addr| addr.is_relay()); + node_addr_tx.send(node_addr).unwrap(); + + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.anyerr()?.await.anyerr()?; + // let node_id = conn.remote_node_id()?; + // assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.anyerr()?; + let mut msg = [0u8; 5]; + recv.read_exact(&mut msg).await.anyerr()?; + assert_eq!(&msg, b"hello"); + info!("received hello"); + let msg = recv.read_to_end(100).await.anyerr()?; + assert_eq!(msg, b"close please"); + info!("received 'close please'"); + // Closing the endpoint closes all connections. + ep.close().await; + Ok(()) + } + + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx, qlog.clone())); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx, qlog)); + + server_task.await.anyerr()??; + let conn_closed = dbg!(client_task.await.anyerr()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(ApplicationClose { .. }) + )); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_two_relay_only_no_ip() -> Result { + // Connect two endpoints on the same network, via a relay server, without + // Address Lookup. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let secret = SecretKey::from_bytes(&rng.random()); + let ep = Endpoint::builder(presets::N0) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .relay_mode(RelayMode::Custom(relay_map)) + .clear_ip_transports() // disable direct + .bind() + .await?; + info!(me = %ep.id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.anyerr()?; + + info!(me = %ep.id().fmt_short(), "client connecting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + let mut paths = conn.paths_stream(); + info!("Waiting for connection"); + 'outer: while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + for info in infos.iter() { + if info.is_ip() { + panic!("should not happen: {:?}", info); + } + if info.is_relay() { + break 'outer; + } + } + } + info!("Have relay connection"); + + send.write_all(b"close please").await.anyerr()?; + send.finish().anyerr()?; + let res = conn.closed().await; + ep.close().await; + Ok(res) + } + + #[instrument(name = "server", skip_all)] + async fn accept( + relay_map: RelayMap, + node_addr_tx: oneshot::Sender, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); + let secret = SecretKey::from_bytes(&rng.random()); + let ep = Endpoint::builder(presets::N0) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .relay_mode(RelayMode::Custom(relay_map)) + .clear_ip_transports() + .bind() + .await?; + ep.online().await; + let node_addr = ep.addr(); + node_addr_tx.send(node_addr).unwrap(); + + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.anyerr()?.await.anyerr()?; + // let node_id = conn.remote_node_id()?; + // assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.anyerr()?; + let mut msg = [0u8; 5]; + recv.read_exact(&mut msg).await.anyerr()?; + assert_eq!(&msg, b"hello"); + info!("received hello"); + let msg = recv.read_to_end(100).await.anyerr()?; + assert_eq!(msg, b"close please"); + info!("received 'close please'"); + // Closing the endpoint closes all connections. + ep.close().await; + Ok(()) + } + + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); + + server_task.await.anyerr()??; + let conn_closed = dbg!(client_task.await.anyerr()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(ApplicationClose { .. }) + )); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_two_direct_add_relay() -> Result { + // Connect two endpoints on the same network, without relay server and without + // Address Lookup. Add a relay connection later. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + ) -> Result<()> { + let secret = SecretKey::from([0u8; 32]); + let ep = Endpoint::builder(presets::N0) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + info!(me = %ep.id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.anyerr()?; + + info!(me = %ep.id().fmt_short(), "client connecting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + info!(me = %ep.id().fmt_short(), "client connected"); + + // We should be connected via IP, because it is faster than the relay server. + // TODO: Maybe not panic if this is not true? + + let path_info = conn.paths(); + assert_eq!(path_info.len(), 1); + assert!(path_info.iter().next().unwrap().is_ip()); + + let mut paths = conn.paths_stream(); + time::timeout(Duration::from_secs(5), async move { + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + if infos.iter().any(|info| info.is_relay()) { + info!("client has a relay path"); + break; + } + } + }) + .await + .anyerr()?; + + // wait for the server to signal it has the relay connection + let mut stream = conn.accept_uni().await.anyerr()?; + stream.read_to_end(100).await.anyerr()?; + + info!("client closing"); + conn.close(0u8.into(), b""); + ep.close().await; + Ok(()) + } + + #[instrument(name = "server", skip_all)] + async fn accept( + relay_map: RelayMap, + node_addr_tx: oneshot::Sender, + ) -> Result { + let secret = SecretKey::from([1u8; 32]); + let ep = Endpoint::builder(presets::N0) + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + ep.online().await; + let node_addr = ep.addr(); + node_addr_tx.send(node_addr).unwrap(); + + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.anyerr()?.await.anyerr()?; + info!(me = %ep.id().fmt_short(), "server accepted connection"); + + // Wait for a relay connection to be added. Client does all the asserting here, + // we just want to wait so we get to see all the mechanics of the connection + // being added on this side too. + let mut paths = conn.paths_stream(); + time::timeout(Duration::from_secs(5), async move { + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + if infos.iter().any(|path| path.is_relay()) { + info!("server has a relay path"); + break; + } + } + }) + .await + .anyerr()?; + + let mut stream = conn.open_uni().await.anyerr()?; + stream.write_all(b"have relay").await.anyerr()?; + stream.finish().anyerr()?; + info!("waiting conn.closed()"); + + Ok(conn.closed().await) + } + + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); + + client_task.await.anyerr()??; + let conn_closed = dbg!(server_task.await.anyerr()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(ApplicationClose { .. }) + )); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_relay_map_change() -> Result { + let (relay_map, relay_url, _guard1) = run_relay_server().await?; + let client = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + let server = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + + let task = tokio::spawn({ + let server = server.clone(); + async move { + for i in 0..2 { + println!("accept: round {i}"); + let Some(conn) = server.accept().await else { + n0_error::bail_any!("Expected an incoming connection"); + }; + let conn = conn.await.anyerr()?; + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let data = recv.read_to_end(1000).await.anyerr()?; + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + conn.closed().await; + } + Ok::<_, Error>(()) + } + }); + + server.online().await; + + let mut addr = server.addr(); + println!("round1: {:?}", addr); + + // remove direct addrs to force relay usage + addr.addrs + .retain(|addr| !matches!(addr, TransportAddr::Ip(_))); + + let conn = client.connect(addr, TEST_ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"Hello, world!").await.anyerr()?; + send.finish().anyerr()?; + let data = recv.read_to_end(1000).await.anyerr()?; + conn.close(0u32.into(), b"bye!"); + + assert_eq!(&data, b"Hello, world!"); + + // setup a second relay server + let (new_relay_map, new_relay_url, _guard2) = run_relay_server().await?; + let new_endpoint = new_relay_map + .get(&new_relay_url) + .expect("missing endpoint") + .clone(); + dbg!(&new_relay_map); + + let addr_watcher = server.watch_addr(); + + // add new new relay + assert!( + server + .insert_relay(new_relay_url.clone(), new_endpoint.clone()) + .await + .is_none() + ); + // remove the old relay + assert!(server.remove_relay(&relay_url).await.is_some()); + + println!("------- changed ----- "); + + let mut addr = tokio::time::timeout(Duration::from_secs(10), async move { + let mut stream = addr_watcher.stream(); + while let Some(addr) = stream.next().await { + if addr.relay_urls().next() != Some(&relay_url) { + return addr; + } + } + panic!("failed to change relay"); + }) + .await + .anyerr()?; + + println!("round2: {:?}", addr); + assert_eq!(addr.relay_urls().next(), Some(&new_relay_url)); + + // remove direct addrs to force relay usage + addr.addrs + .retain(|addr| !matches!(addr, TransportAddr::Ip(_))); + + let conn = client.connect(addr, TEST_ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"Hello, world!").await.anyerr()?; + send.finish().anyerr()?; + let data = recv.read_to_end(1000).await.anyerr()?; + conn.close(0u32.into(), b"bye!"); + + task.await.anyerr()??; + + client.close().await; + server.close().await; + + assert_eq!(&data, b"Hello, world!"); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_bidi_send_recv() -> Result { + let disco = MemoryLookup::new(); + let ep1 = Endpoint::builder(presets::Minimal) + .address_lookup(disco.clone()) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + + let ep2 = Endpoint::builder(presets::Minimal) + .address_lookup(disco.clone()) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + + disco.add_endpoint_info(ep1.addr()); + disco.add_endpoint_info(ep2.addr()); + + let ep1_endpointid = ep1.id(); + let ep2_endpointid = ep2.id(); + eprintln!("endpoint id 1 {ep1_endpointid}"); + eprintln!("endpoint id 2 {ep2_endpointid}"); + + async fn connect_hello(ep: Endpoint, dst: EndpointId) -> Result { + let conn = ep.connect(dst, TEST_ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + info!("sending hello"); + send.write_all(b"hello").await.anyerr()?; + send.finish().anyerr()?; + info!("receiving world"); + let m = recv.read_to_end(100).await.anyerr()?; + assert_eq!(m, b"world"); + conn.close(1u8.into(), b"done"); + Ok(()) + } + + async fn accept_world(ep: Endpoint, src: EndpointId) -> Result { + let incoming = ep.accept().await.anyerr()?; + let mut iconn = incoming.accept().anyerr()?; + let alpn = iconn.alpn().await?; + let conn = iconn.await.anyerr()?; + let endpoint_id = conn.remote_id(); + assert_eq!(endpoint_id, src); + assert_eq!(alpn, TEST_ALPN); + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + info!("receiving hello"); + let m = recv.read_to_end(100).await.anyerr()?; + assert_eq!(m, b"hello"); + info!("sending hello"); + send.write_all(b"world").await.anyerr()?; + send.finish().anyerr()?; + match conn.closed().await { + ConnectionError::ApplicationClosed(closed) => { + assert_eq!(closed.error_code, 1u8.into()); + Ok(()) + } + _ => panic!("wrong close error"), + } + } + + let p1_accept = tokio::spawn(accept_world(ep1.clone(), ep2_endpointid).instrument( + info_span!( + "p1_accept", + ep1 = %ep1.id().fmt_short(), + dst = %ep2_endpointid.fmt_short(), + ), + )); + let p2_accept = tokio::spawn(accept_world(ep2.clone(), ep1_endpointid).instrument( + info_span!( + "p2_accept", + ep2 = %ep2.id().fmt_short(), + dst = %ep1_endpointid.fmt_short(), + ), + )); + let p1_connect = tokio::spawn(connect_hello(ep1.clone(), ep2_endpointid).instrument( + info_span!( + "p1_connect", + ep1 = %ep1.id().fmt_short(), + dst = %ep2_endpointid.fmt_short(), + ), + )); + let p2_connect = tokio::spawn(connect_hello(ep2.clone(), ep1_endpointid).instrument( + info_span!( + "p2_connect", + ep2 = %ep2.id().fmt_short(), + dst = %ep1_endpointid.fmt_short(), + ), + )); + + p1_accept.await.anyerr()??; + p2_accept.await.anyerr()??; + p1_connect.await.anyerr()??; + p2_connect.await.anyerr()??; + + Ok(()) + } + + /// Regression test: Don't fail connections with dead relays on Windows. + /// + /// A single client connecting to a single server over a usable direct path + /// must succeed even when both are configured with an unreachable home relay + /// (`https://127.0.0.1:1`, nothing listening). The dead relay should be irrelevant: + /// the direct path works and the connection comes up in milliseconds. + /// + /// This was broken on Windows because QaD sends over the same socket to the dead + /// relay, and the socket would return recv errors on the next recv to report ICMP + /// errors for the previous send. We now skip over these errors, implemented in + /// https://github.com/n0-computer/net-tools/pull/166, so this no longer fails. + #[tokio::test] + async fn endpoint_unreachable_relay_direct_connect_succeeds() -> Result { + // The relay url and its QADv4 probe must both hit closed ports, so the relay is + // unreachable and the probe draws the ICMP port-unreachable the Windows socket + // reports on its next recv. Claim an ephemeral port, then close it: it's now free, + // so nothing answers. There's nothing stopping the kernel from reusing a port + // right away, but on most machines that's unlikely. The url is dialed over TCP + // (HTTPS), the probe over UDP, so claim each with the matching socket type. + let closed_tcp_port = { + let sock = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind"); + sock.local_addr().expect("local addr").port() + }; + let closed_udp_port = { + let sock = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind"); + sock.local_addr().expect("local addr").port() + }; + let dead_relay: RelayUrl = format!("https://127.0.0.1:{closed_tcp_port}") + .parse() + .expect("valid relay url"); + let dead_relay_config = RelayConfig::new( + dead_relay.clone(), + Some(RelayQuicConfig::new(closed_udp_port)), + ); + + let bind_endpoint = async || { + Endpoint::builder(presets::Minimal) + // Use the broken relay to trigger the ICMP errors from the QaD sends. + .relay_mode(RelayMode::Custom(RelayMap::from_iter([ + dead_relay_config.clone() + ]))) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]) + // Bind on IPv4 only to ensure a single socket to not have spurious polls. + .bind_addr((Ipv4Addr::LOCALHOST, 0)) + .expect("valid addr") + .bind() + .await + }; + + let server = bind_endpoint().await?; + let server_addr = server.addr().with_relay_url(dead_relay.clone()); + let client = bind_endpoint().await?; + + // Server accepts the incoming connection and holds it open until the test ends. + let accept = tokio::spawn(async move { + let incoming = server.accept().await.anyerr()?; + let conn = incoming.await.anyerr()?; + conn.closed().await; + server.close().await; + n0_error::Ok(()) + }); + + // The connect must complete over the direct loopback path despite the dead relay. + let _conn = tokio::time::timeout( + Duration::from_secs(10), + client.connect(server_addr, TEST_ALPN), + ) + .await + .expect("connection should succeed")?; + client.close().await; + accept.await.anyerr()??; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_direct_addresses_no_qad_relay() -> Result { + let (relay_map, _, _guard) = run_relay_server_with(false).await.unwrap(); + + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .alpns(vec![TEST_ALPN.to_vec()]) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + + assert!(ep.addr().ip_addrs().count() > 0); + + Ok(()) + } + + /// Test that configured external addresses are included in the endpoint's + /// direct addresses, both when set via builder and at runtime. + #[tokio::test(flavor = "current_thread", start_paused = true)] + #[traced_test] + async fn test_external_addr() -> Result { + let configured_addr = SocketAddr::from(SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 12345)); + + // Test builder-configured external address + let ep = Endpoint::builder(presets::Minimal) + .external_addr(configured_addr) + .bind() + .await?; + + let addr = ep.addr(); + assert!( + addr.ip_addrs().any(|a| *a == configured_addr), + "builder-configured external addr {configured_addr} not found in endpoint addr: {addr:?}" + ); + + // Test runtime add + let runtime_addr = SocketAddr::from(SocketAddrV4::new(Ipv4Addr::new(5, 6, 7, 8), 54321)); + ep.add_external_addr(runtime_addr).await; + tokio::time::sleep(Duration::from_millis(100)).await; + + let addr = ep.addr(); + assert!( + addr.ip_addrs().any(|a| *a == runtime_addr), + "runtime-added external addr {runtime_addr} not found in endpoint addr: {addr:?}" + ); + + // Test runtime remove + let removed = ep.remove_external_addr(&runtime_addr).await; + assert!(removed); + tokio::time::sleep(Duration::from_millis(100)).await; + + let addr = ep.addr(); + assert!( + !addr.ip_addrs().any(|a| *a == runtime_addr), + "removed external addr {runtime_addr} still found in endpoint addr: {addr:?}" + ); + assert!( + addr.ip_addrs().any(|a| *a == configured_addr), + "builder-configured external addr should still be present: {addr:?}" + ); + + ep.close().await; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn graceful_close() -> Result { + let client = Endpoint::bind(presets::Minimal).await?; + let server = Endpoint::builder(presets::Minimal) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + let server_addr = server.addr(); + let server_task = tokio::spawn(async move { + let incoming = server.accept().await.anyerr()?; + let conn = incoming.await.anyerr()?; + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let msg = recv.read_to_end(1_000).await.anyerr()?; + send.write_all(&msg).await.anyerr()?; + send.finish().anyerr()?; + let close_reason = conn.closed().await; + Ok::<_, Error>(close_reason) + }); + + let conn = client.connect(server_addr, TEST_ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"Hello, world!").await.anyerr()?; + send.finish().anyerr()?; + recv.read_to_end(1_000).await.anyerr()?; + conn.close(42u32.into(), b"thanks, bye!"); + client.close().await; + + let close_err = server_task.await.anyerr()??; + let ConnectionError::ApplicationClosed(app_close) = close_err else { + panic!("Unexpected close reason: {close_err:?}"); + }; + + assert_eq!(app_close.error_code, 42u32.into()); + assert_eq!(app_close.reason.as_ref(), b"thanks, bye!"); + + Ok(()) + } + + #[cfg(feature = "metrics")] + #[tokio::test] + #[traced_test] + async fn metrics_smoke() -> Result { + use iroh_metrics::Registry; + + let secret_key = SecretKey::from_bytes(&[0u8; 32]); + let client = Endpoint::builder(presets::Minimal) + .secret_key(secret_key) + .bind() + .await?; + let secret_key = SecretKey::from_bytes(&[1u8; 32]); + let server = Endpoint::builder(presets::Minimal) + .secret_key(secret_key) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + let server_addr = server.addr(); + let server_task = tokio::task::spawn(async move { + let conn = server.accept().await.anyerr()?.await.anyerr()?; + let mut uni = conn.accept_uni().await.anyerr()?; + uni.read_to_end(10).await.anyerr()?; + drop(conn); + Ok::<_, Error>(server) + }); + let conn = client.connect(server_addr, TEST_ALPN).await?; + let mut uni = conn.open_uni().await.anyerr()?; + uni.write_all(b"helloworld").await.anyerr()?; + uni.finish().anyerr()?; + conn.closed().await; + drop(conn); + let server = server_task.await.anyerr()??; + + let m = client.metrics(); + // assert_eq!(m.socket.num_direct_conns_added.get(), 1); + // assert_eq!(m.socket.connection_became_direct.get(), 1); + // assert_eq!(m.socket.connection_handshake_success.get(), 1); + // assert_eq!(m.socket.endpoints_contacted_directly.get(), 1); + assert!(m.socket.recv_datagrams.get() > 0); + + let m = server.metrics(); + // assert_eq!(m.socket.num_direct_conns_added.get(), 1); + // assert_eq!(m.socket.connection_became_direct.get(), 1); + // assert_eq!(m.socket.endpoints_contacted_directly.get(), 1); + // assert_eq!(m.socket.connection_handshake_success.get(), 1); + assert!(m.socket.recv_datagrams.get() > 0); + + // test openmetrics encoding with labeled subregistries per endpoint + fn register_endpoint(registry: &mut Registry, endpoint: &Endpoint) { + let id = endpoint.id().fmt_short(); + let sub_registry = registry.sub_registry_with_label("id", id.to_string()); + sub_registry.register_all(endpoint.metrics()); + } + let mut registry = Registry::default(); + register_endpoint(&mut registry, &client); + register_endpoint(&mut registry, &server); + // let s = registry.encode_openmetrics_to_string().anyerr()?; + // assert!(s.contains(r#"socket_endpoints_contacted_directly_total{id="3b6a27bcce"} 1"#)); + // assert!(s.contains(r#"socket_endpoints_contacted_directly_total{id="8a88e3dd74"} 1"#)); + Ok(()) + } + + /// Configures the accept side to take `accept_alpns` ALPNs, then connects to it with `primary_connect_alpn` + /// with `secondary_connect_alpns` set, and finally returns the negotiated ALPN. + async fn alpn_connection_test( + accept_alpns: Vec>, + primary_connect_alpn: &[u8], + secondary_connect_alpns: Vec>, + ) -> Result> { + let client = Endpoint::bind(presets::Minimal).await?; + let server = Endpoint::builder(presets::Minimal) + .alpns(accept_alpns) + .bind() + .await?; + let server_addr = server.addr(); + let server_task = tokio::spawn({ + let server = server.clone(); + async move { + let incoming = server.accept().await.anyerr()?; + let conn = incoming.await.anyerr()?; + conn.close(0u32.into(), b"bye!"); + n0_error::Ok(conn.alpn().to_vec()) + } + }); + + let conn = client + .connect_with_opts( + server_addr, + primary_connect_alpn, + ConnectOptions::new().with_additional_alpns(secondary_connect_alpns), + ) + .await?; + let conn = conn.await.anyerr()?; + let client_alpn = conn.alpn(); + conn.closed().await; + client.close().await; + server.close().await; + + let server_alpn = server_task.await.anyerr()??; + + assert_eq!(client_alpn, server_alpn); + + Ok(server_alpn.to_vec()) + } + + #[tokio::test] + #[traced_test] + async fn connect_multiple_alpn_negotiated() -> Result { + const ALPN_ONE: &[u8] = b"alpn/1"; + const ALPN_TWO: &[u8] = b"alpn/2"; + + assert_eq!( + alpn_connection_test( + // Prefer version 2 over version 1 on the accept side + vec![ALPN_TWO.to_vec(), ALPN_ONE.to_vec()], + ALPN_TWO, + vec![ALPN_ONE.to_vec()], + ) + .await?, + ALPN_TWO.to_vec(), + "accept side prefers version 2 over 1" + ); + + assert_eq!( + alpn_connection_test( + // Only support the old version + vec![ALPN_ONE.to_vec()], + ALPN_TWO, + vec![ALPN_ONE.to_vec()], + ) + .await?, + ALPN_ONE.to_vec(), + "accept side only supports the old version" + ); + + assert_eq!( + alpn_connection_test( + vec![ALPN_TWO.to_vec(), ALPN_ONE.to_vec()], + ALPN_ONE, + vec![ALPN_TWO.to_vec()], + ) + .await?, + ALPN_TWO.to_vec(), + "connect side ALPN order doesn't matter" + ); + + assert_eq!( + alpn_connection_test(vec![ALPN_TWO.to_vec(), ALPN_ONE.to_vec()], ALPN_ONE, vec![],) + .await?, + ALPN_ONE.to_vec(), + "connect side only supports the old version" + ); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + #[cfg(feature = "unstable-net-report")] + async fn watch_net_report() -> Result { + let endpoint = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Staging) + .bind() + .await?; + + // can get a first report + endpoint.net_report().updated().await.anyerr()?; + + Ok(()) + } + + /// Tests that initial connection establishment isn't extremely slow compared + /// to subsequent connections. + /// + /// This is a time based test, but uses a very large ratio to reduce flakiness. + /// It also does a number of connections to average out any anomalies. + #[tokio::test] + #[traced_test] + async fn connect_multi_time() -> Result { + let n = 32; + + const NOOP_ALPN: &[u8] = b"noop"; + + #[derive(Debug, Clone)] + struct Noop; + + impl ProtocolHandler for Noop { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + connection.closed().await; + Ok(()) + } + } + + async fn noop_server() -> Result<(Router, EndpointAddr)> { + let endpoint = Endpoint::bind(presets::Minimal).await.anyerr()?; + let addr = endpoint.addr(); + let router = Router::builder(endpoint).accept(NOOP_ALPN, Noop).spawn(); + Ok((router, addr)) + } + + let routers = stream::iter(0..n) + .map(|_| noop_server()) + .buffered_unordered(32) + .collect::>() + .await + .into_iter() + .collect::, _>>() + .anyerr()?; + + let addrs = routers + .iter() + .map(|(_, addr)| addr.clone()) + .collect::>(); + let ids = addrs.iter().map(|addr| addr.id).collect::>(); + let address_lookup = MemoryLookup::from_endpoint_info(addrs); + let endpoint = Endpoint::builder(presets::Minimal) + .address_lookup(address_lookup) + .bind() + .await + .anyerr()?; + // wait for the endpoint to be initialized. This should not be needed, + // but we don't want to measure endpoint init time but connection time + // from a fully initialized endpoint. + endpoint.addr(); + let t0 = Instant::now(); + for id in &ids { + let conn = endpoint.connect(*id, NOOP_ALPN).await?; + conn.close(0u32.into(), b"done"); + } + let dt0 = t0.elapsed().as_secs_f64(); + let t1 = Instant::now(); + for id in &ids { + let conn = endpoint.connect(*id, NOOP_ALPN).await?; + conn.close(0u32.into(), b"done"); + } + let dt1 = t1.elapsed().as_secs_f64(); + + assert!(dt0 / dt1 < 20.0, "First round: {dt0}s, second round {dt1}s"); + Ok(()) + } + + #[tokio::test] + async fn test_custom_relay() -> Result { + let _ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::custom([RelayUrl::from_str( + "https://use1-1.relay.n0.iroh.link.", + )?])) + .bind() + .await?; + + let relays = RelayMap::try_from_iter([ + "https://use1-1.relay.n0.iroh.link/", + "https://euc1-1.relay.n0.iroh.link/", + ])?; + let _ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relays)) + .bind() + .await?; + + Ok(()) + } + + /// Testing bind_addr: Clear IP transports and add single IPv4 bind + #[tokio::test] + #[traced_test] + async fn test_bind_addr_clear() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .clear_ip_transports() + .bind_addr((Ipv4Addr::LOCALHOST, 0))? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + assert_eq!(bound_sockets.len(), 1); + assert_eq!(bound_sockets[0].ip(), IpAddr::V4(Ipv4Addr::LOCALHOST)); + ep.close().await; + Ok(()) + } + + /// Testing bind_addr: Do not clear IP transports and add single non-default IPv4 bind + /// + /// This will bind two sockets: default wildcard bind for IPv6, and our + /// manually-added IPv4 bind. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_no_clear() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .bind_addr((Ipv4Addr::LOCALHOST, 0))? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + assert_eq!(bound_sockets.len(), 2); + assert_eq!(bound_sockets.iter().filter(|x| x.is_ipv4()).count(), 1); + assert_eq!(bound_sockets.iter().filter(|x| x.is_ipv6()).count(), 1); + // Test that our manually added socket is there + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::LOCALHOST)) + ); + ep.close().await; + Ok(()) + } + + // Testing bind_addr: Do not clear IP transports and add single default IPv4 bind. + // + // This replaces the default IPv4 bind added by the builder, + // but keeps the default wildcard IPv6 bind. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_default() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .bind_addr_with_opts( + (Ipv4Addr::LOCALHOST, 0), + BindOpts::default().set_is_default_route(true), + )? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + assert_eq!(bound_sockets.len(), 2); + assert_eq!(bound_sockets.iter().filter(|x| x.is_ipv4()).count(), 1); + assert_eq!(bound_sockets.iter().filter(|x| x.is_ipv6()).count(), 1); + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::LOCALHOST)) + ); + ep.close().await; + drop(ep); + + Ok(()) + } + + /// Testing bind_addr: Do not clear IP transports and add single IPv4 bind with a non-zero prefix len + /// + /// This will bind three sockets: default wildcard bind for IPv4 and IPv6, and our + /// manually-added IPv4 bind. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_nonzero_prefix() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .bind_addr_with_opts( + (Ipv4Addr::LOCALHOST, 0), + BindOpts::default().set_prefix_len(32), + )? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + assert_eq!(bound_sockets.len(), 3); + assert_eq!(bound_sockets.iter().filter(|x| x.is_ipv4()).count(), 2); + assert_eq!(bound_sockets.iter().filter(|x| x.is_ipv6()).count(), 1); + // Test that the default wildcard socket is there + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED)) + ); + // Test that our manually added socket is there + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::LOCALHOST)) + ); + ep.close().await; + Ok(()) + } + + /// Bind on an unusable port with the default opts. + /// + /// Binding the endpoint fails with an AddrInUse error. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_badport() -> Result { + let socket = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?; + let port = socket.local_addr()?.port(); + + let res = Endpoint::builder(presets::Minimal) + .clear_ip_transports() + .bind_addr((Ipv4Addr::LOCALHOST, port))? + .bind() + .await; + + assert!(matches!( + res, + Err(BindError::Sockets { + source: io_error, + .. + }) + if io_error.kind() == io::ErrorKind::AddrInUse + )); + Ok(()) + } + + /// Bind a non-default route on an unusable port, but set is_required = false. + /// + /// Binding the endpoint succeeds. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_badport_notrequired() -> Result { + let socket = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?; + let port = socket.local_addr()?.port(); + + let ep = Endpoint::builder(presets::Minimal) + .bind_addr_with_opts( + (Ipv4Addr::LOCALHOST, port), + BindOpts::default() + .set_prefix_len(32) + .set_is_required(false), + )? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + // just the default wildcard binds + assert_eq!(bound_sockets.len(), 2); + // our requested bind addr is not included because it failed to bind + assert!( + !bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::LOCALHOST)) + ); + Ok(()) + } + + /// Bind on a default route on an unusable port, but set is_required = false. + /// + /// Binding the endpoint succeeds. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_badport_default_notrequired() -> Result { + let socket = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?; + let port = socket.local_addr()?.port(); + + let ep = Endpoint::builder(presets::Minimal) + .bind_addr_with_opts( + (Ipv4Addr::LOCALHOST, port), + BindOpts::default().set_is_required(false), + )? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + // just the IPv6 default, but no IPv4 bind at all because we replaced the default + // with a bind with an unusable port and set it to not be required. + assert_eq!(bound_sockets.len(), 1); + assert!(bound_sockets[0].is_ipv6()); + Ok(()) + } + + /// Bind on an unusable port, with is_required = false, and no other transports. + /// + /// Binding the endpoint fails with "no valid address available". + #[tokio::test] + #[traced_test] + async fn test_bind_addr_badport_notrequired_no_other_transports() -> Result { + let socket = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?; + let port = socket.local_addr()?.port(); + + let res = Endpoint::builder(presets::Minimal) + .clear_ip_transports() + .bind_addr_with_opts( + (Ipv4Addr::LOCALHOST, port), + BindOpts::default().set_is_required(false), + )? + .bind() + .await; + + assert!(matches!( + res, + Err(BindError::CreateQuicEndpoint { + source: io_error, + .. + }) + if io_error.kind() == io::ErrorKind::Other && io_error.to_string() == "no valid address available" + )); + Ok(()) + } + + /// Bind with prefix len 0 but set the route as non-default. + #[tokio::test] + #[traced_test] + async fn test_bind_addr_prefix_len_0_not_default() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .bind_addr_with_opts( + (Ipv4Addr::LOCALHOST, 0), + BindOpts::default().set_is_default_route(false), + )? + .bind() + .await?; + let bound_sockets = ep.bound_sockets(); + // The two default wildcard binds plus our additional route (which does not replace the default route + // because we set is_default_route to false explicitly). + assert_eq!(bound_sockets.len(), 3); + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V6(Ipv6Addr::UNSPECIFIED)) + ); + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED)) + ); + assert!( + bound_sockets + .iter() + .any(|x| x.ip() == IpAddr::V4(Ipv4Addr::LOCALHOST)) + ); + Ok(()) + } + + #[ignore = "flaky"] + #[tokio::test] + #[traced_test] + async fn connect_via_relay_becomes_direct_and_sends_direct() -> Result { + let (relay_map, relay_url, _relay_server_guard) = run_relay_server().await?; + let qlog = Arc::new(QlogFileGroup::from_env( + "connect_via_relay_becomes_direct_and_sends_direct", + )); + let transfer_size = 1_000_000; + + async fn collect_stats(mut events: PathEventStream) -> BTreeMap { + let mut stats = BTreeMap::new(); + while let Some(event) = events.next().await { + if let PathEvent::Closed { + remote_addr, + last_stats, + .. + } = event + { + stats.insert(remote_addr, *last_stats); + } + } + stats + } + + let client = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .transport_config(qlog.create("client")?) + .bind() + .await?; + let server = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .transport_config(qlog.create("server")?) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .await?; + let server_addr = EndpointAddr::new(server.id()).with_relay_url(relay_url); + let server_task = tokio::spawn(async move { + let incoming = server.accept().await.anyerr()?; + let conn = incoming.await.anyerr()?; + let stats_task = tokio::spawn(collect_stats(conn.path_events())); + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let msg = recv.read_to_end(transfer_size).await.anyerr()?; + send.write_all(&msg).await.anyerr()?; + send.finish().anyerr()?; + conn.closed().await; + let stats = stats_task.await.expect("stats task panicked"); + Ok::<_, Error>(stats) + }); + + let conn = client.connect(server_addr, TEST_ALPN).await?; + let client_stats_task = tokio::spawn(collect_stats(conn.path_events())); + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(&vec![42u8; transfer_size]).await.anyerr()?; + send.finish().anyerr()?; + recv.read_to_end(transfer_size).await.anyerr()?; + conn.close(0u32.into(), b"thanks, bye!"); + client.close().await; + let client_stats = client_stats_task.await.expect("stats task panicked"); + let server_stats = server_task.await.anyerr()??; + + info!("client stats: {client_stats:#?}"); + info!("server stats: {server_stats:#?}"); + + let client_total_relay_tx = client_stats + .iter() + .filter(|(remote, _stats)| remote.is_relay()) + .map(|(_, stats)| stats.udp_tx.bytes) + .sum::(); + let client_total_relay_rx = client_stats + .iter() + .filter(|(remote, _stats)| remote.is_relay()) + .map(|(_, stats)| stats.udp_rx.bytes) + .sum::(); + let server_total_relay_tx = server_stats + .iter() + .filter(|(remote, _stats)| remote.is_relay()) + .map(|(_, stats)| stats.udp_tx.bytes) + .sum::(); + let server_total_relay_rx = server_stats + .iter() + .filter(|(remote, _stats)| remote.is_relay()) + .map(|(_, stats)| stats.udp_rx.bytes) + .sum::(); + + info!(?client_total_relay_tx, "total"); + info!(?client_total_relay_rx, "total"); + info!(?server_total_relay_tx, "total"); + info!(?server_total_relay_rx, "total"); + + // We should send/receive only the minorty of traffic via the relay. + assert!(client_total_relay_tx < transfer_size as u64 / 2); + assert!(client_total_relay_rx < transfer_size as u64 / 2); + assert!(server_total_relay_tx < transfer_size as u64 / 2); + assert!(server_total_relay_rx < transfer_size as u64 / 2); + + Ok(()) + } + + /// Tests that correct logs are emitted when connecting two endpoints with same secret keys to a relay. + #[tokio::test] + #[traced_test] + async fn same_endpoint_id_relay() -> Result { + let (relay_map, relay_url, _relay_server_guard) = run_relay_server().await?; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); + let secret_key = SecretKey::from_bytes(&rng.random()); + + let client = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .instrument(error_span!("ep-client")) + .await?; + + info!("client {}", client.id()); + + // bind ep1 and wait until connected to relay. + let ep1 = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .secret_key(secret_key.clone()) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .instrument(error_span!("ep1")) + .await?; + info!("ep1 bound {:?}", ep1.id()); + ep1.online().await; + info!("ep1 online"); + + let addr = EndpointAddr::new(secret_key.public()).with_relay_url(relay_url.clone()); + + tokio::try_join!( + async { + let conn = client.connect(addr.clone(), TEST_ALPN).await?; + let reason = conn.closed().await; + assert!(is_application_closed(&reason, 1)); + n0_error::Ok(()) + }, + async { + let conn = ep1.accept().await.unwrap().await?; + conn.close(1u32.into(), b"bye"); + n0_error::Ok(()) + } + )?; + info!("client connected to ep1"); + + // now start second endpoint with same secret key + let ep2 = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .secret_key(secret_key.clone()) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .instrument(error_span!("ep2")) + .await?; + info!("ep2 bound {:?}", ep2.id()); + ep2.online().await; + println!("ep2 online"); + + // `online` does not mean that the connection to the home relay was *established*, + // only that the home relay was *chosen* based on the net report probes. + // We need to wait for the connection to be established though, to be sure that new packets + // will be routed to the new endpoint and not to the old endpoint anymore. + // We don't expose being connected to the home relay on the endpoint currently, + // so we resort to log assertions. + // TODO(Frando): Replace once we add a proper API for this. + let expected_log_line = format!( + "ep2:endpoint{{id={}}}:relay-actor:active-relay{{url={relay_url}}}:connected: iroh::_events::relay::connected", + ep2.id().fmt_short() + ); + tokio::time::timeout(Duration::from_secs(5), async { + while !logs_contain(&expected_log_line) { + tokio::time::sleep(Duration::from_millis(10)).await + } + }) + .await + .std_context("relay connection did not establish in time")?; + + tokio::try_join!( + async { + let conn = client.connect(addr.clone(), TEST_ALPN).await?; + let reason = conn.closed().await; + assert!(is_application_closed(&reason, 1)); + n0_error::Ok(()) + }, + async { + let conn = ep2.accept().await.unwrap().await?; + conn.close(1u32.into(), b"bye"); + n0_error::Ok(()) + } + )?; + println!("client connected to ep2"); + + // assert that ep1 did not receive a connection + assert!(now_or_never(ep1.accept()).is_none()); + + // We assert that we get the warn log once for endpoint 1, and not at all for endpoint 2. + logs_assert(|logs| { + let expected_line = |line: &str| { + line.contains("WARN") && line.contains("Another endpoint connected with the same endpoint id. No more messages will be received") + }; + let count_line_ep1 = logs + .iter() + .filter(|line| line.contains(":ep1:") && expected_line(line)) + .count(); + let count_line_ep2 = logs + .iter() + .filter(|line| line.contains(":ep2:") && expected_line(line)) + .count(); + if count_line_ep1 == 1 && count_line_ep2 == 0 { + Ok(()) + } else { + Err("Logs don't match expectations".to_string()) + } + }); + tokio::join!(ep1.close(), ep2.close(), client.close()); + Ok(()) + } + + fn is_application_closed(close_reason: &ConnectionError, code: u32) -> bool { + matches!( + close_reason, + ConnectionError::ApplicationClosed(f) if f.error_code ==code.into() + ) + } + + #[tokio::test] + #[traced_test] + async fn test_closed_endpoint_behaviour() -> Result { + // create endpoint + // call endpoint.close + // ensure methods behave in the expected way + info!("Creating endpoint"); + let ep = Endpoint::builder(presets::N0).bind().await?; + let closed = ep.closed(); + info!("Closing endpoint"); + let now = Instant::now(); + ep.close().await; + info!("Endpoint closed in {:?}", now.elapsed()); + + // Assert that the `closed` cancellation token is now cancelled + assert_eq!(now_or_never(closed), Some(())); + + info!("Set ALPNS fails silently"); + ep.set_alpns(vec![b"test".into()]); + + info!("Insert Relay returns None"); + let relay_config = crate::defaults::staging::default_na_east_relay(); + assert!( + ep.insert_relay("localhost:300".parse()?, Arc::new(relay_config)) + .await + .is_none() + ); + + info!("Remove Relay returns None"); + assert!(ep.remove_relay(&"localhost:300".parse()?).await.is_none()); + + info!("Connecting"); + let mut rng = ChaCha8Rng::seed_from_u64(41); + let ep_id = SecretKey::from_bytes(&rng.random()).public(); + + // should likely be an error that states that the + // endpoint is closed instead: + if let ConnectError::Connect { source, .. } = ep.connect(ep_id, b"test").await.unwrap_err() + { + assert!(matches!( + source, + ConnectWithOptsError::EndpointClosed { .. } + )); + } else { + panic!("unexpected error for connect"); + } + + info!("Accepting!"); + assert!(ep.accept().await.is_none()); + + // this should work + info!("Addr: {:?}", ep.addr()); + + // create watchers to verify they terminate after the endpoint is dropped. + let mut addrs = ep.watch_addr().stream(); + + #[cfg(feature = "unstable-net-report")] + let mut net_reports = { + let net_reports = ep.net_report().stream(); + + // returns None + let net_report = ep.net_report().get(); + info!("last Net report {net_report:?}"); + net_reports + }; + + // this should work + let sockets = ep.bound_sockets(); + info!("Sockets: {sockets:?}"); + + // these should return errors + assert!(ep.dns_resolver().is_err()); + assert!(ep.address_lookup().is_err()); + + #[cfg(feature = "metrics")] + { + // this should work + let metrics = ep.metrics(); + info!("Metrics: {metrics:?}"); + } + + // this should return none + assert!(ep.remote_info(ep_id).await.is_none()); + + // this should fail silently + ep.network_change().await; + + // this should fail silently + ep.set_user_data_for_address_lookup(Some( + UserData::try_from("TEST".to_string()).expect("valid string"), + )); + drop(ep); + // now that the endpoint is dropped, all watchers should terminate. + tokio::time::timeout(Duration::from_secs(1), async { + while let Some(addr) = addrs.next().await { + info!("Addrs stream: {addr:?}"); + } + + #[cfg(feature = "unstable-net-report")] + while let Some(net_report) = net_reports.next().await { + info!("Net report stream: {net_report:?}"); + } + }) + .await + .expect("watchers not closed"); + + info!("Done!"); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_closed_endpoint_unpolled_accept_fut() -> Result { + info!("Creating endpoint"); + let ep = Endpoint::builder(presets::N0).bind().await?; + + info!("Get accept future"); + let accept_fut = ep.accept(); + + info!("Closing endpoint"); + let now = Instant::now(); + tokio::time::timeout(Duration::from_secs(5), ep.close()) + .await + .expect("Endpoint closes in a reasonable time"); + info!("Endpoint closed in {:?}", now.elapsed()); + + info!("Accept future returns None after the endpoint has closed"); + let incoming = accept_fut.await; + assert!(incoming.is_none()); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_closed_endpoint_polled_accept_fut() -> Result { + info!("Creating endpoint"); + let ep = Endpoint::builder(presets::N0).bind().await?; + + info!("Run an accept task"); + let ep2 = ep.clone(); + let accept_task = tokio::spawn(async move { + info!("Waiting on Accept"); + let res = ep2.accept().await; + info!("Accept await has returned"); + res + }); + + // Try to ensure the accept future is polled at least once. + tokio::time::sleep(Duration::from_millis(10)).await; + + info!("Closing the endpoint"); + tokio::time::timeout(Duration::from_secs(5), ep.close()) + .await + .expect("Endpoint closes in a reasonable time"); + info!("Endpoint closed"); + + info!("Await the accept task"); + let incoming = accept_task.await.expect("accept task panicked"); + assert!(incoming.is_none()); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_endpoint_online_add_relay() -> Result { + let ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(RelayMap::empty())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + // should not come online without relays. + let res = tokio::time::timeout(Duration::from_millis(500), ep.online()).await; + assert!(res.is_err()); + + // should come online after a relay is added. + let (relay_map, relay_url, _relay_server_guard) = run_relay_server().await?; + ep.insert_relay(relay_url.clone(), relay_map.get(&relay_url).unwrap()) + .await; + let res = tokio::time::timeout(Duration::from_millis(1000), ep.online()).await; + assert!(res.is_ok()); + + // online should still return after endpoint close, if the endpoint was last online + let ep_clone = ep.clone(); + let task = tokio::task::spawn(async move { + tokio::time::timeout(Duration::from_millis(500), ep_clone.online()).await + }); + ep.close().await; + let res = task.await.unwrap(); + assert!(res.is_ok()); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_endpoint_online_close() -> Result { + let ep = Endpoint::bind(presets::Minimal).await?; + // should not come online without relays. + let res = tokio::time::timeout(Duration::from_millis(500), ep.online()).await; + assert!(res.is_err()); + + // online should remain pending after the endpoint is closed. + let ep_clone = ep.clone(); + let task = tokio::task::spawn(async move { + tokio::time::timeout(Duration::from_millis(500), ep_clone.online()).await + }); + ep.close().await; + let res = task.await.unwrap(); + assert!(res.is_err()); + Ok(()) + } + + /// Verifies that an endpoint configured with [`RelayConfig::with_auth_token`] + /// is admitted to a relay whose access control checks the token only when + /// the token matches. + /// + /// Also verifies that [`RelayStatus::auth_denied_reason`] works correctly. + #[tokio::test] + #[traced_test] + async fn test_endpoint_relay_auth_token() -> Result { + const TOKEN: &str = "valid-token"; + const DENIAL_REASON: &str = "this token is no good"; + + /// Admits a connection only if it carries the expected auth token. + #[derive(Debug)] + struct TokenAccess(&'static str); + + impl iroh_relay::server::AccessControl for TokenAccess { + async fn on_connect(&self, request: &iroh_relay::server::ClientRequest) -> Access { + if request.auth_token().as_deref() == Some(self.0) { + Access::Allow + } else { + Access::Deny { + reason: Some(DENIAL_REASON.to_string()), + } + } + } + } + + let access = Arc::new(TokenAccess(TOKEN)); + let (_relay_map, relay_url, _guard) = run_relay_server_with_access(false, access).await?; + + // Wrong token: the connection attempt fails, and the status reports the + // relay-side denial both as an error and as an authentication failure. + let bad_map: RelayMap = RelayConfig::new(relay_url.clone(), None) + .with_auth_token("wrong-token") + .into(); + let bad_ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(bad_map)) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + let mut stream = bad_ep.home_relay_status().stream(); + let (auth_err, auth_denied_reason) = tokio::time::timeout(Duration::from_secs(5), async { + while let Some(status) = stream.next().await { + if let Some(relay) = status.iter().find(|s| s.last_error().is_some()) { + let err = relay.last_error().expect("checked above"); + return ( + format!("{err:#}"), + relay.auth_denied_reason().map(ToOwned::to_owned), + ); + } + } + panic!("home relay stream ended"); + }) + .await + .std_context("waiting for auth error")?; + assert!( + auth_err.contains(DENIAL_REASON), + "expected {DENIAL_REASON:?} in error, got: {auth_err}" + ); + assert_eq!( + auth_denied_reason.as_deref(), + Some(DENIAL_REASON), + "auth_denied_reason did not recognise a relay-side denial (error was: {auth_err})" + ); + + // Correct token: the endpoint reaches the connected state. + let good_map: RelayMap = RelayConfig::new(relay_url, None) + .with_auth_token(TOKEN) + .into(); + let good_ep = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(good_map)) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + tokio::time::timeout(Duration::from_secs(5), good_ep.online()) + .await + .std_context("waiting for endpoint to come online")?; + + Ok(()) + } +} diff --git a/vendor/iroh/src/endpoint/bind.rs b/vendor/iroh/src/endpoint/bind.rs new file mode 100644 index 0000000..b8f579e --- /dev/null +++ b/vendor/iroh/src/endpoint/bind.rs @@ -0,0 +1,252 @@ +use std::{ + convert::Infallible, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, +}; + +use n0_error::stack_error; + +/// Options when configuring binding an IP socket. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BindOpts { + /// Sets the network prefix length of the subnet for this interface. + /// + /// The prefix length is used in the routing table that is built to decide where + /// datagrams without a specific source address are sent. If these number of leading + /// bits in the destination IP address match the same number of leading bits in this + /// bound socket address, then it matches the subnet and the datagram will be sent + /// here. Otherwise the next bound sockets will be checked for a subnet match. Sockets + /// are ordered from longest prefix to shortest prefix. + /// + /// If no bound address has a matching subnet, the bind marked with + /// [`Self::is_default_route`] will be used. + /// + /// Note that most datagrams belonging to a traffic flow are in response to an incoming + /// datagram. Those are usually sent on the same bound socket as they were received and + /// will not consult the routing table derived from these bound sockets to select the + /// socket on which they will be sent. + prefix_len: u8, + /// If set, binding this interface is required and any errors will abort the + /// initialization of the endpoint. + /// + /// Defaults to `true`. + is_required: bool, + /// Whether this socket should be used as default route. + /// + /// The default route is used for outgoing datagrams not belonging to an existing + /// traffic flow, which does not fit in any subnet of the bound sockets. It is assumed + /// this subnet has a gateway router to route such packets. + /// + /// See [`Self::prefix_len`] for details of how such routing works. + is_default_route: Option, +} + +impl Default for BindOpts { + fn default() -> Self { + Self { + prefix_len: 0, + is_required: true, + is_default_route: None, + } + } +} + +impl BindOpts { + /// Sets the network prefix length of the subnet this interface is in. + /// + /// + /// The subnets of bound sockets are used to route outgoing datagrams not belonging to + /// an existing traffic flow to the socket they should be sent on. Subnets are ordered + /// from longest prefix length to shortest prefix length and the first subnet which + /// contains the destination IP address will be chosen. If no subnet matches but there + /// is a bound socket marked with [`Self::set_is_default_route`] then this socket will + /// be used. In this case it is assumed the attached subnet has a gateway router to + /// forward the datagram. + /// + /// Defaults to `0`, which means *all* IP addresses will belong to the subnet of this + /// socket's address. If multiple sockets of the same address family (IPv4 or IPv6) are + /// bound with such a `/0` prefix the socket which will be chosen is undefined. + /// + /// For IPv4 sockets the maximum prefix is `32`. For IPv6 the maximum prefix is `128`. + pub fn set_prefix_len(mut self, prefix_len: u8) -> Self { + self.prefix_len = prefix_len; + self + } + + /// Returns the `prefix_len`, see [`Self::set_prefix_len`]. + pub fn prefix_len(&self) -> u8 { + self.prefix_len + } + + /// Sets whether bind errors are fatal for this socket. + /// + /// If `false` and this socket fails to bind, the error will be silently ignored and the + /// endpoint will still be created. + /// + /// Defaults to `true`. + pub fn set_is_required(mut self, is_required: bool) -> Self { + self.is_required = is_required; + self + } + + /// Returns the value set by [`Self::set_is_required`]. + pub fn is_required(&self) -> bool { + self.is_required + } + + /// Sets whether this is a default route. + /// + /// The default route is used for outgoing datagrams not belonging to an existing + /// traffic flow, which does not fit in any subnet of the bound sockets. It is assumed + /// this subnet has a gateway router to route such packets. + /// + /// See [`Self::set_prefix_len`] for details on how this routing works. + /// + /// If not set explicitly, then [`Self::is_default_route`] will return `true` + /// if the prefix length is set to `0` (the default) and `false` otherwise. + pub fn set_is_default_route(mut self, is_default_route: bool) -> Self { + self.is_default_route = Some(is_default_route); + self + } + + /// Returns whether this is a default route. + /// + /// If [`Self::set_is_default_route`] has been called then that value is returned. + /// Otherwise, returns `true` if the [`prefix_len`][`Self::prefix_len`] is `0` and + /// `false` otherwise. + pub fn is_default_route(&self) -> bool { + match self.is_default_route { + Some(is_default) => is_default, + None => self.prefix_len() == 0, + } + } +} + +/// A simpler version of [`ToSocketAddrs`], that does not do any DNS resolution. +/// +/// [`ToSocketAddrs`]: std::net::ToSocketAddrs +pub trait ToSocketAddr { + /// Error type on failed conversion. + type Err: std::error::Error; + + /// Tries to convert this type to a [`SocketAddr`]. + fn to_socket_addr(&self) -> Result; +} + +impl ToSocketAddr for SocketAddr { + type Err = Infallible; + + fn to_socket_addr(&self) -> Result { + Ok(*self) + } +} + +impl ToSocketAddr for SocketAddrV4 { + type Err = Infallible; + + fn to_socket_addr(&self) -> Result { + Ok(SocketAddr::V4(*self)) + } +} + +impl ToSocketAddr for SocketAddrV6 { + type Err = Infallible; + + fn to_socket_addr(&self) -> Result { + Ok(SocketAddr::V6(*self)) + } +} + +impl ToSocketAddr for (IpAddr, u16) { + type Err = Infallible; + + fn to_socket_addr(&self) -> Result { + let (ip, port) = *self; + match ip { + IpAddr::V4(ref a) => (*a, port).to_socket_addr(), + IpAddr::V6(ref a) => (*a, port).to_socket_addr(), + } + } +} + +impl ToSocketAddr for (Ipv4Addr, u16) { + type Err = Infallible; + + fn to_socket_addr(&self) -> Result { + let (ip, port) = *self; + SocketAddrV4::new(ip, port).to_socket_addr() + } +} + +impl ToSocketAddr for (Ipv6Addr, u16) { + type Err = Infallible; + + fn to_socket_addr(&self) -> Result { + let (ip, port) = *self; + SocketAddrV6::new(ip, port, 0, 0).to_socket_addr() + } +} + +impl ToSocketAddr for (&str, u16) { + type Err = std::net::AddrParseError; + + fn to_socket_addr(&self) -> Result { + let (host, port) = *self; + + let addr = host.parse::()?; + let addr = SocketAddr::new(addr, port); + Ok(addr) + } +} + +impl ToSocketAddr for (String, u16) { + type Err = std::net::AddrParseError; + + fn to_socket_addr(&self) -> Result { + (&*self.0, self.1).to_socket_addr() + } +} + +impl ToSocketAddr for str { + type Err = std::net::AddrParseError; + + fn to_socket_addr(&self) -> Result { + let addr = self.parse()?; + Ok(addr) + } +} + +impl ToSocketAddr for &T { + type Err = T::Err; + + fn to_socket_addr(&self) -> Result { + (**self).to_socket_addr() + } +} + +impl ToSocketAddr for String { + type Err = std::net::AddrParseError; + + fn to_socket_addr(&self) -> Result { + (**self).to_socket_addr() + } +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta, from_sources)] +#[non_exhaustive] +pub enum InvalidSocketAddr { + #[error(transparent)] + AddrParse { + #[error(std_err)] + source: std::net::AddrParseError, + }, + #[error("Invalid IP prefix length")] + InvalidPrefixLength {}, + #[error(transparent)] + Infallible { + #[error(std_err)] + source: Infallible, + }, + #[error("Only a single default address can be set per IP family")] + DuplicateDefaultAddr, +} diff --git a/vendor/iroh/src/endpoint/connection.rs b/vendor/iroh/src/endpoint/connection.rs new file mode 100644 index 0000000..38ffbcd --- /dev/null +++ b/vendor/iroh/src/endpoint/connection.rs @@ -0,0 +1,1682 @@ +//! The [`Connection`] wraps a `noq::Connection`. +//! +//! The [`Connection`] is how you send data to and receive data from the remote endpoint. +//! +//! There are many transitions states between attempting to start a connection and +//! receiving a cryptographically secure connection. +//! +//! The main items in this module are: +//! +//! - [`Connection`] to create streams to talk to a remote endpoint. +//! - [`Connecting`] for operating on connections that haven't finished their handshake yet. +//! - [`Incoming`] to accept or reject an incoming connection. +//! - [`OutgoingZeroRttConnection`] to attempt to send 0-RTT data before the cryptographic +//! handshake has completed. +//! - [`IncomingZeroRttConnection`] to attempt to read 0-RTT or send 0.5-RTT data before the cryptographic +//! handshake has completed. +//! +//! [module docs]: crate +use std::{ + any::Any, + future::{Future, IntoFuture}, + net::SocketAddr, + pin::Pin, + sync::Arc, + task::Poll, +}; + +use bytes::Bytes; +use ed25519_dalek::{VerifyingKey, pkcs8::DecodePublicKey}; +use futures_util::{FutureExt, future::Shared}; +use iroh_base::{EndpointId, RelayUrl}; +use n0_error::{e, stack_error}; +use n0_future::{TryFutureExt, future::Boxed as BoxFuture, time::Duration}; +use noq::WeakConnectionHandle as NoqWeakConnectionHandle; +use pin_project::pin_project; +use tracing::{error, event, warn}; + +use super::quic::DecryptedInitial; +use crate::{ + Endpoint, + endpoint::{ + AfterHandshakeOutcome, + quic::{ + AcceptBi, AcceptUni, Closed, ConnectionError, ConnectionStats, Controller, + ExportKeyingMaterialError, OpenBi, OpenUni, PathId, ReadDatagram, ReadManyDatagrams, + SendDatagram, SendDatagramError, ServerConfig, Side, VarInt, + }, + }, + socket::{ + RemoteStateActorStoppedError, + remote_map::{PathEventStream, PathList, PathListStream, PathStateReceiver}, + transports::{self, LocalTransportAddr}, + }, +}; + +/// The remote address for an incoming connection. +/// +/// When the incoming connection is a direct connection, this is a SocketAddr. +/// When it is a relay connection, we know both the relay URL and the endpoint ID. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum IncomingAddr { + /// A direct connection from an IP address. + Ip(SocketAddr), + /// A connection via a relay. + Relay { + /// The URL of the relay. + url: RelayUrl, + /// The endpoint ID of the remote peer. + endpoint_id: EndpointId, + }, + /// A connection via a custom transport. + Custom(iroh_base::CustomAddr), +} + +impl From for iroh_base::TransportAddr { + fn from(addr: IncomingAddr) -> Self { + match addr { + IncomingAddr::Ip(addr) => Self::Ip(addr), + IncomingAddr::Relay { url, .. } => Self::Relay(url), + IncomingAddr::Custom(addr) => Self::Custom(addr), + } + } +} + +impl From for IncomingAddr { + fn from(addr: transports::Addr) -> Self { + match addr { + transports::Addr::Ip(addr) => Self::Ip(addr), + transports::Addr::Relay(url, endpoint_id) => Self::Relay { url, endpoint_id }, + transports::Addr::Custom(addr) => Self::Custom(addr), + } + } +} + +/// Future produced by [`Endpoint::accept`]. +#[derive(derive_more::Debug)] +#[pin_project] +pub struct Accept<'a> { + #[pin] + #[debug("noq::Accept")] + pub(crate) inner: noq::Accept<'a>, + pub(crate) ep: Endpoint, +} + +impl Future for Accept<'_> { + type Output = Option; + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let this = self.project(); + match this.inner.poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(None) => Poll::Ready(None), + Poll::Ready(Some(inner)) => { + let incoming = Incoming { + inner, + ep: this.ep.clone(), + }; + event!( + target: "iroh::_events::conn::incoming", + tracing::Level::DEBUG, + remote_addr = ?incoming.remote_addr(), + ); + Poll::Ready(Some(incoming)) + } + } + } +} + +/// An incoming connection for which the server has not yet begun its parts of the +/// handshake. +#[derive(Debug)] +pub struct Incoming { + inner: noq::Incoming, + ep: Endpoint, +} + +impl Incoming { + /// Attempts to accept this incoming connection (an error may still occur). + /// + /// Errors occurring here are likely not caused by the application or remote. The QUIC + /// connection listens on a normal UDP socket and any reachable network endpoint can + /// send datagrams to it, solicited or not. Even if the first few bytes look like a + /// QUIC packet, it might not even be a QUIC packet that is being received. + /// + /// Thus it is common to simply log the errors here and accept them as something which + /// can happen. + pub fn accept(self) -> Result { + self.inner + .accept() + .map(|conn| Accepting::new(conn, self.ep)) + } + + /// Accepts this incoming connection using a custom configuration. + /// + /// Use the [`Endpoint::create_server_config_builder`] method to create a [`ServerConfigBuilder`] + /// to customize a [`ServerConfig`]. + /// + /// See [`accept()`] for more details. + /// + /// [`accept()`]: Incoming::accept + /// [`Endpoint::create_server_config_builder`]: crate::Endpoint::create_server_config_builder + /// [`ServerConfigBuilder`]: crate::endpoint::ServerConfigBuilder + /// [`ServerConfig`]: crate::endpoint::ServerConfig + pub fn accept_with( + self, + server_config: Arc, + ) -> Result { + self.inner + .accept_with(server_config.to_inner_arc()) + .map(|conn| Accepting::new(conn, self.ep)) + } + + /// Rejects this incoming connection attempt. + pub fn refuse(self) { + self.inner.refuse() + } + + /// Responds with a retry packet. + /// + /// This requires the client to retry with address validation. + /// + /// Errors if `remote_address_validated()` is true. + #[allow(clippy::result_large_err)] + pub fn retry(self) -> Result<(), RetryError> { + self.inner + .retry() + .map_err(|err| e!(RetryError { err, ep: self.ep })) + } + + /// Ignores this incoming connection attempt, not sending any packet in response. + pub fn ignore(self) { + self.inner.ignore() + } + + /// Returns the local address that received this incoming connection. + pub fn local_addr(&self) -> LocalTransportAddr { + let remote_addr = self.inner.remote_address(); + let local_ip = self.inner.local_ip(); + self.ep.inner.to_local_transport_addr(local_ip, remote_addr) + } + + /// Returns the remote address of this incoming connection. + pub fn remote_addr(&self) -> IncomingAddr { + let remote = self.inner.remote_address(); + self.ep + .to_transport_addr(remote) + .unwrap_or_else(|| { + error!(mapped_addr = ?remote, "Incoming::remote_addr: invalid mapped address"); + transports::Addr::Ip(remote) + }) + .into() + } + + /// Whether the socket address that is initiating this connection has been validated. + /// + /// This means that the sender of the initial packet has proved that they can receive + /// traffic sent to `self.remote_addr()`. + pub fn remote_addr_validated(&self) -> bool { + self.inner.remote_address_validated() + } + + /// Decrypts the Initial packet payload. + /// + /// This clones and decrypts the packet payload (~1200 bytes). + /// Can be used to extract information from the TLS ClientHello without completing the handshake. + pub fn decrypt(&self) -> Option { + self.inner.decrypt() + } +} + +impl IntoFuture for Incoming { + type Output = Result; + type IntoFuture = IncomingFuture; + + fn into_future(self) -> Self::IntoFuture { + IncomingFuture(Box::pin(async move { + let noq_conn = self.inner.into_future().await?; + let conn = conn_from_noq_conn(noq_conn, &self.ep)?.await?; + Ok(conn) + })) + } +} + +/// Error for attempting to retry an [`Incoming`] which already bears a token from a previous retry +#[stack_error(derive, add_meta, from_sources)] +#[error("retry() with validated Incoming")] +pub struct RetryError { + err: noq::RetryError, + ep: Endpoint, +} + +impl RetryError { + /// Get the [`Incoming`] + pub fn into_incoming(self) -> Incoming { + Incoming { + inner: self.err.into_incoming(), + ep: self.ep, + } + } +} + +/// Adaptor to let [`Incoming`] be `await`ed like a [`Connecting`]. +#[derive(derive_more::Debug)] +#[debug("IncomingFuture")] +pub struct IncomingFuture(BoxFuture>); + +impl Future for IncomingFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + self.0.poll_unpin(cx) + } +} + +/// Extracts the ALPN protocol from the peer's handshake data. +fn alpn_from_noq_conn(conn: &noq::Connection) -> Option> { + let data = conn.handshake_data()?; + match data.downcast::() { + Ok(data) => data.protocol, + Err(_) => None, + } +} + +async fn alpn_from_noq_connecting(conn: &mut noq::Connecting) -> Result, AlpnError> { + let data = conn.handshake_data().await?; + match data.downcast::() { + Ok(data) => match data.protocol { + Some(protocol) => Ok(protocol), + None => Err(e!(AlpnError::Unavailable)), + }, + Err(_) => Err(e!(AlpnError::UnknownHandshake)), + } +} + +#[stack_error(add_meta, derive, from_sources)] +#[allow(missing_docs)] +#[non_exhaustive] +#[derive(Clone)] +pub enum AuthenticationError { + #[error(transparent)] + RemoteId { source: RemoteEndpointIdError }, + #[error("no ALPN provided")] + NoAlpn {}, +} + +/// Converts a `noq::Connection` to a `Connection`. +/// +/// Returns an error if there was a connection error, the handshake data has not completed +/// or if the remote did not set an ALPN. +/// +/// Otherwise returns a future that completes once the connection has been registered with the +/// socket. This future can return an [`RemoteStateActorStoppedError`], which will only be +/// emitted if the endpoint is closing. +/// +/// The returned future is `'static`, so it can be stored without being lifetime-bound on `&ep`. +fn conn_from_noq_conn( + conn: noq::Connection, + ep: &Endpoint, +) -> Result< + impl Future> + Send + 'static, + ConnectingError, +> { + let info = match static_info_from_conn(&conn) { + Ok(val) => val, + Err(auth_err) => { + // If the authentication error raced with a connection error, the connection + // error wins. + if let Some(conn_err) = conn.close_reason() { + return Err(e!(ConnectingError::ConnectionError { source: conn_err })); + } else { + return Err(e!(ConnectingError::HandshakeFailure { source: auth_err })); + } + } + }; + + event!( + target: "iroh::_events::conn::connected", + tracing::Level::DEBUG, + conn_id = conn.stable_id(), + side = ?conn.side(), + remote_id = %info.endpoint_id.fmt_short(), + alpn = %String::from_utf8_lossy(&info.alpn), + ); + + // Register this connection with the socket. + let fut = ep.inner.register_connection(info.endpoint_id, conn.clone()); + + // Check hooks + let inner = ep.inner.clone(); + Ok(async move { + let paths = fut.await?; + let conn = Connection { + data: HandshakeCompletedData { info, paths }, + inner: conn, + }; + + if let AfterHandshakeOutcome::Reject { error_code, reason } = + inner.hooks.after_handshake(&conn).await + { + conn.close(error_code, &reason); + return Err(e!(ConnectingError::LocallyRejected)); + } + + Ok(conn) + }) +} + +fn static_info_from_conn(conn: &noq::Connection) -> Result { + let endpoint_id = remote_id_from_noq_conn(conn)?; + let alpn = alpn_from_noq_conn(conn).ok_or_else(|| e!(AuthenticationError::NoAlpn))?; + Ok(StaticInfo { endpoint_id, alpn }) +} + +/// Returns the [`EndpointId`] from the peer's TLS certificate. +/// +/// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is +/// included in the TLS certificate presented during the handshake when connecting. +/// This function allows you to get the [`EndpointId`] of the remote endpoint of this +/// connection. +/// +/// [`PublicKey`]: iroh_base::PublicKey +fn remote_id_from_noq_conn(conn: &noq::Connection) -> Result { + let data = conn.peer_identity(); + match data { + None => { + warn!("no peer certificate found"); + Err(RemoteEndpointIdError::new()) + } + Some(data) => match data.downcast::>() { + Ok(certs) => { + if certs.len() != 1 { + warn!( + "expected a single peer certificate, but {} found", + certs.len() + ); + return Err(RemoteEndpointIdError::new()); + } + + let peer_id = EndpointId::from_verifying_key( + VerifyingKey::from_public_key_der(&certs[0]) + .map_err(|_| RemoteEndpointIdError::new())?, + ); + + Ok(peer_id) + } + Err(err) => { + warn!("invalid peer certificate: {:?}", err); + Err(RemoteEndpointIdError::new()) + } + }, + } +} + +/// An outgoing connection in progress. +/// +/// This future resolves to a [`Connection`] once the handshake completes. +#[derive(derive_more::Debug)] +pub struct Connecting { + inner: noq::Connecting, + /// Future to register the connection with the socket. + /// + /// This is set and polled after `inner` completes. We are using an option instead of an enum + /// because we need infallible access to `inner` in some methods. + #[debug("{}", register_with_socket.as_ref().map(|_| "Some(RegisterWithSocketFut)").unwrap_or("None"))] + register_with_socket: Option, + ep: Endpoint, + /// `Some(remote_id)` if this is an outgoing connection, `None` if this is an incoming conn + remote_endpoint_id: EndpointId, +} + +type RegisterWithSocketFut = BoxFuture>; + +/// In-progress connection attempt future +#[derive(derive_more::Debug)] +pub struct Accepting { + inner: noq::Connecting, + /// Future to register the connection with the socket. + /// + /// This is set and polled after `inner` completes. We are using an option instead of an enum + /// because we need infallible access to `inner` in some methods. + #[debug("{}", register_with_socket.as_ref().map(|_| "Some(RegisterWithSocketFut)").unwrap_or("None"))] + register_with_socket: Option, + ep: Endpoint, +} + +#[stack_error(add_meta, derive, from_sources)] +#[allow(missing_docs)] +#[non_exhaustive] +pub enum AlpnError { + #[error(transparent)] + ConnectionError { + #[error(std_err)] + source: ConnectionError, + }, + #[error("No ALPN available")] + Unavailable, + #[error("Unknown handshake type")] + UnknownHandshake, +} + +#[stack_error(add_meta, derive, from_sources)] +#[allow(missing_docs)] +#[non_exhaustive] +#[derive(Clone)] +#[allow(private_interfaces)] +pub enum ConnectingError { + #[error(transparent)] + ConnectionError { + #[error(std_err)] + source: ConnectionError, + }, + #[error("Failure finalizing the handshake")] + HandshakeFailure { source: AuthenticationError }, + #[error("internal consistency error")] + InternalConsistencyError { + /// Private source type, cannot be created publicly. + source: RemoteStateActorStoppedError, + }, + #[error("Connection was rejected locally")] + LocallyRejected, +} + +impl Connecting { + pub(crate) fn new( + inner: noq::Connecting, + ep: Endpoint, + remote_endpoint_id: EndpointId, + ) -> Self { + Self { + inner, + ep, + remote_endpoint_id, + register_with_socket: None, + } + } + + /// Converts this [`Connecting`] into a 0-RTT connection at the cost of weakened security. + /// + /// If 0-RTT can be attempted, returns a [`OutgoingZeroRttConnection`], which represents + /// outgoing 0-RTT connection. + /// + /// If the 0-RTT cannot even be attempted, returns back the same [`Connecting`] without + /// changes. You can still `.await` this [`Connecting`] to get a normal [`Connection`]. + /// + /// The [`OutgoingZeroRttConnection`] will attempt to resume a previous TLS session. However, + /// **the remote endpoint may actually _reject_ the 0-RTT data--yet still accept + /// the connection attempt in general**, once the handshake has completed. + /// + /// This possibility of whether the 0-RTT data was accepted or rejected is conveyed + /// through the [`ZeroRttStatus`] after calling [`OutgoingZeroRttConnection::handshake_completed`]. + /// When the handshake completes, it returns [`ZeroRttStatus::Accepted`] if the 0-RTT data + /// was accepted and [`ZeroRttStatus::Rejected`] if it was rejected. If it was rejected, the + /// existence of any streams opened and application data sent prior to the handshake + /// completing will not be conveyed to the remote application, and local operations on them + /// will return `ZeroRttRejected` errors. + /// + /// A server may reject 0-RTT data at its discretion, but accepting 0-RTT data requires the + /// relevant resumption state to be stored in the server, which servers may limit or lose for + /// various reasons including not persisting resumption state across server restarts. + /// + /// ## Security + /// + /// This enables transmission of 0-RTT data, which is vulnerable to replay attacks, and + /// should therefore never invoke non-idempotent operations. + /// + /// You can use [`RecvStream::is_0rtt`] to check whether a stream has been opened in 0-RTT + /// and thus whether parts of the stream are operating under this reduced security level. + /// + /// See also documentation for [`Accepting::into_0rtt`]. + /// + /// [`RecvStream::is_0rtt`]: noq::RecvStream::is_0rtt + #[allow(clippy::result_large_err)] + pub fn into_0rtt(self) -> Result { + match self.inner.into_0rtt() { + Ok((noq_conn, zrtt_accepted)) => { + let accepted: BoxFuture<_> = Box::pin({ + let noq_conn = noq_conn.clone(); + async move { + let accepted = zrtt_accepted.await; + let conn = conn_from_noq_conn(noq_conn, &self.ep)?.await?; + Ok(match accepted { + true => ZeroRttStatus::Accepted(conn), + false => ZeroRttStatus::Rejected(conn), + }) + } + }); + let accepted = accepted.shared(); + Ok(Connection { + inner: noq_conn, + data: OutgoingZeroRttData { accepted }, + }) + } + Err(inner) => Err(Self { inner, ..self }), + } + } + + /// Parameters negotiated during the handshake + pub async fn handshake_data(&mut self) -> Result, ConnectionError> { + self.inner.handshake_data().await + } + + /// Extracts the ALPN protocol from the peer's handshake data. + pub async fn alpn(&mut self) -> Result, AlpnError> { + alpn_from_noq_connecting(&mut self.inner).await + } + + /// Returns the [`EndpointId`] of the endpoint that this connection attempt tries to connect to. + pub fn remote_id(&self) -> EndpointId { + self.remote_endpoint_id + } +} + +impl Future for Connecting { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + loop { + if let Some(fut) = &mut self.register_with_socket { + return fut.poll_unpin(cx).map_err(Into::into); + } else { + let noq_conn = std::task::ready!(self.inner.poll_unpin(cx)?); + let fut = conn_from_noq_conn(noq_conn, &self.ep)?; + self.register_with_socket = Some(Box::pin(fut.err_into())); + } + } + } +} + +impl Accepting { + pub(crate) fn new(inner: noq::Connecting, ep: Endpoint) -> Self { + Self { + inner, + ep, + register_with_socket: None, + } + } + + /// Converts this [`Accepting`] into a 0-RTT or 0.5-RTT connection at the cost of weakened + /// security. + /// + /// Returns a [`IncomingZeroRttConnection`], which represents an incoming 0-RTT or 0.5-RTT connection. + /// + /// If the connection was initiated with 0-RTT by the remote endpoint, the local endpoint + /// might accept the 0-RTT attempt, allowing the local endpoint to receive application streams + /// and data before the handshake finishes. + /// + /// Otherwise this will enable 0.5-RTT, allowing the [`IncomingZeroRttConnection`] to open streams and send + /// data before the handshake finishes. + /// + /// ## Security + /// + /// Transmitted 0-RTT data from the client is vulnerable to replay attacks, and should + /// therefore never invoke non-idempotent operations. + /// + /// Transmission of 0.5-RTT data from the server may be sent before TLS client authentication + /// has occurred, and should therefore not be used to send data for which client + /// authentication is required. + /// + /// You can use [`RecvStream::is_0rtt`] to check whether a stream has been opened in 0-RTT + /// and thus whether parts of the stream are operating under this reduced security level. + /// + /// See also documentation for [`Connecting::into_0rtt`]. + /// + /// [`RecvStream::is_0rtt`]: crate::endpoint::RecvStream::is_0rtt + pub fn into_0rtt(self) -> IncomingZeroRttConnection { + let (noq_conn, zrtt_accepted) = self + .inner + .into_0rtt() + .expect("incoming connections can always be converted to 0-RTT"); + + let accepted: BoxFuture<_> = Box::pin({ + let noq_conn = noq_conn.clone(); + async move { + let _ = zrtt_accepted.await; + let conn = conn_from_noq_conn(noq_conn, &self.ep)?.await?; + Ok(conn) + } + }); + let accepted = accepted.shared(); + + IncomingZeroRttConnection { + inner: noq_conn, + data: IncomingZeroRttData { accepted }, + } + } + + /// Returns the remote address of this connection. + pub fn remote_addr(&self) -> IncomingAddr { + let remote = self.inner.remote_address(); + self.ep + .to_transport_addr(remote) + .unwrap_or_else(|| { + error!(mapped_addr = ?remote, "Accepting::remote_addr: invalid mapped address"); + transports::Addr::Ip(remote) + }) + .into() + } + + /// Parameters negotiated during the handshake + pub async fn handshake_data(&mut self) -> Result, ConnectionError> { + self.inner.handshake_data().await + } + + /// Extracts the ALPN protocol from the peer's handshake data. + pub async fn alpn(&mut self) -> Result, AlpnError> { + alpn_from_noq_connecting(&mut self.inner).await + } +} + +impl Future for Accepting { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + loop { + if let Some(fut) = &mut self.register_with_socket { + return fut.poll_unpin(cx).map_err(Into::into); + } else { + let noq_conn = std::task::ready!(self.inner.poll_unpin(cx)?); + match conn_from_noq_conn(noq_conn, &self.ep) { + Err(err) => return Poll::Ready(Err(err)), + Ok(fut) => self.register_with_socket = Some(Box::pin(fut.err_into())), + }; + } + } + } +} + +/// The client side of a 0-RTT connection. +/// +/// This is created using [`Connecting::into_0rtt`]. +/// +/// Creating a `OutgoingZeroRttConnection` means that the endpoint is capable +/// of attempting a 0-RTT connection with the remote. The remote may still +/// reject the 0-RTT connection. In which case, any data sent before the +/// handshake has completed may need to be resent. +/// +/// Look at the [`OutgoingZeroRttConnection::handshake_completed`] method for +/// more details. +pub type OutgoingZeroRttConnection = Connection; + +/// Returned from [`OutgoingZeroRttConnection::handshake_completed`]. +#[derive(Debug, Clone)] +pub enum ZeroRttStatus { + /// If the 0-RTT data was accepted, you can continue to use any streams + /// that were created before the handshake was completed. + Accepted(Connection), + /// If the 0-RTT data was rejected, any streams that were created before + /// the handshake was completed will error and any data that was + /// previously sent on those streams will need to be resent. + Rejected(Connection), +} + +/// A QUIC connection on the server-side that can possibly accept 0-RTT data. +/// +/// It is very similar to a `Connection`, but the `IncomingZeroRttConnection::remote_id` +/// and `IncomingZeroRttConnection::alpn` may not be set yet, since the handshake has +/// not necessarily occurred yet. +/// +/// If the `IncomingZeroRttConnection` has rejected 0-RTT or does not have enough information +/// to accept 0-RTT, any received 0-RTT packets will simply be dropped before +/// reaching any receive streams. +/// +/// Any streams that are created to send or receive data can continue to be used +/// even after the handshake has completed and we are no longer in a 0-RTT +/// situation. +/// +/// Use the [`IncomingZeroRttConnection::handshake_completed`] method to get a [`Connection`] from a +/// `IncomingZeroRttConnection`. This waits until 0-RTT connection has completed +/// the handshake and can now confidently derive the ALPN and the +/// [`EndpointId`] of the remote endpoint. +pub type IncomingZeroRttConnection = Connection; + +/// A QUIC connection. +/// +/// If all references to a connection (including every clone of the Connection handle, +/// streams of incoming streams, and the various stream types) have been dropped, then the +/// connection will be automatically closed with an error_code of 0 and an empty reason. You +/// can also close the connection explicitly by calling [`Connection::close`]. +/// +/// Closing the connection immediately abandons efforts to deliver data to the peer. Upon +/// receiving CONNECTION_CLOSE the peer may drop any stream data not yet delivered to the +/// application. [`Connection::close`] describes in more detail how to gracefully close a +/// connection without losing application data. +/// +/// May be cloned to obtain another handle to the same connection. +#[derive(Debug, Clone)] +pub struct Connection { + inner: noq::Connection, + /// State-specific information + data: State::Data, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct HandshakeCompletedData { + info: StaticInfo, + paths: PathStateReceiver, +} + +/// Static info from a completed TLS handshake. +#[derive(Debug, Clone)] +struct StaticInfo { + endpoint_id: EndpointId, + alpn: Vec, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct IncomingZeroRttData { + accepted: Shared>>, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct OutgoingZeroRttData { + accepted: Shared>>, +} + +mod sealed { + pub trait Sealed {} +} + +/// Trait to track the state of a [`Connection`] at compile time. +pub trait ConnectionState: sealed::Sealed { + /// State-specific data stored in the [`Connection`]. + type Data: std::fmt::Debug + Clone; +} + +/// Marker type for a connection that has completed the handshake. +#[derive(Debug, Clone)] +pub struct HandshakeCompleted; + +/// Marker type for a connection that is in the incoming 0-RTT state. +#[derive(Debug, Clone)] +pub struct IncomingZeroRtt; + +/// Marker type for a connection that is in the outgoing 0-RTT state. +#[derive(Debug, Clone)] +pub struct OutgoingZeroRtt; + +impl sealed::Sealed for HandshakeCompleted {} +impl ConnectionState for HandshakeCompleted { + type Data = HandshakeCompletedData; +} +impl sealed::Sealed for IncomingZeroRtt {} +impl ConnectionState for IncomingZeroRtt { + type Data = IncomingZeroRttData; +} + +impl sealed::Sealed for OutgoingZeroRtt {} +impl ConnectionState for OutgoingZeroRtt { + type Data = OutgoingZeroRttData; +} + +#[allow(missing_docs)] +#[stack_error(add_meta, derive)] +#[error("Protocol error: no remote id available")] +#[derive(Clone)] +pub struct RemoteEndpointIdError; + +impl Connection { + /// Initiates a new outgoing unidirectional stream. + /// + /// A unidirectional stream can only transmit data from the endpoint which opens the + /// stream, the endpoint accepting the stream can not send any data back on the same + /// stream. + /// + /// # QUIC streams + /// + /// QUIC can multiplex many streams onto a single connection. Streams can be short or + /// long lived and may be opened and closed without incurring any extra cost. The data + /// sent in each stream is delivered strictly ordered, yet multiple streams will be + /// transmitted interleaved and packet loss on one stream will not delay other + /// streams. Thus streams do not suffer head-of-line blocking. + /// + /// # Opening streams + /// + /// Both peers of a connection can open streams at any time. Opening a new stream does + /// not incur any extra overhead compared to sending data on an existing stream. However + /// only once some data has been transmitted on the stream, will the peer become aware + /// of the newly opened stream. + /// + /// # Accepting streams + /// + /// Each stream needs to be *accepted* by the peer, using either [`Self::accept_uni`] or + /// [`Self::accept_bi`] depending on the stream type. Repeated accept call will yield a + /// new stream whenever the peer opens a new stream. + /// + /// Note that opening a stream is not sufficient for the accept call to yield a new + /// stream. Data must be sent on a stream before the respective accept call at the peer + /// will yield a [`RecvStream`]. + /// + /// # Stream priorities + /// + /// Streams can have different priorities set using [`SendStream::set_priority`]. Data + /// of streams with a higher priority will be transmitted to the peer before data from + /// streams with a lower priority. + /// + /// # Stream limits + /// + /// The number of streams which can be open concurrently defaults to + /// [`QuicTransportConfigBuilder::max_concurrent_uni_streams`] and + /// [`QuicTransportConfigBuilder::max_concurrent_bidi_streams`]. While the connection is + /// open these limits can be changed using [`Self::set_max_concurrent_uni_streams`] and + /// [`Self::set_max_concurrent_bi_streams`]. + /// + /// Each stream has a *receive window* of a maximum number of bytes that may be + /// in-flight before the sender is blocked from transmitting more. This is configured in + /// [`QuicTransportConfigBuilder::stream_receive_window`]. There is also a + /// [`QuicTransportConfigBuilder::receive_window`] which applies to all streams combined + /// and can be changed during a connection using [`Self::set_receive_window`]. + /// + /// The protocol limits the total number of streams during the lifetime of a connection + /// to 2**62, this limit applies to the sum of uni- and bi-directional streams. For most + /// practical purposes this is essentially unlimited. + /// + /// [`QuicTransportConfigBuilder::max_concurrent_uni_streams`]: super::QuicTransportConfigBuilder::max_concurrent_uni_streams + /// [`QuicTransportConfigBuilder::max_concurrent_bidi_streams`]: super::QuicTransportConfigBuilder::max_concurrent_bidi_streams + /// [`QuicTransportConfigBuilder::stream_receive_window`]: super::QuicTransportConfigBuilder::stream_receive_window + /// [`QuicTransportConfigBuilder::receive_window`]: super::QuicTransportConfigBuilder::receive_window + /// [`SendStream::set_priority`]: super::SendStream::set_priority + /// [`RecvStream`]: super::RecvStream + #[inline] + pub fn open_uni(&self) -> OpenUni<'_> { + self.inner.open_uni() + } + + /// Initiates a new outgoing bidirectional stream. + /// + /// Bidirectional streams allows both peers to send as well as receive data. They act as + /// a pair of related unidirectional streams. + /// + /// See [`Self::open_uni`] for a detailed description of how streams work. + #[inline] + pub fn open_bi(&self) -> OpenBi<'_> { + self.inner.open_bi() + } + + /// Accepts the next incoming uni-directional stream. + /// + /// See [`Self::open_uni`] for a detailed description of how streams work. + #[inline] + pub fn accept_uni(&self) -> AcceptUni<'_> { + self.inner.accept_uni() + } + + /// Accepts the next incoming bidirectional stream. + /// + /// See [`Self::open_uni`] for a detailed description of how streams work. + #[inline] + pub fn accept_bi(&self) -> AcceptBi<'_> { + self.inner.accept_bi() + } + + /// Receives an application datagram. + #[inline] + pub fn read_datagram(&self) -> ReadDatagram<'_> { + self.inner.read_datagram() + } + + /// Receives a batch of application datagrams into `out`, in arrival order. + /// + /// This is the batch analogue of [`read_datagram()`](Self::read_datagram). The returned + /// future resolves once at least one datagram is buffered, drains up to `out.len()` of + /// them into `out` from the front, and yields the count written. Use this instead of + /// `read_datagram()` in a loop when forwarding bursts: a whole batch is taken under a + /// single lock hold. + #[inline] + pub fn read_many_datagrams<'a, 'b>( + &'a self, + out: &'b mut [Bytes], + ) -> ReadManyDatagrams<'a, 'b> { + self.inner.read_many_datagrams(out) + } + + /// Waits for the connection to be closed for any reason. + /// + /// Despite the return type's name, closed connections are often not an error condition + /// at the application layer. Cases that might be routine include + /// [`ConnectionError::LocallyClosed`] and [`ConnectionError::ApplicationClosed`]. + #[inline] + pub async fn closed(&self) -> ConnectionError { + self.inner.closed().await + } + + /// If the connection is closed, the reason why. + /// + /// Returns `None` if the connection is still open. + #[inline] + pub fn close_reason(&self) -> Option { + self.inner.close_reason() + } + + /// Closes the connection immediately. + /// + /// Pending operations will fail immediately with [`ConnectionError::LocallyClosed`]. No + /// more data is sent to the peer and the peer may drop buffered data upon receiving the + /// CONNECTION_CLOSE frame. + /// + /// `error_code` and `reason` are not interpreted, and are provided directly to the + /// peer. + /// + /// `reason` will be truncated to fit in a single packet with overhead; to improve odds + /// that it is preserved in full, it should be kept under 1KiB. + /// + /// # Gracefully closing a connection + /// + /// Only the peer last receiving application data can be certain that all data is + /// delivered. The only reliable action it can then take is to close the connection, + /// potentially with a custom error code. The delivery of the final CONNECTION_CLOSE + /// frame is very likely if both endpoints stay online long enough, calling + /// [`Endpoint::close`] will wait to provide sufficient time. Otherwise, the remote peer + /// will time out the connection, provided that the idle timeout is not disabled. + /// + /// The sending side can not guarantee all stream data is delivered to the remote + /// application. It only knows the data is delivered to the QUIC stack of the remote + /// endpoint. Once the local side sends a CONNECTION_CLOSE frame in response to calling + /// [`close`] the remote endpoint may drop any data it received but is as yet + /// undelivered to the application, including data that was acknowledged as received to + /// the local endpoint. + /// + /// [`close`]: Connection::close + #[inline] + pub fn close(&self, error_code: VarInt, reason: &[u8]) { + self.inner.close(error_code, reason) + } + + /// Transmits `data` as an unreliable, unordered application datagram. + /// + /// Application datagrams are a low-level primitive. They may be lost or delivered out + /// of order, and `data` must both fit inside a single QUIC packet and be smaller than + /// the maximum dictated by the peer. + #[inline] + pub fn send_datagram(&self, data: Bytes) -> Result<(), SendDatagramError> { + self.inner.send_datagram(data) + } + + /// Transmits many unreliable, unordered application datagrams in a single call. + /// + /// This is the batch analogue of [`send_datagram()`](Self::send_datagram): it queues the + /// whole batch under one lock hold and wakes the driver once, reducing the per-datagram + /// overhead of calling `send_datagram()` repeatedly. Like `send_datagram()`, older queued + /// datagrams may be dropped to make room. + /// + /// Returns the number of datagrams queued. The batch is rejected with + /// [`SendDatagramError::TooLarge`] if any datagram exceeds the maximum datagram size. + #[inline] + pub fn send_many_datagrams(&self, datagrams: &[Bytes]) -> Result { + self.inner.send_many_datagrams(datagrams) + } + + /// Transmits `data` as an unreliable, unordered application datagram + /// + /// Unlike [`send_datagram()`], this method will wait for buffer space during congestion + /// conditions, which effectively prioritizes old datagrams over new datagrams. + /// + /// See [`send_datagram()`] for details. + /// + /// [`send_datagram()`]: Connection::send_datagram + #[inline] + pub fn send_datagram_wait(&self, data: Bytes) -> SendDatagram<'_> { + self.inner.send_datagram_wait(data) + } + + /// Computes the maximum size of datagrams that may be passed to [`send_datagram`]. + /// + /// Returns `None` if datagrams are unsupported by the peer or disabled locally. + /// + /// This may change over the lifetime of a connection according to variation in the path + /// MTU estimate. The peer can also enforce an arbitrarily small fixed limit, but if the + /// peer's limit is large this is guaranteed to be a little over a kilobyte at minimum. + /// + /// Not necessarily the maximum size of received datagrams. + /// + /// [`send_datagram`]: Self::send_datagram + #[inline] + pub fn max_datagram_size(&self) -> Option { + self.inner.max_datagram_size() + } + + /// Bytes available in the outgoing datagram buffer. + /// + /// When greater than zero, calling [`send_datagram`] with a + /// datagram of at most this size is guaranteed not to cause older datagrams to be + /// dropped. + /// + /// [`send_datagram`]: Self::send_datagram + #[inline] + pub fn datagram_send_buffer_space(&self) -> usize { + self.inner.datagram_send_buffer_space() + } + + /// Current best estimate of this connection's latency (round-trip-time). + #[inline] + pub fn rtt(&self, path_id: PathId) -> Option { + self.inner.rtt(path_id) + } + + /// Returns connection statistics. + #[inline] + pub fn stats(&self) -> ConnectionStats { + self.inner.stats() + } + + /// Current state of the congestion control algorithm, for debugging purposes. + #[inline] + pub fn congestion_state(&self, path_id: PathId) -> Option> { + self.inner.congestion_state(path_id) + } + + /// Parameters negotiated during the handshake. + /// + /// Guaranteed to return `Some` on fully established connections or after + /// [`Connecting::handshake_data()`] succeeds. See that method's documentations for + /// details on the returned value. + /// + /// [`Connection::handshake_data()`]: crate::endpoint::Connecting::handshake_data + #[inline] + pub fn handshake_data(&self) -> Option> { + self.inner.handshake_data() + } + + /// Cryptographic identity of the peer. + /// + /// The dynamic type returned is determined by the configured [`Session`]. For the + /// default `rustls` session, the return value can be [`downcast`] to a + /// Vec<[rustls::pki_types::CertificateDer]> + /// + /// [`Session`]: noq_proto::crypto::Session + /// [`downcast`]: Box::downcast + #[inline] + pub fn peer_identity(&self) -> Option> { + self.inner.peer_identity() + } + + /// A stable identifier for this connection. + /// + /// Peer addresses and connection IDs can change, but this value will remain fixed for + /// the lifetime of the connection. + #[inline] + pub fn stable_id(&self) -> usize { + self.inner.stable_id() + } + + /// Derives keying material from this connection's TLS session secrets. + /// + /// When both peers call this method with the same `label` and `context` + /// arguments and `output` buffers of equal length, they will get the + /// same sequence of bytes in `output`. These bytes are cryptographically + /// strong and pseudorandom, and are suitable for use as keying material. + /// + /// See [RFC5705](https://tools.ietf.org/html/rfc5705) for more information. + #[inline] + pub fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> Result<(), ExportKeyingMaterialError> { + self.inner.export_keying_material(output, label, context) + } + + /// Modifies the number of unidirectional streams that may be concurrently opened. + /// + /// No streams may be opened by the peer unless fewer than `count` are already + /// open. Large `count`s increase both minimum and worst-case memory consumption. + #[inline] + pub fn set_max_concurrent_uni_streams(&self, count: VarInt) { + self.inner.set_max_concurrent_uni_streams(count) + } + + /// Sets the connection-level flow control receive window. + /// + /// See [`QuicTransportConfigBuilder::receive_window`]. + /// + /// [`QuicTransportConfigBuilder::receive_window`]: super::QuicTransportConfigBuilder::receive_window + #[inline] + pub fn set_receive_window(&self, receive_window: VarInt) { + self.inner.set_receive_window(receive_window) + } + + /// Modifies the number of bidirectional streams that may be concurrently opened. + /// + /// No streams may be opened by the peer unless fewer than `count` are already + /// open. Large `count`s increase both minimum and worst-case memory consumption. + #[inline] + pub fn set_max_concurrent_bi_streams(&self, count: VarInt) { + self.inner.set_max_concurrent_bi_streams(count) + } +} + +impl Connection { + /// Extracts the ALPN protocol from the peer's handshake data. + pub fn alpn(&self) -> &[u8] { + &self.data.info.alpn + } + + /// Returns the [`EndpointId`] from the peer's TLS certificate. + /// + /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is + /// included in the TLS certificate presented during the handshake when connecting. + /// This function allows you to get the [`EndpointId`] of the remote endpoint of this + /// connection. + /// + /// [`PublicKey`]: iroh_base::PublicKey + pub fn remote_id(&self) -> EndpointId { + self.data.info.endpoint_id + } + + /// Returns the currently open network paths for this connection. + /// + /// A connection typically has one path via the relay server and, + /// once holepunching succeeds, a direct path. The returned + /// [`PathList`] is a snapshot taken at call time: it does not + /// reflect later changes, and it does not include paths that have + /// already closed. + /// + /// To observe changes over time, see [`Connection::paths_stream`] + /// for a stream of [`PathList`] snapshots and + /// [`Connection::path_events`] for individual [`PathEvent`]s. + /// + /// [`PathEvent`]: crate::endpoint::PathEvent + pub fn paths(&self) -> PathList<'_> { + self.data.paths.get(&self.inner) + } + + /// Returns a stream of [`PathList`] snapshots for this connection. + /// + /// Yields the current snapshot on the first poll, and a fresh + /// snapshot whenever the open paths or the selected path change. + /// Ends when the connection closes. + /// + /// The stream borrows this [`Connection`]. To consume it from a + /// spawned task, move a [`Connection`] clone into the task and + /// call this method inside. + pub fn paths_stream(&self) -> PathListStream<'_> { + self.data.paths.stream(&self.inner) + } + + /// Returns a stream of [`PathEvent`]s for this connection. + /// + /// Each event reports one of: a path opened, a path closed (with + /// final per-path statistics), the selected transmission path + /// changed, or the consumer fell behind. The stream ends when the + /// connection closes. It does not borrow this [`Connection`] and + /// may be moved into a spawned task. + /// + /// If the consumer does not poll fast enough, the stream yields a + /// single [`PathEvent::Lagged`]; the current state of the open + /// paths and the selected path remains recoverable via + /// [`Connection::paths`]. + /// + /// [`PathEvent`]: crate::endpoint::PathEvent + /// [`PathEvent::Lagged`]: crate::endpoint::PathEvent::Lagged + pub fn path_events(&self) -> PathEventStream { + self.data.paths.events() + } + + /// Returns the side of the connection (client or server). + pub fn side(&self) -> Side { + self.inner.side() + } + + /// Returns a [`WeakConnectionHandle`] for this connection. + /// + /// A [`WeakConnectionHandle`] does not keep the connection alive. It can be used to + /// wait for the connection to be closed via [`WeakConnectionHandle::closed`] and to + /// attempt to upgrade back to a strong [`Connection`] via + /// [`WeakConnectionHandle::upgrade`]. + pub fn weak_handle(&self) -> WeakConnectionHandle { + WeakConnectionHandle { + data: self.data.clone(), + inner: self.inner.weak_handle(), + } + } +} + +impl Connection { + /// Extracts the ALPN protocol from the peer's handshake data. + pub fn alpn(&self) -> Option> { + alpn_from_noq_conn(&self.inner) + } + + /// Waits until the full handshake occurs and then returns a [`Connection`]. + /// + /// This may fail with [`ConnectingError::ConnectionError`], if there was + /// some general failure with the connection, such as a network timeout since + /// we accepted the connection. + /// + /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side + /// doesn't use the right TLS authentication, which usually every iroh endpoint + /// uses and requires. + /// + /// Thus, those errors should only occur if someone connects to you with a + /// modified iroh endpoint or with a plain QUIC client. + pub async fn handshake_completed(&self) -> Result { + self.data.accepted.clone().await + } + + /// Returns the [`EndpointId`] from the peer's TLS certificate. + /// + /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is + /// included in the TLS certificate presented during the handshake when connecting. + /// This function allows you to get the [`EndpointId`] of the remote endpoint of this + /// connection. + /// + /// [`PublicKey`]: iroh_base::PublicKey + pub fn remote_id(&self) -> Result { + remote_id_from_noq_conn(&self.inner) + } +} + +impl Connection { + /// Extracts the ALPN protocol from the peer's handshake data. + pub fn alpn(&self) -> Option> { + alpn_from_noq_conn(&self.inner) + } + + /// Waits until the full handshake occurs and returns a [`ZeroRttStatus`]. + /// + /// If `ZeroRttStatus::Accepted` is returned, then any streams created before + /// the handshake has completed can still be used. + /// + /// If `ZeroRttStatus::Rejected` is returned, then any streams created before + /// the handshake will error and any data sent should be re-sent on a + /// new stream. + /// + /// This may fail with [`ConnectingError::ConnectionError`], if there was + /// some general failure with the connection, such as a network timeout since + /// we initiated the connection. + /// + /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side + /// doesn't use the right TLS authentication, which usually every iroh endpoint + /// uses and requires. + /// + /// Thus, those errors should only occur if someone connects to you with a + /// modified iroh endpoint or with a plain QUIC client. + pub async fn handshake_completed(&self) -> Result { + self.data.accepted.clone().await + } + + /// Returns the [`EndpointId`] from the peer's TLS certificate. + /// + /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is + /// included in the TLS certificate presented during the handshake when connecting. + /// This function allows you to get the [`EndpointId`] of the remote endpoint of this + /// connection. + /// + /// [`PublicKey`]: iroh_base::PublicKey + pub fn remote_id(&self) -> Result { + remote_id_from_noq_conn(&self.inner) + } +} + +/// A weak handle to a [`Connection`]. +/// +/// A [`WeakConnectionHandle`] does not keep the connection alive: holding one will not +/// prevent the connection from being closed when the last [`Connection`] handle is dropped. +/// +/// Use [`upgrade`] to obtain a strong [`Connection`], and [`closed`] to wait for the +/// connection to be closed without keeping it alive. +/// +/// [`upgrade`]: WeakConnectionHandle::upgrade +/// [`closed`]: WeakConnectionHandle::closed +#[derive(Debug, Clone)] +pub struct WeakConnectionHandle { + data: HandshakeCompletedData, + inner: NoqWeakConnectionHandle, +} + +impl WeakConnectionHandle { + /// Attempts to upgrade this weak handle to a strong [`Connection`]. + /// + /// Returns `None` if the connection has already been dropped. + pub fn upgrade(&self) -> Option { + self.inner.upgrade().map(|inner| Connection { + inner, + data: self.data.clone(), + }) + } + + /// Returns a future that resolves once the connection has been closed. + /// + /// If no strong references to the [`Connection`] exist at the time this is called, + /// the future resolves to `None`. If at least one strong reference still exists at + /// the time of this call, the returned future is guaranteed to receive the close + /// event with the close reason and final connection statistics, even if all strong + /// references are dropped before the future is awaited. + /// + /// The future does not keep the connection alive. + pub fn closed(&self) -> impl Future> + Send + 'static { + let registered = self.inner.upgrade().map(|conn| conn.on_closed()); + async move { + match registered { + Some(fut) => Some(fut.await), + None => None, + } + } + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::time::Duration; + + use iroh_base::{EndpointAddr, SecretKey}; + use iroh_relay::tls::CaTlsConfig; + use n0_error::{Result, StackResultExt, StdResultExt}; + use n0_future::{Stream, StreamExt}; + use n0_tracing_test::traced_test; + use rand::{RngExt, SeedableRng}; + use tracing::{Instrument, error_span, info, info_span, trace_span}; + + use super::Endpoint; + use crate::{ + RelayMode, + endpoint::{ConnectOptions, Incoming, PathList, ZeroRttStatus, presets}, + test_utils::run_relay_server, + }; + + const TEST_ALPN: &[u8] = b"n0/iroh/test"; + + async fn spawn_0rtt_server(secret_key: SecretKey, log_span: tracing::Span) -> Result { + let server = Endpoint::builder(presets::Minimal) + .secret_key(secret_key) + .alpns(vec![TEST_ALPN.to_vec()]) + .bind() + .instrument(log_span.clone()) + .await?; + + async fn handle_incoming(incoming: Incoming) -> Result { + let accepting = incoming + .accept() + .std_context("Failed to accept incoming connection")?; + + // accept a possible 0-RTT connection + let zrtt_conn = accepting.into_0rtt(); + + let (mut send, mut recv) = zrtt_conn + .accept_bi() + .await + .std_context("failed to accept bi stream")?; + + let data = recv + .read_to_end(10_000_000) + .await + .std_context("Failed to read data")?; + + send.write_all(&data) + .await + .std_context("Failed to write data")?; + send.finish().std_context("Failed to finish send")?; + + // Stay alive until the other side closes the connection. + zrtt_conn.closed().await; + Ok(()) + } + + // Gets aborted via the endpoint closing causing an `Err` + // a simple echo server + tokio::spawn({ + let server = server.clone(); + async move { + tracing::trace!("Server accept loop started"); + while let Some(incoming) = server.accept().await { + tracing::trace!("Server received incoming connection"); + // Handle connection errors gracefully instead of exiting the task + if let Err(e) = handle_incoming(incoming).await { + tracing::warn!("Failure while handling connection: {e:#}"); + } + tracing::trace!("Connection closed, ready for next"); + } + tracing::trace!("Server accept loop exiting"); + n0_error::Ok(()) + } + .instrument(log_span) + }); + + Ok(server) + } + + async fn connect_client_0rtt_expect_err( + client: &Endpoint, + server_addr: EndpointAddr, + ) -> Result { + let conn = client + .connect_with_opts(server_addr, TEST_ALPN, ConnectOptions::new()) + .await? + .into_0rtt() + .expect_err("expected 0-RTT to fail") + .await?; + + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + send.finish().anyerr()?; + let received = recv.read_to_end(1_000).await.anyerr()?; + assert_eq!(&received, b"hello"); + conn.close(0u32.into(), b"thx"); + Ok(()) + } + + async fn connect_client_0rtt_expect_ok( + client: &Endpoint, + server_addr: EndpointAddr, + expect_server_accepts: bool, + ) -> Result { + tracing::trace!(?server_addr, "Client connecting with 0-RTT"); + let zrtt_conn = client + .connect_with_opts(server_addr, TEST_ALPN, ConnectOptions::new()) + .await + .context("connect")? + .into_0rtt() + .ok() + .context("into_0rtt")?; + + tracing::trace!("Client established 0-RTT connection"); + // This is how we send data in 0-RTT: + let (mut send, mut recv) = zrtt_conn.open_bi().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + send.finish().anyerr()?; + tracing::trace!("Client sent 0-RTT data, waiting for server response"); + // When this resolves, we've gotten a response from the server about whether the 0-RTT data above was accepted: + let zrtt_res = zrtt_conn.handshake_completed().await; + tracing::trace!(?zrtt_res, "Server responded to 0-RTT"); + let zrtt_res = zrtt_res.context("handshake completed")?; + let conn = match zrtt_res { + ZeroRttStatus::Accepted(conn) => { + assert!(expect_server_accepts); + conn + } + ZeroRttStatus::Rejected(conn) => { + assert!(!expect_server_accepts); + // in this case we need to re-send data by re-creating the stream. + let (mut send, r) = conn.open_bi().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + send.finish().anyerr()?; + recv = r; + conn + } + }; + let received = recv.read_to_end(1_000).await.anyerr()?; + assert_eq!(&received, b"hello"); + conn.close(0u32.into(), b"thx"); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_0rtt() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); + let client = Endpoint::bind(presets::Minimal).await?; + let server = + spawn_0rtt_server(SecretKey::from_bytes(&rng.random()), info_span!("server")).await?; + + connect_client_0rtt_expect_err(&client, server.addr()).await?; + // The second 0rtt attempt should work + connect_client_0rtt_expect_ok(&client, server.addr(), true).await?; + + client.close().await; + server.close().await; + + Ok(()) + } + + // We have this test, as this would've failed at some point. + // This effectively tests that we correctly categorize the TLS session tickets we + // receive into the respective "bucket" for the recipient. + #[tokio::test] + #[traced_test] + async fn test_0rtt_non_consecutive() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); + let client = Endpoint::bind(presets::Minimal).await?; + let server = + spawn_0rtt_server(SecretKey::from_bytes(&rng.random()), info_span!("server")).await?; + + connect_client_0rtt_expect_err(&client, server.addr()).await?; + + // connecting with another endpoint should not interfere with our + // TLS session ticket cache for the first endpoint: + let another = + spawn_0rtt_server(SecretKey::from_bytes(&rng.random()), info_span!("another")).await?; + connect_client_0rtt_expect_err(&client, another.addr()).await?; + another.close().await; + + connect_client_0rtt_expect_ok(&client, server.addr(), true).await?; + + client.close().await; + server.close().await; + + Ok(()) + } + + // Test whether 0-RTT is possible after a restart: + #[tokio::test] + #[traced_test] + async fn test_0rtt_after_server_restart() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); + let client = Endpoint::builder(presets::Minimal) + .bind() + .instrument(info_span!("client")) + .await?; + let server_key = SecretKey::from_bytes(&rng.random()); + let server = spawn_0rtt_server(server_key.clone(), info_span!("server-initial")).await?; + + connect_client_0rtt_expect_err(&client, server.addr()) + .instrument(trace_span!("connect1")) + .await + .context("client connect 1")?; + connect_client_0rtt_expect_ok(&client, server.addr(), true) + .instrument(trace_span!("connect2")) + .await + .context("client connect 2")?; + + // adds time to the test, but we need to ensure the server is fully closed before spawning the next one. + server.close().await; + + let server = spawn_0rtt_server(server_key, info_span!("server-restart")).await?; + + // we expect the client to *believe* it can 0-RTT connect to the server (hence expect_ok), + // but the server will reject the early data because it discarded necessary state + // to decrypt it when restarting. + connect_client_0rtt_expect_ok(&client, server.addr(), false) + .instrument(trace_span!("connect3")) + .await + .context("client connect 3")?; + + tokio::join!(client.close(), server.close()); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_paths_watcher() -> Result { + const ALPN: &[u8] = b"test"; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let (relay_map, _relay_url, _guard) = run_relay_server().await?; + let server = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .secret_key(SecretKey::from_bytes(&rng.random())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await?; + + let client = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .secret_key(SecretKey::from_bytes(&rng.random())) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + + server.online().await; + let server_addr = server.addr(); + info!("server addr: {server_addr:?}"); + + let (conn_client, conn_server) = tokio::join!( + async { client.connect(server_addr, ALPN).await.unwrap() }, + async { server.accept().await.unwrap().await.unwrap() } + ); + info!("connected"); + + let mut paths_client = conn_client.paths_stream(); + let mut paths_server = conn_server.paths_stream(); + + /// Advances the path stream until at least one IP and one relay path is available. + /// + /// Panics if the path stream finishes before that happens. + async fn wait_for_paths(mut stream: impl Send + Unpin + Stream>) { + loop { + let paths = stream.next().await.expect("paths stream ended"); + info!(?paths, "paths"); + if paths.len() >= 2 + && paths.iter().any(|p| p.is_relay()) + && paths.iter().any(|p| p.is_ip()) + { + info!("break"); + return; + } + } + } + + // Verify that both connections are notified of path changes and get an IP and a relay path. + tokio::join!( + async { + tokio::time::timeout(Duration::from_secs(1), wait_for_paths(&mut paths_server)) + .instrument(error_span!("paths-server")) + .await + .unwrap() + }, + async { + tokio::time::timeout(Duration::from_secs(1), wait_for_paths(&mut paths_client)) + .instrument(error_span!("paths-client")) + .await + .unwrap() + } + ); + + tokio::time::pause(); + + // Close the client connection. + info!("close client conn"); + conn_client.close(0u32.into(), b""); + + // Verify that the path watch streams close shortly after the connection is closed + tokio::time::timeout(Duration::from_nanos(1), async { + while paths_client.next().await.is_some() {} + }) + .await + .expect("client paths watcher did not close within 1s of connection close"); + tokio::time::timeout(Duration::from_nanos(1), async { + while paths_server.next().await.is_some() {} + }) + .await + .expect("server paths watcher did not close within 1s of connection close"); + + server.close().await; + client.close().await; + + Ok(()) + } +} diff --git a/vendor/iroh/src/endpoint/hooks.rs b/vendor/iroh/src/endpoint/hooks.rs new file mode 100644 index 0000000..9c81825 --- /dev/null +++ b/vendor/iroh/src/endpoint/hooks.rs @@ -0,0 +1,171 @@ +use std::pin::Pin; + +use iroh_base::EndpointAddr; + +use crate::endpoint::{connection::Connection, quic::VarInt}; + +type BoxFuture<'a, T> = Pin + Send + 'a>>; + +/// Outcome of [`EndpointHooks::before_connect`] +#[derive(Debug)] +pub enum BeforeConnectOutcome { + /// Accept the connect attempt. + Accept, + /// Reject the connect attempt. + Reject, +} + +/// Outcome of [`EndpointHooks::after_handshake`] +#[derive(Debug)] +pub enum AfterHandshakeOutcome { + /// Accept the connection. + Accept, + /// Reject and close the connection. + /// + /// See [`Connection::close`] for details on `error_code` and `reason`. + /// + /// [`Connection::close`]: crate::endpoint::Connection::close + Reject { + /// Error code to send with the connection close frame. + error_code: VarInt, + /// Close reason to send with the connection close frame. + reason: Vec, + }, +} + +impl AfterHandshakeOutcome { + /// Returns [`Self::Accept`]. + pub fn accept() -> Self { + Self::Accept + } + + /// Returns [`Self::Reject`]. + pub fn reject(&self, error_code: VarInt, reason: &[u8]) -> Self { + Self::Reject { + error_code, + reason: reason.to_vec(), + } + } +} + +/// EndpointHooks intercept the connection establishment process of an [`Endpoint`]. +/// +/// Use [`Builder::hooks`] to install hooks onto an endpoint. +/// +/// For each hook, all installed hooks are invoked in the order they were installed on +/// the endpoint builder. If a hook returns `Accept`, processing continues with the next +/// hook. If a hook returns `Reject`, processing is aborted and further hooks +/// are not invoked for this hook point. +/// +/// ## Notes to implementers +/// +/// As hooks are stored on the endpoint, you must make sure to never store an [`Endpoint`] +/// on the hook struct itself, as this would create reference counting loop and cause the +/// endpoint to never be dropped, leaking memory. +/// +/// [`Endpoint`]: crate::Endpoint +/// [`Builder::hooks`]: crate::endpoint::Builder::hooks +pub trait EndpointHooks: std::fmt::Debug + Send + Sync { + /// Intercept outgoing connections before they are started. + /// + /// This is called whenever a new outgoing connection is initiated via [`Endpoint::connect`] + /// or [`Endpoint::connect_with_opts`]. + /// + /// If any hook returns [`BeforeConnectOutcome::Reject`], the connection attempt is aborted + /// before any packets are sent to the remote. + /// + /// [`Endpoint::connect`]: crate::Endpoint::connect + /// [`Endpoint::connect_with_opts`]: crate::Endpoint::connect_with_opts + fn before_connect<'a>( + &'a self, + _remote_addr: &'a EndpointAddr, + _alpn: &'a [u8], + ) -> impl Future + Send + 'a { + async { BeforeConnectOutcome::Accept } + } + + /// Intercept both incoming and outgoing connections once the TLS handshake has completed. + /// + /// At this point in time, we know the remote's endpoint id and ALPN. If any hook returns + /// [`AfterHandshakeOutcome::Reject`], the connection is closed with the provided error code + /// and reason. + /// + /// The hook receives the [`Connection`] by reference so that implementations can read any + /// information they need synchronously. Do not clone the [`Connection`] out of the hook: + /// holding a strong [`Connection`] handle keeps the connection alive and disables + /// close-on-drop, which the primary use sites of the connection might rely on. If you need to + /// keep a reference for later (for example to wait for the connection to close, or to + /// look up the connection in a map), call [`Connection::weak_handle`] and store the + /// resulting [`WeakConnectionHandle`] instead. + /// + /// [`WeakConnectionHandle`]: crate::endpoint::WeakConnectionHandle + fn after_handshake<'a>( + &'a self, + _conn: &'a Connection, + ) -> impl Future + Send + 'a { + async { AfterHandshakeOutcome::accept() } + } +} + +pub(crate) trait DynEndpointHooks: std::fmt::Debug + Send + Sync { + fn before_connect<'a>( + &'a self, + remote_addr: &'a EndpointAddr, + alpn: &'a [u8], + ) -> BoxFuture<'a, BeforeConnectOutcome>; + fn after_handshake<'a>(&'a self, conn: &'a Connection) -> BoxFuture<'a, AfterHandshakeOutcome>; +} + +impl DynEndpointHooks for T { + fn before_connect<'a>( + &'a self, + remote_addr: &'a EndpointAddr, + alpn: &'a [u8], + ) -> BoxFuture<'a, BeforeConnectOutcome> { + Box::pin(EndpointHooks::before_connect(self, remote_addr, alpn)) + } + + fn after_handshake<'a>(&'a self, conn: &'a Connection) -> BoxFuture<'a, AfterHandshakeOutcome> { + Box::pin(EndpointHooks::after_handshake(self, conn)) + } +} + +#[derive(Debug, Default)] +pub(crate) struct EndpointHooksList { + inner: Vec>, +} + +impl EndpointHooksList { + pub(super) fn push(&mut self, hook: impl EndpointHooks + 'static) { + let hook: Box = Box::new(hook); + self.inner.push(hook); + } + + pub(super) async fn before_connect( + &self, + remote_addr: &EndpointAddr, + alpn: &[u8], + ) -> BeforeConnectOutcome { + for hook in self.inner.iter() { + match hook.before_connect(remote_addr, alpn).await { + BeforeConnectOutcome::Accept => continue, + reject @ BeforeConnectOutcome::Reject => { + return reject; + } + } + } + BeforeConnectOutcome::Accept + } + + pub(super) async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome { + for hook in self.inner.iter() { + match hook.after_handshake(conn).await { + AfterHandshakeOutcome::Accept => continue, + reject @ AfterHandshakeOutcome::Reject { .. } => { + return reject; + } + } + } + AfterHandshakeOutcome::Accept + } +} diff --git a/vendor/iroh/src/endpoint/presets.rs b/vendor/iroh/src/endpoint/presets.rs new file mode 100644 index 0000000..461a6a0 --- /dev/null +++ b/vendor/iroh/src/endpoint/presets.rs @@ -0,0 +1,184 @@ +//! Presets allow configuring an endpoint quickly with a chosen set of defaults. +//! +//! # Example +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # { +//! # async fn wrapper() -> n0_error::Result { +//! use iroh::{Endpoint, RelayMode, Watcher, endpoint::presets}; +//! +//! let endpoint = Endpoint::builder(presets::N0).bind().await?; +//! # let _ = endpoint; +//! # Ok(()) +//! # } +//! # } +//! ``` + +use crate::endpoint::Builder; + +/// A reusable bundle of endpoint [`Builder`] configuration. +pub trait Preset { + /// Applies the configuration to the passed in [`Builder`]. + fn apply(self, builder: Builder) -> Builder; +} + +/// An empty preset that doesn't set anything on the builder. +/// +/// This doesn't set mandatory builder options, so using this in +/// `Endpoint::bind(presets::Empty)` will always fail. +/// +/// However, it can be useful, if you want control over all mandatory options +/// yourself, by using `Endpoint::builder(presets::Empty)`. +/// +/// If you prefer a minimal version that is guaranteed to work, see the +/// [`Minimal`] preset. +#[derive(Debug, Copy, Clone, Default)] +pub struct Empty; + +impl Preset for Empty { + fn apply(self, builder: Builder) -> Builder { + builder + } +} + +/// A preset that is almost empty, besides setting mandatory options. +/// +/// At the moment the only mandatory option to set on the endpoint builder is +/// [`Builder::crypto_provider`]. This preset makes a choice for that based on +/// the current set of enabled features in iroh, which is why it's only available +/// with the `tls-ring` or `tls-aws-lc-rs` feature flag. +/// +/// It uses either [ring] or [aws-lc-rs], depending on which feature is enabled +/// on iroh (preferring ring if both are enabled). +/// +/// [ring]: rustls::crypto::ring::default_provider +/// [aws-lc-rs]: rustls::crypto::aws_lc_rs::default_provider +#[cfg(with_crypto_provider)] +#[derive(Debug, Copy, Clone, Default)] +pub struct Minimal; + +#[cfg(with_crypto_provider)] +impl Preset for Minimal { + fn apply(self, mut builder: Builder) -> Builder { + use std::sync::Arc; + + #[cfg(feature = "tls-ring")] + { + builder = builder.crypto_provider(Arc::new(rustls::crypto::ring::default_provider())); + } + + #[cfg(all(feature = "tls-aws-lc-rs", not(feature = "tls-ring")))] + { + builder = + builder.crypto_provider(Arc::new(rustls::crypto::aws_lc_rs::default_provider())); + } + + builder + } +} + +/// Configures the endpoint to use the n0 defaults +/// +/// Currently this consists of +/// - the DNS Address Lookup service. +/// - the default relay servers provided by Number 0. +/// - setting the [`rustls::crypto::CryptoProvider`] to [ring] or [aws-lc-rs], depending +/// on which feature is enabled in iroh (preferring ring if both are enabled). +/// +/// Due to the last point, this preset is only available with the `tls-ring` or +/// `tls-aws-lc-rs` feature enabled. +/// If you want to set your own crypto provider, we recommend copying the +/// implementation of this preset into your own and setting the appropriate crypto +/// provider there. +/// +/// The default address lookup service publishes to and resolves from the +/// n0.computer dns server `iroh.link`. +/// +/// This is equivalent to adding a [`crate::address_lookup::PkarrPublisher`], +/// a [`crate::address_lookup::PkarrResolver`], and (outside browsers) a +/// [`crate::address_lookup::DnsAddressLookup`], all configured to use the +/// n0.computer dns server. +/// +/// This will by default use [`N0_DNS_PKARR_RELAY_PROD`]. +/// When in tests, or when the `test-utils` feature is enabled, this will use the +/// [`N0_DNS_PKARR_RELAY_STAGING`]. +/// +/// [ring]: rustls::crypto::ring::default_provider +/// [aws-lc-rs]: rustls::crypto::aws_lc_rs::default_provider +/// [`N0_DNS_PKARR_RELAY_PROD`]: crate::address_lookup::N0_DNS_PKARR_RELAY_PROD +/// [`N0_DNS_PKARR_RELAY_STAGING`]: crate::address_lookup::N0_DNS_PKARR_RELAY_STAGING +#[cfg(with_crypto_provider)] +#[derive(Debug, Copy, Clone, Default)] +pub struct N0; + +#[cfg(with_crypto_provider)] +impl Preset for N0 { + fn apply(self, mut builder: Builder) -> Builder { + use crate::{ + address_lookup::{PkarrPublisher, PkarrResolver}, + endpoint::default_relay_mode, + }; + + builder = Minimal.apply(builder); + + builder = builder.address_lookup(PkarrPublisher::n0_dns()); + + // Resolve using HTTPS requests to our DNS server's /pkarr path. + builder = builder.address_lookup(PkarrResolver::n0_dns()); + + // Additionally resolve using DNS queries outside browsers. + #[cfg(not(wasm_browser))] + { + builder = builder.address_lookup(crate::address_lookup::DnsAddressLookup::n0_dns()); + } + + builder = builder.relay_mode(default_relay_mode()); + + builder + } +} + +/// Configures the endpoint to use the n0 defaults, but with relay mode disabled. +/// +/// Currently this consists of +/// - setting `RelayMode::Disabled` +/// - the DNS Address Lookup service, that publishes IP addresses rather than +/// relay urls. +/// - setting the [`rustls::crypto::CryptoProvider`] to [ring] or [aws-lc-rs], depending +/// on which feature is enabled in iroh (preferring ring if both are enabled). +/// +/// Due to the last point, this preset is only available with the `tls-ring` or +/// `tls-aws-lc-rs` feature enabled. +/// If you want to set your own crypto provider, we recommend copying the +/// implementation of this preset into your own and setting the appropriate crypto +/// provider there. +/// +/// The default address lookup service publishes to and resolves from the +/// n0.computer dns server `iroh.link`. +/// +/// This is equivalent to adding a [`crate::address_lookup::PkarrPublisher`], +/// a [`crate::address_lookup::PkarrResolver`], and (outside browsers) a +/// [`crate::address_lookup::DnsAddressLookup`], all configured to use the +/// n0.computer dns server. +/// +/// This will by default use [`N0_DNS_PKARR_RELAY_PROD`]. +/// When in tests, or when the `test-utils` feature is enabled, this will use the +/// [`N0_DNS_PKARR_RELAY_STAGING`]. +/// +/// [ring]: rustls::crypto::ring::default_provider +/// [aws-lc-rs]: rustls::crypto::aws_lc_rs::default_provider +/// [`N0_DNS_PKARR_RELAY_PROD`]: crate::address_lookup::N0_DNS_PKARR_RELAY_PROD +/// [`N0_DNS_PKARR_RELAY_STAGING`]: crate::address_lookup::N0_DNS_PKARR_RELAY_STAGING +#[cfg(with_crypto_provider)] +#[derive(Debug, Copy, Clone, Default)] +pub struct N0DisableRelay; + +#[cfg(with_crypto_provider)] +impl Preset for N0DisableRelay { + fn apply(self, builder: Builder) -> Builder { + use crate::RelayMode; + + N0.apply(builder).relay_mode(RelayMode::Disabled) + } +} diff --git a/vendor/iroh/src/endpoint/quic.rs b/vendor/iroh/src/endpoint/quic.rs new file mode 100644 index 0000000..2d98570 --- /dev/null +++ b/vendor/iroh/src/endpoint/quic.rs @@ -0,0 +1,717 @@ +//! Exporting and encapsulating structs from noq +//! +//! Co-locates all iroh-noq exports. +//! +//! There are some structs that we use in particular ways, where we would like +//! to limit or expand how those structs are used in iroh. By encapsulating them +//! we can ensure the functionality needed to make iroh work. + +#[cfg(feature = "qlog")] +use std::path::Path; +use std::{sync::Arc, time::Duration}; + +/// `noq` types that are used in the public iroh API. +// Each type is notated with the iroh type or noq type that uses it. +pub use noq::{ + AcceptBi, // iroh::endpoint::Connection + AcceptUni, // iroh::endpoint::Connection + AckFrequencyConfig, // iroh::endpoint::quic::QuicTransportConfig + Closed, // iroh::endpoint::WeakConnectionHandle + ClosedStream, // iroh::protocol::AcceptError, noq::RecvStream, noq::SendStream + ConnectionError, // iroh::endpoint::ConnectError + ConnectionStats, // iroh::endpoint::Connection + Dir, // noq::StreamId + IdleTimeout, // iroh::endpoint::quic::QuicTransportConfig + MtuDiscoveryConfig, // iroh::endpoint::quic::QuicTransportConfig + OpenBi, // iroh::endpoint::Connection + OpenUni, // iroh::endpoint::Connection + PathStats, // iroh::socket::remote_map::remote_state::PathInfo + ReadDatagram, // iroh::endpoint::Connection + ReadError, // noq::RecvStream + ReadExactError, // noq::RecvStream + ReadManyDatagrams, // iroh::endpoint::Connection + ReadToEndError, // noq::RecvStream + RecvStream, // noq::AcceptBi, noq::AcceptUni, noq::OpenBi, noq::OpenUni + ResetError, // noq::RecvStream + SendDatagram, // iroh::endpoint::Connection + SendDatagramError, // iroh::endpoint::Connection + SendStream, // noq::AcceptBi, noq::OpenUni + Side, // iroh::endpoint::Connection, noq::StreamId, + StoppedError, // noq::SendStream + StreamId, // noq::RecvStream + UnorderedRecvStream, // noq::RecvStream + VarInt, // various + VarIntBoundsExceeded, // noq::VarInt, noq::IdleTimeout + WriteError, // noq::SendStream +}; +#[cfg(feature = "qlog")] +pub use noq::{QlogConfig, QlogFactory, QlogFileFactory}; +// `noq_proto` types that are used in the public iroh API. +// Each type is notated with the iroh type or noq type that uses it. +pub use noq_proto::{ + ApplicationClose, // noq::ConnectionError + Chunk, // noq::RecvStream + ConnectError as QuicConnectError, // iroh::endpoint::ConnectWithOptsError + ConnectionClose, // noq::ConnectionError + DecryptedInitial, // iroh::endpoint::connection::Incoming + FrameStats, // noq::ConnectionStats + FrameType, // noq_proto::TransportError + IncomingAlpns, // iroh::endpoint::DecryptedInitial + PathId, // noq_proto::crypto::PacketKey + RttEstimator, // noq_proto::congestion::Controller + TimeSource, // iroh::endpoint::quic::ServerConfig + TokenLog, // noq::ValidationTokenConfig + TokenReuseError, // noq::TokenLog + TransportError, // noq::ConnectionError + TransportErrorCode, // noq_proto::TransportError + UdpStats, // noq::ConnectionStats + ValidationTokenConfig, // iroh::endpoint::quic::::ServerConfig + congestion::{ + Controller, // iroh::endpoint::Connection + ControllerFactory, // iroh::endpoint::quic::QuicTransportConfig + ControllerMetrics, // noq_proto::congestion::Controller + }, + crypto::{ + CryptoError, // noq_proto::crypto::CryptoError, noq_proto::crypto::PacketKey + ExportKeyingMaterialError, // iroh::endpoint::Connection + HandshakeTokenKey, // iroh::endpoint::quic::ServerConfig + HeaderKey, // noq_proto::crypto::Keys + Keys, // noq_proto::crypto::Session + PacketKey, // noq_proto::crypto::Keys + UnsupportedVersion, // noq_proto::ConnectError + }, + transport_parameters::TransportParameters, // noq_proto::crypot::ServerConfig +}; +use tracing::warn; + +use crate::socket::{ + HEARTBEAT_INTERVAL, MAX_MULTIPATH_PATHS, MAX_QNT_ADDRESSES, PATH_MAX_IDLE_TIMEOUT, +}; + +/// Builder for a [`QuicTransportConfig`]. +#[derive(Debug, Clone)] +pub struct QuicTransportConfigBuilder(noq::TransportConfig); + +/// Parameters governing the core QUIC state machine +/// +/// Default values should be suitable for most internet applications. Applications protocols which +/// forbid remotely-initiated streams should set `max_concurrent_bidi_streams` and +/// `max_concurrent_uni_streams` to zero. +/// +/// In some cases, performance or resource requirements can be improved by tuning these values to +/// suit a particular application and/or network connection. In particular, data window sizes can be +/// tuned for a particular expected round trip time, link capacity, and memory availability. Tuning +/// for higher bandwidths and latencies increases worst-case memory consumption, but does not impair +/// performance at lower bandwidths and latencies. The default configuration is tuned for a 100Mbps +/// link with a 100ms round trip time. +/// +/// Use the [`QuicTransportConfigBuilder`] to customize these tunable fields. +/// +/// In iroh, the config has some specific default values that make iroh's holepunching work +/// well with QUIC multipath. Adjusting those settings may cause suboptimal usage. +/// +/// Look at the following methods for more details: +/// - [`QuicTransportConfigBuilder::default_path_keep_alive_interval`] +/// - [`QuicTransportConfigBuilder::default_path_max_idle_timeout`] +/// - [`QuicTransportConfigBuilder::max_concurrent_multipath_paths`] +/// - [`QuicTransportConfigBuilder::max_remote_nat_traversal_addresses`] +/// +/// # Examples +/// ``` +/// use std::time::Duration; +/// +/// use iroh::endpoint::QuicTransportConfig; +/// +/// let _cfg = QuicTransportConfig::builder() +/// .send_observed_address_reports(true) +/// .build(); +/// ``` +#[derive(Debug, Clone)] +pub struct QuicTransportConfig(Arc); + +impl QuicTransportConfig { + /// Returns a default [`QuicTransportConfigBuilder`] that allows customizing + /// a [`QuicTransportConfig`]. + pub fn builder() -> QuicTransportConfigBuilder { + QuicTransportConfigBuilder::new() + } +} + +impl Default for QuicTransportConfig { + fn default() -> Self { + QuicTransportConfigBuilder::new().build() + } +} + +impl QuicTransportConfig { + pub(crate) fn to_inner_arc(&self) -> Arc { + self.0.clone() + } +} + +impl QuicTransportConfigBuilder { + /// Creates a default [`QuicTransportConfigBuilder`]. + fn new() -> Self { + let mut cfg = noq::TransportConfig::default(); + // Override some transport config settings. + cfg.keep_alive_interval(Some(HEARTBEAT_INTERVAL)); + cfg.default_path_keep_alive_interval(Some(HEARTBEAT_INTERVAL)); + cfg.default_path_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)); + cfg.max_concurrent_multipath_paths(MAX_MULTIPATH_PATHS); + cfg.max_remote_nat_traversal_addresses(MAX_QNT_ADDRESSES); + cfg.server_handshake_migration(true); + Self(cfg) + } + + /// Builds a [`QuicTransportConfig`] from the builder. + pub fn build(self) -> QuicTransportConfig { + QuicTransportConfig(Arc::new(self.0)) + } + + /// Maximum number of incoming bidirectional streams that may be open concurrently. + /// + /// Must be nonzero for the peer to open any bidirectional streams. + /// + /// Worst-case memory use is directly proportional to `max_concurrent_bidi_streams * + /// stream_receive_window`, with an upper bound proportional to `receive_window`. + pub fn max_concurrent_bidi_streams(mut self, value: VarInt) -> Self { + self.0.max_concurrent_bidi_streams(value); + self + } + + /// Variant of `max_concurrent_bidi_streams` affecting unidirectional streams. + pub fn max_concurrent_uni_streams(mut self, value: VarInt) -> Self { + self.0.max_concurrent_uni_streams(value); + self + } + + /// Maximum duration of inactivity to accept before timing out the connection. + /// + /// The true idle timeout is the minimum of this and the peer's own max idle timeout. `None` + /// represents an infinite timeout. Defaults to 30 seconds. + /// + /// **WARNING**: If a peer or its network path malfunctions or acts maliciously, an infinite + /// idle timeout can result in permanently hung futures! + /// + /// ``` + /// # use std::{convert::TryInto, time::Duration}; + /// # use iroh::endpoint::{QuicTransportConfig, VarInt, VarIntBoundsExceeded}; + /// # fn main() -> Result<(), VarIntBoundsExceeded> { + /// let mut builder = QuicTransportConfig::builder() + /// // Set the idle timeout as `VarInt`-encoded milliseconds + /// .max_idle_timeout(Some(VarInt::from_u32(10_000).into())); + /// + /// // Set the idle timeout as a `Duration` + /// builder = builder.max_idle_timeout(Some(Duration::from_secs(10).try_into()?)); + /// + /// let _cfg = builder.build(); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn max_idle_timeout(mut self, value: Option) -> Self { + self.0.max_idle_timeout(value); + self + } + + /// Maximum number of bytes the peer may transmit without acknowledgement on any one stream + /// before becoming blocked. + /// + /// This should be set to at least the expected connection latency multiplied by the maximum + /// desired throughput. Setting this smaller than `receive_window` helps ensure that a single + /// stream doesn't monopolize receive buffers, which may otherwise occur if the application + /// chooses not to read from a large stream for a time while still requiring data on other + /// streams. + pub fn stream_receive_window(mut self, value: VarInt) -> Self { + self.0.stream_receive_window(value); + self + } + + /// Maximum number of bytes the peer may transmit across all streams of a connection before + /// becoming blocked. + /// + /// This should be set to at least the expected connection latency multiplied by the maximum + /// desired throughput. Larger values can be useful to allow maximum throughput within a + /// stream while another is blocked. + pub fn receive_window(mut self, value: VarInt) -> Self { + self.0.receive_window(value); + self + } + + /// Maximum number of bytes to transmit to a peer without acknowledgment. + /// + /// Provides an upper bound on memory when communicating with peers that issue large amounts of + /// flow control credit. Endpoints that wish to handle large numbers of connections robustly + /// should take care to set this low enough to guarantee memory exhaustion does not occur if + /// every connection uses the entire window. + pub fn send_window(mut self, value: u64) -> Self { + self.0.send_window(value); + self + } + + /// Whether to implement fair queuing for send streams having the same priority. + /// + /// When enabled, connections schedule data from outgoing streams having the same priority in a + /// round-robin fashion. When disabled, streams are scheduled in the order they are written to. + /// + /// Note that this only affects streams with the same priority. Higher priority streams always + /// take precedence over lower priority streams. + /// + /// Disabling fairness can reduce fragmentation and protocol overhead for workloads that use + /// many small streams. + pub fn send_fairness(mut self, value: bool) -> Self { + self.0.send_fairness(value); + self + } + + /// Maximum reordering in packet number space before FACK style loss detection considers a + /// packet lost. Should not be less than 3, per RFC5681. + pub fn packet_threshold(mut self, value: u32) -> Self { + self.0.packet_threshold(value); + self + } + + /// Maximum reordering in time space before time based loss detection considers a packet lost, + /// as a factor of RTT. + pub fn time_threshold(mut self, value: f32) -> Self { + self.0.time_threshold(value); + self + } + + /// The RTT used before an RTT sample is taken. + pub fn initial_rtt(mut self, value: Duration) -> Self { + self.0.initial_rtt(value); + self + } + + /// The initial value to be used as the maximum UDP payload size before running MTU discovery + /// (see [`QuicTransportConfigBuilder::mtu_discovery_config`]). + /// + /// Must be at least 1200, which is the default, and known to be safe for typical internet + /// applications. Larger values are more efficient, but increase the risk of packet loss due to + /// exceeding the network path's IP MTU. If the provided value is higher than what the network + /// path actually supports, packet loss will eventually trigger black hole detection and bring + /// it down to [`QuicTransportConfigBuilder::min_mtu`]. + pub fn initial_mtu(mut self, value: u16) -> Self { + self.0.initial_mtu(value); + self + } + + /// The maximum UDP payload size guaranteed to be supported by the network. + /// + /// Must be at least 1200, which is the default, and lower than or equal to + /// [`QuicTransportConfigBuilder::initial_mtu`]. + /// + /// Real-world MTUs can vary according to ISP, VPN, and properties of intermediate network links + /// outside of either endpoint's control. Extreme care should be used when raising this value + /// outside of private networks where these factors are fully controlled. If the provided value + /// is higher than what the network path actually supports, the result will be unpredictable and + /// catastrophic packet loss, without a possibility of repair. Prefer + /// [`QuicTransportConfigBuilder::initial_mtu`] together with + /// [`QuicTransportConfigBuilder::mtu_discovery_config`] to set a maximum UDP payload size that robustly + /// adapts to the network. + pub fn min_mtu(mut self, value: u16) -> Self { + self.0.min_mtu(value); + self + } + + /// Specifies the MTU discovery config (see [`MtuDiscoveryConfig`] for details). + /// + /// Enabled by default. + pub fn mtu_discovery_config(mut self, value: Option) -> Self { + self.0.mtu_discovery_config(value); + self + } + + /// Pad UDP datagrams carrying application data to current maximum UDP payload size. + /// + /// Disabled by default. UDP datagrams containing loss probes are exempt from padding. + /// + /// Enabling this helps mitigate traffic analysis by network observers, but it increases + /// bandwidth usage. Without this mitigation precise plain text size of application datagrams as + /// well as the total size of stream write bursts can be inferred by observers under certain + /// conditions. This analysis requires either an uncongested connection or application datagrams + /// too large to be coalesced. + pub fn pad_to_mtu(mut self, value: bool) -> Self { + self.0.pad_to_mtu(value); + self + } + + /// Specifies the ACK frequency config (see [`AckFrequencyConfig`] for details). + /// + /// The provided configuration will be ignored if the peer does not support the acknowledgement + /// frequency QUIC extension. + /// + /// Defaults to `None`, which disables controlling the peer's acknowledgement frequency. Even + /// if set to `None`, the local side still supports the acknowledgement frequency QUIC + /// extension and may use it in other ways. + pub fn ack_frequency_config(mut self, value: Option) -> Self { + self.0.ack_frequency_config(value); + self + } + + /// Number of consecutive PTOs after which network is considered to be experiencing persistent congestion. + pub fn persistent_congestion_threshold(mut self, value: u32) -> Self { + self.0.persistent_congestion_threshold(value); + self + } + + /// Period of inactivity before sending a keep-alive packet. + /// + /// Keep-alive packets prevent an inactive but otherwise healthy connection from timing + /// out. They are important to keep NAT bindings alive and firewalls open. + /// + /// The default is 5s, please be careful when modifying this as it may affect connection + /// stability. Must be set lower than the idle_timeout of both peers to be effective. + pub fn keep_alive_interval(mut self, value: Duration) -> Self { + self.0.keep_alive_interval(Some(value)); + self + } + + /// Maximum quantity of out-of-order crypto layer data to buffer. + pub fn crypto_buffer_size(mut self, value: usize) -> Self { + self.0.crypto_buffer_size(value); + self + } + + /// Whether the implementation is permitted to set the spin bit on this connection. + /// + /// This allows passive observers to easily judge the round trip time of a connection, which can + /// be useful for network administration but sacrifices a small amount of privacy. + pub fn allow_spin(mut self, value: bool) -> Self { + self.0.allow_spin(value); + self + } + + /// Maximum number of incoming application datagram bytes to buffer, or None to disable + /// incoming datagrams. + /// + /// The peer is forbidden to send single datagrams larger than this size. If the aggregate size + /// of all datagrams that have been received from the peer but not consumed by the application + /// exceeds this value, old datagrams are dropped until it is no longer exceeded. + pub fn datagram_receive_buffer_size(mut self, value: Option) -> Self { + self.0.datagram_receive_buffer_size(value); + self + } + + /// Maximum number of outgoing application datagram bytes to buffer. + /// + /// While datagrams are sent ASAP, it is possible for an application to generate data faster + /// than the link, or even the underlying hardware, can transmit them. This limits the amount of + /// memory that may be consumed in that case. When the send buffer is full and a new datagram is + /// sent, older datagrams are dropped until sufficient space is available. + pub fn datagram_send_buffer_size(mut self, value: usize) -> Self { + self.0.datagram_send_buffer_size(value); + self + } + + /// How to construct new `congestion::Controller`s. + /// + /// Typically the refcounted configuration of a `congestion::Controller`, + /// e.g. a `congestion::NewRenoConfig`. + /// + /// # Example + /// ``` + /// # use iroh::endpoint::QuicTransportConfig; use noq_proto::congestion; use std::sync::Arc; + /// let config = QuicTransportConfig::builder() + /// .congestion_controller_factory(Arc::new(congestion::NewRenoConfig::default())) + /// .build(); + /// ``` + pub fn congestion_controller_factory( + mut self, + factory: Arc, + ) -> Self { + self.0.congestion_controller_factory(factory); + self + } + + /// Whether to use "Generic Segmentation Offload" to accelerate transmits, when supported by the + /// environment. + /// + /// Defaults to `true`. + /// + /// GSO dramatically reduces CPU consumption when sending large numbers of packets with the same + /// headers, such as when transmitting bulk data on a connection. However, it is not supported + /// by all network interface drivers or packet inspection tools. `noq-udp` will attempt to + /// disable GSO automatically when unavailable, but this can lead to spurious packet loss at + /// startup, temporarily degrading performance. + pub fn enable_segmentation_offload(mut self, enabled: bool) -> Self { + self.0.enable_segmentation_offload(enabled); + self + } + + /// Whether to send observed address reports to peers. + /// + /// This will aid peers in inferring their reachable address, which in most NATd networks + /// will not be easily available to them. + pub fn send_observed_address_reports(mut self, enabled: bool) -> Self { + self.0.send_observed_address_reports(enabled); + self + } + + /// Whether to receive observed address reports from other peers. + /// + /// Peers with the address discovery extension enabled that are willing to provide observed + /// address reports will do so if this transport parameter is set. In general, observed address + /// reports cannot be trusted. This, however, can aid the current endpoint in inferring its + /// reachable address, which in most NATd networks will not be easily available. + pub fn receive_observed_address_reports(mut self, enabled: bool) -> Self { + self.0.receive_observed_address_reports(enabled); + self + } + + /// Enables the Multipath Extension for QUIC. + /// + /// Setting this to any nonzero value will enable the Multipath Extension for QUIC, + /// . + /// + /// The value provided specifies the number maximum number of paths this endpoint may open + /// concurrently when multipath is negotiated. For any path to be opened, the remote must + /// enable multipath as well. + /// + /// Note: this method will ignore values less than the recommended 13 and will log a warning. + pub fn max_concurrent_multipath_paths(mut self, max_concurrent: u32) -> Self { + if max_concurrent < MAX_MULTIPATH_PATHS + 1 { + warn!( + "QuicTransportConfig::max_concurrent_multipath_paths must be at minimum {}, ignoring user supplied value", + MAX_MULTIPATH_PATHS + 1 + ); + return self; + } + self.0.max_concurrent_multipath_paths(max_concurrent); + self + } + + /// Sets a default per-path maximum idle timeout. + /// + /// If the path is idle for this long the path will be abandoned. Bear in mind this will + /// interact with the [`QuicTransportConfigBuilder::max_idle_timeout`], if the last path is + /// abandoned the entire connection will be closed. + /// + /// Note: values higher than `PATH_MAX_IDLE_TIMEOUT` (15 seconds) are clamped and a warning is logged. + pub fn default_path_max_idle_timeout(mut self, timeout: Duration) -> Self { + if timeout > PATH_MAX_IDLE_TIMEOUT { + warn!( + "QuicTransportConfig::default_path_max_idle must be at most {:?}, clamping", + PATH_MAX_IDLE_TIMEOUT + ); + self.0 + .default_path_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)); + return self; + } + self.0.default_path_max_idle_timeout(Some(timeout)); + self + } + + /// Sets a default per-path keep alive interval. + /// + /// Note that this does not interact with the connection-wide + /// [`QuicTransportConfigBuilder::keep_alive_interval`]. This setting will keep this path active, + /// [`QuicTransportConfigBuilder::keep_alive_interval`] will keep the connection active, with no + /// control over which path is used for this. + /// + /// Note: this method will ignore values higher than the recommended 5 seconds and will log a warning. + pub fn default_path_keep_alive_interval(mut self, interval: Duration) -> Self { + if interval > HEARTBEAT_INTERVAL { + warn!( + "QuicTransportConfig::default_path_keep_alive must be at most {:?}, ignoring user supplied value", + HEARTBEAT_INTERVAL + ); + return self; + } + self.0.default_path_keep_alive_interval(Some(interval)); + self + } + + /// Sets the maximum number of nat traversal addresses this endpoint allows the remote to + /// advertise. + /// + /// Setting this to any nonzero value will enable Iroh's holepunching, loosely based in the Nat + /// Traversal Extension for QUIC, see + /// + /// + /// This implementation expects the multipath extension to be enabled as well. If not yet + /// enabled via [`Self::max_concurrent_multipath_paths`], a default value of + /// 8 will be used. + /// + /// Note: this method will ignore values less than the recommended 8 and will log a warning. + pub fn max_remote_nat_traversal_addresses(mut self, max_addresses: u8) -> Self { + if max_addresses < MAX_MULTIPATH_PATHS as u8 { + warn!( + "QuicTransportConfig::max_remote_nat_traversal_addresses must be at least {}, ignoring user supplied value", + MAX_MULTIPATH_PATHS + ); + return self; + } + self.0.max_remote_nat_traversal_addresses(max_addresses); + self + } + + /// Configures qlog capturing by setting a [`QlogFactory`]. + /// + /// This assigns a [`QlogFactory`] that produces qlog capture configurations for + /// individual connections. + #[cfg(feature = "qlog")] + pub fn qlog_factory(mut self, factory: Arc) -> Self { + self.0.qlog_factory(factory); + self + } + + /// Configures qlog capturing through the `QLOGDIR` environment variable. + /// + /// This uses [`QlogFileFactory::from_env`] to create a factory to write qlog traces + /// into the directory set through the `QLOGDIR` environment variable. + /// + /// If `QLOGDIR` is not set, no traces will be written. If `QLOGDIR` is set to a path + /// that does not exist, it will be created. + /// + /// The files will be prefixed with `prefix`. + #[cfg(feature = "qlog")] + pub fn qlog_from_env(mut self, prefix: &str) -> Self { + self.0.qlog_from_env(prefix); + self + } + + /// Configures qlog capturing into a directory. + /// + /// This uses [`QlogFileFactory`] to create a factory to write qlog traces into + /// the specified directory. The files will be prefixed with `prefix`. + #[cfg(feature = "qlog")] + pub fn qlog_from_path(mut self, path: impl AsRef, prefix: &str) -> Self { + self.0.qlog_from_path(path, prefix); + self + } +} + +/// A builder for a [`ServerConfig`]. +#[derive(Debug, Clone)] +pub struct ServerConfigBuilder { + inner: noq::ServerConfig, + transport: QuicTransportConfig, +} + +/// Parameters governing incoming connections +/// +/// Default values should be suitable for most internet applications. +/// +/// Use a [`ServerConfigBuilder`] to adjust the default values. +/// +/// To create a [`ServerConfig`] compatible with your [`Endpoint`] identity, use the [`Endpoint::create_server_config_builder`] method. +/// +/// [`Endpoint`]: crate::Endpoint +/// [`Endpoint::create_server_config_builder`]: crate::Endpoint::create_server_config_builder +// Note: used in `iroh::endpoint::connection::Incoming::accept_with` +// This is new-typed since `noq::ServerConfig` takes a `TransportConfig`, which we new-type as a `QuicTransportConfig` +#[derive(Debug, Clone)] +pub struct ServerConfig(Arc); + +impl ServerConfig { + pub(crate) fn to_inner_arc(&self) -> Arc { + self.0.clone() + } +} + +impl ServerConfigBuilder { + /// Build a [`ServerConfig`] from a [`ServerConfigBuilder`]. + pub fn build(self) -> ServerConfig { + ServerConfig(Arc::new(self.inner)) + } + + pub(crate) fn new(inner: noq::ServerConfig, transport: QuicTransportConfig) -> Self { + Self { inner, transport } + } + + /// Sets a custom [`QuicTransportConfig`]. + pub fn set_transport_config(mut self, transport: QuicTransportConfig) -> Self { + self.inner.transport_config(transport.to_inner_arc()); + self.transport = transport; + self + } + + /// Sets a custom [`ValidationTokenConfig`]. + pub fn set_validation_token_config(mut self, validation_token: ValidationTokenConfig) -> Self { + self.inner.validation_token_config(validation_token); + self + } + + /// Private key used to authenticate data included in handshake tokens + pub fn set_token_key(mut self, value: Arc) -> Self { + self.inner.token_key(value); + self + } + + /// Duration after a retry token was issued for which it's considered valid + /// + /// Defaults to 15 seconds. + pub fn set_retry_token_lifetime(mut self, value: Duration) -> Self { + self.inner.retry_token_lifetime(value); + self + } + + /// Maximum number of [`Incoming`] to allow to exist at a time. + /// + /// An [`Incoming`] comes into existence when an incoming connection attempt + /// is received and stops existing when the application either accepts it or otherwise disposes + /// of it. While this limit is reached, new incoming connection attempts are immediately + /// refused. Larger values have greater worst-case memory consumption, but accommodate greater + /// application latency in handling incoming connection attempts. + /// + /// The default value is set to 65536. With a typical Ethernet MTU of 1500 bytes, this limits + /// memory consumption from this to under 100 MiB--a generous amount that still prevents memory + /// exhaustion in most contexts. + /// + /// [`Incoming`]: crate::endpoint::Incoming + pub fn set_max_incoming(mut self, max_incoming: usize) -> Self { + self.inner.max_incoming(max_incoming); + self + } + + /// Maximum number of received bytes to buffer for each [`Incoming`]. + /// + /// An [`Incoming`] comes into existence when an incoming connection attempt + /// is received and stops existing when the application either accepts it or otherwise disposes + /// of it. This limit governs only packets received within that period, and does not include + /// the first packet. Packets received in excess of this limit are dropped, which may cause + /// 0-RTT or handshake data to have to be retransmitted. + /// + /// The default value is set to 10 MiB--an amount such that in most situations a client would + /// not transmit that much 0-RTT data faster than the server handles the corresponding + /// [`Incoming`]. + /// + /// [`Incoming`]: crate::endpoint::Incoming + pub fn set_incoming_buffer_size(mut self, incoming_buffer_size: u64) -> Self { + self.inner.incoming_buffer_size(incoming_buffer_size); + self + } + + /// Maximum number of received bytes to buffer for all [`Incoming`] + /// collectively. + /// + /// An [`Incoming`] comes into existence when an incoming connection attempt + /// is received and stops existing when the application either accepts it or otherwise disposes + /// of it. This limit governs only packets received within that period, and does not include + /// the first packet. Packets received in excess of this limit are dropped, which may cause + /// 0-RTT or handshake data to have to be retransmitted. + /// + /// The default value is set to 100 MiB--a generous amount that still prevents memory + /// exhaustion in most contexts. + /// + /// [`Incoming`]: crate::endpoint::Incoming + pub fn set_incoming_buffer_size_total(mut self, incoming_buffer_size_total: u64) -> Self { + self.inner + .incoming_buffer_size_total(incoming_buffer_size_total); + self + } + + /// Object to get current [`SystemTime`]. + /// + /// This exists to allow system time to be mocked in tests, or wherever else desired. + /// + /// Defaults to [`noq::StdSystemTime`], which simply calls [`SystemTime::now()`](std::time::SystemTime::now). + /// + /// [`SystemTime`]: std::time::SystemTime + pub fn set_time_source(mut self, time_source: Arc) -> Self { + self.inner.time_source(time_source); + self + } +} diff --git a/vendor/iroh/src/lib.rs b/vendor/iroh/src/lib.rs new file mode 100644 index 0000000..c97449d --- /dev/null +++ b/vendor/iroh/src/lib.rs @@ -0,0 +1,306 @@ +//! Peer-to-peer QUIC connections. +//! +//! iroh is a library to establish direct connectivity between peers. It exposes an +//! interface to [QUIC] connections and streams to the user, while implementing direct +//! connectivity using [hole punching] complemented by relay servers under the hood. +//! +//! An iroh endpoint is created and controlled by the [`Endpoint`], e.g. connecting to +//! another endpoint: +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # { +//! # use iroh::{Endpoint, EndpointAddr, endpoint::presets}; +//! # use n0_error::{StackResultExt, StdResultExt}; +//! # async fn wrapper() -> n0_error::Result<()> { +//! let addr: EndpointAddr = todo!(); +//! let ep = Endpoint::bind(presets::N0).await?; +//! let conn = ep.connect(addr, b"my-alpn").await?; +//! let mut send_stream = conn.open_uni().await.std_context("unable to open uni")?; +//! send_stream +//! .write_all(b"msg") +//! .await +//! .std_context("unable to write all")?; +//! # Ok(()) +//! # } +//! # } +//! ``` +//! +//! The other endpoint can accept incoming connections using the [`Endpoint`] as well: +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # { +//! # use iroh::{Endpoint, EndpointAddr, endpoint::presets}; +//! # use n0_error::{StackResultExt, StdResultExt}; +//! # async fn wrapper() -> n0_error::Result<()> { +//! let ep = Endpoint::builder(presets::N0) +//! .alpns(vec![b"my-alpn".to_vec()]) +//! .bind() +//! .await?; +//! let conn = ep +//! .accept() +//! .await +//! .context("accept error")? +//! .await +//! .std_context("connecting error")?; +//! let mut recv_stream = conn.accept_uni().await.std_context("unable to open uni")?; +//! let mut buf = [0u8; 3]; +//! recv_stream +//! .read_exact(&mut buf) +//! .await +//! .std_context("unable to read")?; +//! # Ok(()) +//! # } +//! # } +//! ``` +//! +//! Of course you can also use [bi-directional streams] or any other features from QUIC. +//! +//! For more elaborate examples, see [below](#examples) or the examples directory in +//! the source repository. +//! +//! +//! # Connection Establishment +//! +//! An iroh connection between two iroh endpoints is usually established with the help +//! of a Relay server. When creating the [`Endpoint`] it connects to the closest Relay +//! server and designates this as the *home relay*. When other endpoints want to connect they +//! first establish connection via this home relay. As soon as connection between the two +//! endpoints is established they will attempt to create a direct connection, using [hole +//! punching] if needed. Once the direct connection is established the relay server is no +//! longer involved in the connection. +//! +//! If one of the iroh endpoints can be reached directly, connectivity can also be +//! established without involving a Relay server. This is done by using the endpoint's +//! listening addresses in the connection establishment instead of the [`RelayUrl`] which +//! is used to identify a Relay server. Of course it is also possible to use both a +//! [`RelayUrl`] and direct addresses at the same time to connect. +//! +//! +//! # Encryption +//! +//! The connection is encrypted using TLS, like standard QUIC connections. Unlike standard +//! QUIC there is no client, server or server TLS key and certificate chain. Instead each iroh endpoint has a +//! unique [`SecretKey`] used to authenticate and encrypt the connection. When an iroh +//! endpoint connects, it uses the corresponding [`PublicKey`] to ensure the connection is only +//! established with the intended peer. +//! +//! Since the [`PublicKey`] is also used to identify the iroh endpoint it is also known as +//! the [`EndpointId`]. As encryption is an integral part of TLS as used in QUIC this +//! [`EndpointId`] is always a required parameter to establish a connection. +//! +//! When accepting connections the peer's [`EndpointId`] is authenticated. However it is up to +//! the application to decide if a particular peer is allowed to connect or not. +//! +//! +//! # Relay Servers +//! +//! Relay servers exist to ensure all iroh endpoints are always reachable. They accept +//! **encrypted** traffic for iroh endpoints which are connected to them, forwarding it to +//! the correct destination based on the [`EndpointId`] only. Since endpoints only send encrypted +//! traffic, the Relay servers can not decode any traffic for other iroh endpoints and only +//! forward it. +//! +//! The connections to the Relay server are initiated as normal HTTP 1.1 connections using +//! TLS. Once connected the transport is upgraded to a plain TCP connection using a custom +//! protocol. All further data is then sent using this custom relaying protocol. Usually +//! soon after the connection is established via the Relay it will migrate to a direct +//! connection. However if this is not possible the connection will keep flowing over the +//! relay server as a fallback. +//! +//! Additionally to providing reliable connectivity between iroh endpoints, Relay servers +//! provide some functions to assist in [hole punching]. They have various services to help +//! endpoints understand their own network situation. This includes offering a [QAD] server, +//! but also a few HTTP extra endpoints as well as responding to ICMP echo requests. +//! +//! By default the [number 0] relay servers are used, see [`RelayMode::Default`]. +//! +//! +//! # Connections and Streams +//! +//! An iroh endpoint is managed using the [`Endpoint`] and this is used to create or accept +//! connections to other endpoints. To establish a connection to an iroh endpoint you need to +//! know three pieces of information: +//! +//! - The [`EndpointId`] of the peer to connect to. +//! - Some addressing information: +//! - Usually the [`RelayUrl`] identifying the Relay server. +//! - Sometimes, or usually additionally, any direct addresses which might be known. +//! - The QUIC/TLS Application-Layer Protocol Negotiation, or [ALPN], name to use. +//! +//! The ALPN is used by both sides to agree on which application-specific protocol will be +//! used over the resulting QUIC connection. These can be protocols like `h3` used for +//! [HTTP/3][HTTP3], but more commonly will be a custom identifier for the application. +//! +//! Once connected the API exposes QUIC streams. These are very cheap to create so can be +//! created at any time and can be used to create very many short-lived stream as well as +//! long-lived streams. There are two stream types to choose from: +//! +//! - **Uni-directional** which only allows the peer which initiated the stream to send +//! data. +//! +//! - **Bi-directional** which allows both peers to send and receive data. However, the +//! initiator of this stream has to send data before the peer will be aware of this +//! stream. +//! +//! Additionally to being extremely light-weight, streams can be interleaved and will not +//! block each other. Allowing many streams to co-exist, regardless of how long they last. +//! +//!
+//! +//! To keep streams cheap, they are lazily created on the network: only once a sender starts +//! sending data on the stream will the receiver become aware of a stream. This means only +//! calling [`Connection::open_bi`] is not sufficient for the corresponding call to +//! [`Connection::accept_bi`] to return. The sender **must** send data on the stream before +//! the receiver's [`Connection::accept_bi`] call will return. +//! +//!
+//! +//! ## Address Lookup +//! +//! The need to know the [`RelayUrl`] *or* some direct addresses in addition to the +//! [`EndpointId`] to connect to an iroh endpoint can be an obstacle. To address this, the +//! [`endpoint::Builder`] allows you to configure an [`address_lookup`] service. +//! +//! The [`address_lookup::DnsAddressLookup`] service is an address lookup service which will publish the [`RelayUrl`] +//! and direct addresses to a service publishing those as DNS records. To connect it looks +//! up the [`EndpointId`] in the DNS system to find the addressing details. This enables +//! connecting using only the [`EndpointId`] which is often more convenient and resilient. +//! +//! See [the Address Lookup module] for more details. +//! +//! +//! # Examples +//! +//! The central struct is the [`Endpoint`], which allows you to connect to other endpoints: +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # { +//! use iroh::{Endpoint, EndpointAddr, endpoint::presets}; +//! use n0_error::{Result, StackResultExt, StdResultExt}; +//! +//! async fn connect(addr: EndpointAddr) -> Result<()> { +//! // The Endpoint is the central object that manages an iroh node. +//! let ep = Endpoint::bind(presets::N0).await?; +//! +//! // Establish a QUIC connection, open a bi-directional stream, exchange messages. +//! let conn = ep.connect(addr, b"hello-world").await?; +//! let (mut send_stream, mut recv_stream) = conn.open_bi().await.std_context("open bi")?; +//! send_stream.write_all(b"hello").await.std_context("write")?; +//! send_stream.finish().std_context("finish")?; +//! // `read_to_end` waits until the peer finishes or closes its send side. +//! let _msg = recv_stream.read_to_end(10).await.std_context("read")?; +//! +//! // Gracefully close the connection and endpoint. +//! conn.close(1u8.into(), b"done"); +//! ep.close().await; +//! println!("Client closed"); +//! Ok(()) +//! } +//! # } +//! ``` +//! +//! Every [`Endpoint`] can also accept connections: +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # { +//! use iroh::{Endpoint, EndpointAddr, endpoint::presets}; +//! use n0_error::{Result, StackResultExt, StdResultExt}; +//! use n0_future::StreamExt; +//! +//! async fn accept() -> Result<()> { +//! // To accept connections at least one ALPN must be configured. +//! let ep = Endpoint::builder(presets::N0) +//! .alpns(vec![b"hello-world".to_vec()]) +//! .bind() +//! .await?; +//! +//! // Accept a QUIC connection, accept a bi-directional stream, exchange messages. +//! let conn = ep +//! .accept() +//! .await +//! .context("no incoming connection")? +//! .await +//! .context("accept conn")?; +//! let (mut send_stream, mut recv_stream) = +//! conn.accept_bi().await.std_context("accept stream")?; +//! // `read_to_end` terminates once the client calls `finish` on its send stream. +//! let _msg = recv_stream.read_to_end(10).await.std_context("read")?; +//! send_stream.write_all(b"world").await.std_context("write")?; +//! send_stream.finish().std_context("finish")?; +//! +//! // Wait for the client to close the connection and gracefully close the endpoint. +//! conn.closed().await; +//! ep.close().await; +//! Ok(()) +//! } +//! # } +//! ``` +//! +//! Please see the examples directory for more nuanced examples. +//! +//! +//! [QUIC]: https://quicwg.org +//! [bi-directional streams]: crate::endpoint::Connection::open_bi +//! [hole punching]: https://en.wikipedia.org/wiki/Hole_punching_(networking) +//! [socket addresses]: https://doc.rust-lang.org/stable/std/net/enum.SocketAddr.html +//! [QAD]: https://www.ietf.org/archive/id/draft-ietf-quic-address-discovery-00.html +//! [ALPN]: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation +//! [HTTP3]: https://en.wikipedia.org/wiki/HTTP/3 +//! [`SecretKey`]: crate::SecretKey +//! [`PublicKey`]: crate::PublicKey +//! [`RelayUrl`]: crate::RelayUrl +//! [`address_lookup`]: crate::endpoint::Builder::address_lookup +//! [`address_lookup::DnsAddressLookup`]: crate::address_lookup::DnsAddressLookup +//! [number 0]: https://n0.computer +//! [`RelayMode::Default`]: crate::RelayMode::Default +//! [the Address Lookup module]: crate::address_lookup +//! [`Connection::open_bi`]: crate::endpoint::Connection::open_bi +//! [`Connection::accept_bi`]: crate::endpoint::Connection::accept_bi + +#![recursion_limit = "256"] +#![deny(missing_docs, rustdoc::broken_intra_doc_links, unreachable_pub)] +#![cfg_attr(wasm_browser, allow(unused))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(iroh_docsrs, feature(doc_cfg))] + +mod socket; +pub mod tls; + +pub(crate) mod portmapper; +pub(crate) mod runtime; +pub(crate) mod util; + +pub mod address_lookup; +pub mod defaults; +pub mod endpoint; +pub mod metrics; +mod net_report; +pub mod protocol; + +pub use endpoint::{Endpoint, RelayMode}; +pub use iroh_base::{ + EndpointAddr, EndpointId, KeyParsingError, PublicKey, RelayUrl, RelayUrlParseError, SecretKey, + Signature, SignatureError, TransportAddr, +}; +#[cfg(not(wasm_browser))] +pub use iroh_dns::dns; +pub use iroh_dns::endpoint_info; +pub use iroh_relay::{RelayConfig, RelayMap}; +pub use n0_watcher::Watcher; +pub use net_report::{NetReportConfig, TIMEOUT as NET_REPORT_TIMEOUT}; + +#[cfg(feature = "unstable-net-report")] +pub mod unstable_net_report { + //! Exports of net report types reachable via [`crate::endpoint::Endpoint::net_report`]. + /// This API is unstable and gated behind the `unstable-net-report` feature. + /// It is not covered by semantic versioning guarantees and may change in any release + /// without a major version bump. + pub use crate::net_report::{Probe, RelayLatencies, Report as NetReport}; +} + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/vendor/iroh/src/metrics.rs b/vendor/iroh/src/metrics.rs new file mode 100644 index 0000000..2b3872d --- /dev/null +++ b/vendor/iroh/src/metrics.rs @@ -0,0 +1,57 @@ +//! Co-locating all of the iroh metrics structs +use std::sync::Arc; + +use iroh_metrics::MetricsGroupSet; +#[cfg(feature = "test-utils")] +pub use iroh_relay::server::Metrics as RelayMetrics; +use serde::{Deserialize, Serialize}; + +pub use crate::{ + address_lookup::Metrics as AddressLookupMetrics, net_report::Metrics as NetReportMetrics, + socket::Metrics as SocketMetrics, +}; + +/// Metrics collected by an [`crate::endpoint::Endpoint`]. +/// +/// See [`crate::endpoint::Endpoint::metrics`] for details. +#[derive(Default, Debug, Clone, Serialize, Deserialize, MetricsGroupSet)] +#[metrics(name = "endpoint")] +#[non_exhaustive] +pub struct EndpointMetrics { + /// Metrics collected by the endpoint's socket. + pub socket: Arc, + /// Metrics collected by net reports. + pub net_report: Arc, + /// Metrics collected by address lookup. + pub address_lookup: Arc, +} + +#[cfg(test)] +mod tests { + use super::EndpointMetrics; + #[test] + fn test_serde() { + use crate::address_lookup::ServiceLabels; + + let metrics = EndpointMetrics::default(); + metrics.socket.actor_link_change.inc(); + metrics.net_report.reports.inc_by(10); + metrics + .address_lookup + .service_results + .get_or_create(&ServiceLabels::new("dns")) + .inc_by(3); + let encoded = postcard::to_stdvec(&metrics).unwrap(); + let decoded: EndpointMetrics = postcard::from_bytes(&encoded).unwrap(); + assert_eq!(decoded.socket.actor_link_change.get(), 1); + assert_eq!(decoded.net_report.reports.get(), 10); + assert_eq!( + decoded + .address_lookup + .service_results + .get(&ServiceLabels::new("dns")) + .map(|counter| counter.get()), + Some(3) + ); + } +} diff --git a/vendor/iroh/src/net_report.rs b/vendor/iroh/src/net_report.rs new file mode 100644 index 0000000..63cd500 --- /dev/null +++ b/vendor/iroh/src/net_report.rs @@ -0,0 +1,1255 @@ +//! Checks the network conditions from the current host. +//! +//! NetReport is responsible for finding out the network conditions of the current host, like +//! whether it is connected to the internet via IPv4 and/or IPv6, what the NAT situation is +//! etc and reachability to the configured relays. +// Based on + +#![cfg_attr(wasm_browser, allow(unused))] + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, + net::SocketAddr, + sync::Arc, +}; + +use defaults::timeouts::PROBES_TIMEOUT; +use iroh_base::RelayUrl; +#[cfg(not(wasm_browser))] +use iroh_dns::dns::DnsResolver; +#[cfg(not(wasm_browser))] +use iroh_relay::{RelayConfig, quic::QuicClient}; +use iroh_relay::{ + RelayMap, + quic::{QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON}, +}; +use n0_error::e; +#[cfg(not(wasm_browser))] +use n0_error::stack_error; +#[cfg(not(wasm_browser))] +use n0_future::task; +use n0_future::{ + StreamExt, + task::AbortOnDropHandle, + time::{self, Duration, Instant}, +}; +use n0_watcher::{Watchable, Watcher}; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; +use tracing::{debug, trace, warn}; + +use self::reportgen::{ProbeFinished, ProbeReport}; +#[cfg(not(wasm_browser))] +use self::reportgen::{QadProbeReport, SocketState}; +#[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] +pub use self::{ + // exported primarily for use in documentation + defaults::timeouts::TIMEOUT, + metrics::Metrics, + probes::Probe, + report::{RelayLatencies, Report}, +}; +pub(crate) use self::{ + options::Options, + reportgen::{IfStateDetails, QuicConfig}, +}; + +mod defaults; +mod metrics; +mod options; +mod probes; +mod report; +mod reportgen; + +#[cfg(not(wasm_browser))] +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +enum QadProbeError { + #[error("Failed to resolve relay address")] + GetRelayAddr { + source: self::reportgen::GetRelayAddrError, + }, + #[error("Missing host in relay URL")] + MissingHost, + #[error("QUIC connection failed")] + Quic { source: iroh_relay::quic::Error }, + #[error("Receiver dropped")] + ReceiverDropped, +} + +/// Configuration for the net report component. +/// +/// Controls which probes and checks are performed when generating network reports. +/// All options default to `true`. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct NetReportConfig { + /// Run HTTPS latency probes against relay servers. + /// + /// HTTPS latency probes perform an empty HTTPS GET request to each configured + /// relay server and measure latency. + /// + /// They are performed in addition to the QUIC address discovery (QAD) probes. + /// In networks that do not allow QUIC traffic, they are the only way to detect + /// relay latencies and thus the preferred relay. + /// + /// Disabling them is harmless on networks that do allow QUIC traffic, but will + /// completely prevent finding the home relay on networks that do block QUIC. + pub https_probes: bool, + + /// Check for captive portals when generating the first report. + /// + /// This is done by accessing a well-known URL that is available on each relay + /// server, `/generate_204`. If a GET request to this URL returns anything else + /// but a 204 No Content response, we assume we are behind a captive portal. + /// + /// When we have detected that we are behind a captive portal, we try to contact + /// the relay servers more frequently in case the captive portal status changes. + pub captive_portal_check: bool, +} + +impl NetReportConfig { + /// Creates a minimal configuration that disables all optional probes and checks. + pub fn minimal() -> Self { + Self { + https_probes: false, + captive_portal_check: false, + } + } +} + +impl Default for NetReportConfig { + fn default() -> Self { + Self { + https_probes: true, + captive_portal_check: true, + } + } +} + +const FULL_REPORT_INTERVAL: Duration = Duration::from_secs(5 * 60); +const ENOUGH_ENDPOINTS: usize = 3; + +/// Client to run net_reports. +#[derive(Debug)] +pub(crate) struct Client { + #[cfg(not(wasm_browser))] + socket_state: SocketState, + metrics: Arc, + probes: BTreeSet, + relay_map: RelayMap, + #[cfg(not(wasm_browser))] + qad_conns: QadConns, + #[cfg(not(wasm_browser))] + tls_config: rustls::ClientConfig, + /// Whether to check for captive portals. + captive_portal_check: bool, + /// A collection of previously generated reports. + /// + /// Sometimes it is useful to look at past reports to decide what to do. + reports: Reports, +} + +#[cfg(not(wasm_browser))] +#[derive(Debug, Default)] +struct QadConns { + v4: Option<(RelayUrl, QadConn)>, + v6: Option<(RelayUrl, QadConn)>, +} + +#[cfg(not(wasm_browser))] +impl QadConns { + fn clear(&mut self) { + if let Some((_, conn)) = self.v4.take() { + conn.conn + .close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON); + } + if let Some((_, conn)) = self.v6.take() { + conn.conn + .close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON); + } + } + + fn current_v4(&self) -> Option { + if let Some((_, ref conn)) = self.v4 + && let Some(mut r) = conn.observer.get() + { + // grab latest rtt + + use noq_proto::PathId; + if let Some(latency) = conn.conn.rtt(PathId::ZERO) { + r.latency = latency; + } + return Some(ProbeReport::QadIpv4(r)); + } + None + } + + fn current_v6(&self) -> Option { + if let Some((_, ref conn)) = self.v6 + && let Some(mut r) = conn.observer.get() + { + // grab latest rtt + + use noq_proto::PathId; + if let Some(latency) = conn.conn.rtt(PathId::ZERO) { + r.latency = latency; + } + return Some(ProbeReport::QadIpv6(r)); + } + None + } + + fn watch_v4(&self) -> impl n0_future::Stream> + Unpin + use<> { + let watcher = self.v4.as_ref().map(|(_url, conn)| conn.observer.watch()); + + if let Some(watcher) = watcher { + watcher.stream_updates_only().boxed() + } else { + n0_future::stream::empty().boxed() + } + } + + fn watch_v6(&self) -> impl n0_future::Stream> + Unpin + use<> { + let watcher = self.v6.as_ref().map(|(_url, conn)| conn.observer.watch()); + if let Some(watcher) = watcher { + watcher.stream_updates_only().boxed() + } else { + n0_future::stream::empty().boxed() + } + } +} + +#[cfg(not(wasm_browser))] +#[derive(Debug)] +struct QadConn { + conn: noq::Connection, + observer: Watchable>, + _handle: AbortOnDropHandle>, +} + +#[derive(Debug)] +struct Reports { + /// Do a full relay scan, even if last is `Some`. + next_full: bool, + /// Some previous reports. + prev: BTreeMap, + /// Most recent report. + last: Option, + /// Time of last full (non-incremental) report. + last_full: Instant, +} + +impl Default for Reports { + fn default() -> Self { + Self { + next_full: true, + prev: Default::default(), + last: Default::default(), + last_full: Instant::now(), + } + } +} + +impl Client { + /// Creates a new net_report client. + pub(crate) fn new( + #[cfg(not(wasm_browser))] dns_resolver: DnsResolver, + relay_map: RelayMap, + opts: Options, + metrics: Arc, + ) -> Self { + let probes = opts.as_protocols(); + + #[cfg(not(wasm_browser))] + let quic_client = opts + .quic_config + .map(|c| iroh_relay::quic::QuicClient::new(c.ep, c.client_config)); + + #[cfg(not(wasm_browser))] + let socket_state = SocketState { + quic_client, + dns_resolver, + proxy_url: opts.proxy_url, + }; + + Client { + #[cfg(not(wasm_browser))] + socket_state, + metrics, + reports: Reports::default(), + probes, + relay_map, + #[cfg(not(wasm_browser))] + qad_conns: QadConns::default(), + #[cfg(not(wasm_browser))] + tls_config: opts.tls_config, + captive_portal_check: opts.user_config.captive_portal_check, + } + } + + /// Generates a [`Report`]. + /// + /// Look at [`Options`] for the different configuration options. + pub(crate) async fn get_report( + &mut self, + if_state: IfStateDetails, + is_major: bool, + shutdown_token: CancellationToken, + ) -> Report { + let now = Instant::now(); + + let mut do_full = is_major + || self.reports.next_full + || now.duration_since(self.reports.last_full) > FULL_REPORT_INTERVAL; + + debug!(%do_full, "net_report starting"); + + // If the last report had a captive portal and reported no UDP access, + // it's possible that we didn't get a useful net_report due to the + // captive portal blocking us. If so, make this report a full (non-incremental) one. + if !do_full + && let Some(ref last) = self.reports.last + && !last.has_udp() + && last.captive_portal == Some(true) + { + do_full = true; + } + if do_full { + self.reports.last = None; // causes ProbePlan::new below to do a full (initial) plan + self.reports.next_full = false; + self.reports.last_full = now; + self.metrics.reports_full.inc(); + } + self.metrics.reports.inc(); + + let num_relays = self.relay_map.len(); + let enough_relays = std::cmp::min(num_relays, ENOUGH_ENDPOINTS); + #[cfg(wasm_browser)] + let if_state = IfStateDetails::default(); + #[cfg(not(wasm_browser))] + let if_state = IfStateDetails { + have_v4: if_state.have_v4, + have_v6: if_state.have_v6, + }; + + let mut report = Report::default(); + + // Start the reportgen client to start any needed probes + let (actor, mut probe_rx) = reportgen::Client::new( + self.reports.last.clone(), + self.relay_map.clone(), + self.probes.clone(), + self.captive_portal_check, + if_state.clone(), + shutdown_token.child_token(), + #[cfg(not(wasm_browser))] + self.socket_state.clone(), + #[cfg(not(wasm_browser))] + self.tls_config.clone(), + ); + + #[cfg(not(wasm_browser))] + let reports = self + .spawn_qad_probes( + &if_state, + enough_relays, + do_full, + shutdown_token.child_token(), + ) + .await; + + #[cfg(not(wasm_browser))] + for r in reports { + report.update(&r); + } + + if self.have_enough_reports(&if_state, do_full, num_relays, &report) { + // check if we already have enough probes immediately after QAD returns + trace!("have enough probe reports, aborting further probes"); + // shuts down the probes + drop(actor); + } else { + #[cfg(not(wasm_browser))] + let mut qad_v4_stream = self.qad_conns.watch_v4(); + #[cfg(wasm_browser)] + let mut qad_v4_stream = n0_future::stream::empty::>(); + #[cfg(not(wasm_browser))] + let mut qad_v6_stream = self.qad_conns.watch_v6(); + #[cfg(wasm_browser)] + let mut qad_v6_stream = n0_future::stream::empty::>(); + + loop { + tokio::select! { + biased; + + Some(Some(r)) = qad_v4_stream.next() => { + #[cfg(not(wasm_browser))] + { + trace!(?r, "new report from QAD V4"); + report.update(&ProbeReport::QadIpv4(r)); + } + } + + Some(Some(r)) = qad_v6_stream.next() => { + #[cfg(not(wasm_browser))] + { + trace!(?r, "new report from QAD V6"); + report.update(&ProbeReport::QadIpv6(r)); + } + } + + maybe_probe = probe_rx.recv() => { + let Some(probe_res) = maybe_probe else { + break; + }; + match probe_res { + ProbeFinished::Regular(probe) => match probe { + Ok(probe) => { + report.update(&probe); + if self.have_enough_reports(&if_state, do_full, num_relays, &report) { + trace!("have enough probe reports, aborting further probes"); + // shuts down the probes + drop(actor); + break; + } + } + Err(err) => { + trace!("probe failed: {:?}", err); + } + }, + #[cfg(not(wasm_browser))] + ProbeFinished::CaptivePortal(portal) => { + report.captive_portal = portal; + } + } + } + } + } + } + self.add_report_history_and_set_preferred_relay(&mut report); + debug!( + ?report, + duration = ?now.elapsed(), + "net_report generated", + ); + + report + } + + #[cfg(not(wasm_browser))] + async fn spawn_qad_probes( + &mut self, + if_state: &IfStateDetails, + enough_relays: usize, + do_full: bool, + shutdown_token: CancellationToken, + ) -> Vec { + use tracing::{Instrument, info_span}; + + let Some(ref quic_client) = self.socket_state.quic_client else { + return Vec::new(); + }; + + if do_full { + // clear out existing connections if we are doing a full reset + self.qad_conns.clear(); + } + + if let Some((url, conn)) = &self.qad_conns.v4 { + // verify conn is still around + if let Some(reason) = conn.conn.close_reason() { + trace!(?url, "QAD v4 conn closed: {}", reason); + self.qad_conns.v4.take(); + } + } + if let Some((url, conn)) = &self.qad_conns.v6 { + // verify conn is still around + if let Some(reason) = conn.conn.close_reason() { + trace!(?url, "QAD v6 conn closed: {}", reason); + self.qad_conns.v6.take(); + } + } + + let v4_report = self.qad_conns.current_v4(); + let v6_report = self.qad_conns.current_v6(); + let needs_v4_probe = v4_report.is_none(); + let needs_v6_probe = v6_report.is_some() != if_state.have_v6; + + let mut reports = Vec::new(); + + if let Some(report) = v4_report { + reports.push(report); + } + if let Some(report) = v6_report { + reports.push(report); + } + + if !needs_v4_probe && !needs_v6_probe { + return reports; + } + + trace!("spawning QAD probes"); + + // TODO: randomize choice? + const MAX_RELAYS: usize = 5; + + let mut v4_buf = JoinSet::new(); + let cancel_v4 = shutdown_token.child_token(); + let mut v6_buf = JoinSet::new(); + let cancel_v6 = shutdown_token.child_token(); + + let relays = self.relay_map.relays::>(); + for relay in relays.into_iter().take(MAX_RELAYS) { + if if_state.have_v4 && needs_v4_probe { + trace!(?relay.url, "v4 QAD probe starting"); + let relay = relay.clone(); + let dns_resolver = self.socket_state.dns_resolver.clone(); + let quic_client = quic_client.clone(); + let relay_url = relay.url.clone(); + let inner_token = cancel_v4.child_token(); + v4_buf.spawn( + cancel_v4 + .child_token() + .run_until_cancelled_owned(time::timeout( + PROBES_TIMEOUT, + run_probe_v4(relay, quic_client, dns_resolver, inner_token), + )) + .instrument(info_span!("QADv4", %relay_url)), + ); + } + if if_state.have_v6 && needs_v6_probe { + trace!(?relay.url, "v6 QAD probe starting"); + let relay = relay.clone(); + let dns_resolver = self.socket_state.dns_resolver.clone(); + let quic_client = quic_client.clone(); + let relay_url = relay.url.clone(); + let inner_token = cancel_v6.child_token(); + v6_buf.spawn( + cancel_v6 + .child_token() + .run_until_cancelled_owned(time::timeout( + PROBES_TIMEOUT, + run_probe_v6(relay, quic_client, dns_resolver, inner_token), + )) + .instrument(info_span!("QADv6", %relay_url)), + ); + } + } + + // We set _pending to true if at least one report was started for each category. + // If we did not start any report for either category, _pending is set to false right away + // (it "completed" in the sense that nothing will ever run). If we did start at least one report, + // _pending is set to true, and will be set to false further down once the first task + // completed. + let mut ipv4_pending = !v4_buf.is_empty(); + let mut ipv6_pending = !v6_buf.is_empty(); + + while !v4_buf.is_empty() || !v6_buf.is_empty() { + // We early-abort the tasks once we have at least `enough_relays` reports, + // and at least one ipv4 and one ipv6 report completed (if they were started, see comment above). + + if reports.len() >= enough_relays && !ipv4_pending && !ipv6_pending { + debug!("enough probes: {}", reports.len()); + cancel_v4.cancel(); + cancel_v6.cancel(); + break; + } + + tokio::select! { + biased; + + _ = shutdown_token.cancelled() => { + trace!("qad report cancelled"); + break; + } + + val = v4_buf.join_next(), if !v4_buf.is_empty() => { + let span = info_span!("QADv4"); + let _guard = span.enter(); + ipv4_pending = false; + match val { + Some(Ok(Some(Ok(res)))) => { + match res { + Ok((r, conn)) => { + debug!(?r, "probe report"); + let url = r.relay.clone(); + reports.push(ProbeReport::QadIpv4(r)); + if self.qad_conns.v4.is_none() { + self.qad_conns.v4.replace((url, conn)); + } else { + conn.conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON); + } + } + Err(err) => { + debug!("probe failed: {err:#}"); + } + } + } + Some(Err(err)) => { + if err.is_panic() { + panic!("probe panicked: {err:#}"); + } + warn!("probe failed: {err:#}"); + } + Some(Ok(None)) => { + debug!("probe canceled"); + } + Some(Ok(Some(Err(time::Elapsed { .. })))) => { + debug!("probe timed out"); + } + None => { + trace!("report canceled"); + } + } + } + val = v6_buf.join_next(), if !v6_buf.is_empty() => { + let span = info_span!("QADv6"); + let _guard = span.enter(); + ipv6_pending = false; + match val { + Some(Ok(Some(Ok(res)))) => { + match res { + Ok((r, conn)) => { + debug!(?r, "probe report"); + let url = r.relay.clone(); + reports.push(ProbeReport::QadIpv6(r)); + if self.qad_conns.v6.is_none() { + self.qad_conns.v6.replace((url, conn)); + } else { + conn.conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON); + } + } + Err(err) => { + debug!("probe failed: {err:#}"); + } + } + } + Some(Err(err)) => { + if err.is_panic() { + panic!("probe panicked: {err:#}"); + } + warn!("probe failed: {err:#}"); + } + Some(Ok(None)) => { + debug!("probe canceled"); + } + Some(Ok(Some(Err(time::Elapsed { .. })))) => { + debug!("probe timed out"); + } + None => { + trace!("report canceled"); + } + } + } + else => { + break; + } + } + } + + // make sure to cancel all outstanding reports + v4_buf.abort_all(); + v6_buf.abort_all(); + + reports + } + + /// Check if we have enough information to consider the current report "good enough". + fn have_enough_reports( + &self, + state: &IfStateDetails, + do_full: bool, + num_relays: usize, + report: &Report, + ) -> bool { + #[cfg_attr(wasm_browser, allow(unused_mut))] + let mut num_ipv4 = 0; + #[cfg_attr(wasm_browser, allow(unused_mut))] + let mut num_ipv6 = 0; + let mut num_https = 0; + for (typ, _, _) in report.relay_latency.iter() { + match typ { + #[cfg(not(wasm_browser))] + Probe::QadIpv4 => { + num_ipv4 += 1; + } + #[cfg(not(wasm_browser))] + Probe::QadIpv6 => { + num_ipv6 += 1; + } + Probe::Https => { + num_https += 1; + } + } + } + + if do_full { + // Full report, require more probes + match (state.have_v4, state.have_v6) { + (true, true) => { + // Both IPv4 and IPv6 are expected to be available + if num_ipv4 >= 2 && num_ipv6 >= 1 || num_ipv6 >= 2 && num_ipv4 >= 1 { + return true; + } + } + (true, false) => { + // Just Ipv4 is expected + if num_ipv4 >= 2 { + return true; + } + } + (false, true) => { + // Just Ipv6 is expected + if num_ipv6 >= 2 { + return true; + } + } + (false, false) => {} + } + if num_https >= num_relays { + // If we have at least one https probe per relay, we are happy + return true; + } + false + } else { + // Incremental reports, here the requirements are reduced further + match (state.have_v4, state.have_v6) { + (true, true) => { + // Both IPv4 and IPv6 are expected to be available + if num_ipv4 >= 1 && num_ipv6 >= 1 { + return true; + } + } + (true, false) => { + // Just Ipv4 is expected + if num_ipv4 >= 1 { + return true; + } + } + (false, true) => { + // Just Ipv6 is expected + if num_ipv6 >= 1 { + return true; + } + } + (false, false) => {} + } + if num_https >= num_relays { + // If we have at least one https probe per relay, we are happy + return true; + } + false + } + } + + /// Adds `r` to the set of recent Reports and mutates `r.preferred_relay` to contain the best recent one. + fn add_report_history_and_set_preferred_relay(&mut self, r: &mut Report) { + let mut prev_relay = None; + if let Some(ref last) = self.reports.last { + prev_relay.clone_from(&last.preferred_relay); + + // If we don't have new information, copy this from the last report + if r.mapping_varies_by_dest_ipv4.is_none() { + r.mapping_varies_by_dest_ipv4 = last.mapping_varies_by_dest_ipv4; + } + if r.mapping_varies_by_dest_ipv6.is_none() { + r.mapping_varies_by_dest_ipv6 = last.mapping_varies_by_dest_ipv6; + } + } + + let now = Instant::now(); + const MAX_AGE: Duration = Duration::from_secs(5 * 60); + + // relay ID => its best recent latency in last MAX_AGE + let mut best_recent = RelayLatencies::default(); + + // chain the current report as we are still mutating it + let prevs_iter = self + .reports + .prev + .iter() + .map(|(a, b)| -> (&Instant, &Report) { (a, b) }); + + let mut to_remove = Vec::new(); + for (t, pr) in prevs_iter { + if now.duration_since(*t) > MAX_AGE { + to_remove.push(*t); + continue; + } + best_recent.merge(&pr.relay_latency); + } + // merge in current run + best_recent.merge(&r.relay_latency); + + for t in to_remove { + self.reports.prev.remove(&t); + } + + // Then, pick which currently-alive relay server from the + // current report has the best latency over the past MAX_AGE. + let mut best_any = Duration::default(); + let mut old_relay_cur_latency = Duration::default(); + { + for (_, url, duration) in r.relay_latency.iter() { + if Some(url) == prev_relay.as_ref() { + old_relay_cur_latency = duration; + } + if let Some(best) = best_recent.get(url) + && (r.preferred_relay.is_none() || best < best_any) + { + best_any = best; + r.preferred_relay.replace(url.clone()); + } + } + + // If we're changing our preferred relay but the old one's still + // accessible and the new one's not much better, just stick with + // where we are. + if prev_relay.is_some() + && r.preferred_relay != prev_relay + && !old_relay_cur_latency.is_zero() + && best_any > old_relay_cur_latency / 3 * 2 + { + r.preferred_relay = prev_relay; + } + } + + self.reports.prev.insert(now, r.clone()); + self.reports.last = Some(r.clone()); + } +} + +#[cfg(not(wasm_browser))] +async fn run_probe_v4( + relay: Arc, + quic_client: QuicClient, + dns_resolver: DnsResolver, + shutdown_token: CancellationToken, +) -> n0_error::Result<(QadProbeReport, QadConn), QadProbeError> { + use noq_proto::PathId; + + let relay_addr = reportgen::get_relay_addr_ipv4(&dns_resolver, &relay) + .await + .map_err(|source| e!(QadProbeError::GetRelayAddr { source }))?; + + trace!(?relay_addr, "resolved relay server address"); + let host = relay + .url + .host_str() + .ok_or_else(|| e!(QadProbeError::MissingHost))?; + let conn = quic_client + .create_conn(relay_addr.into(), host) + .await + .map_err(|source| e!(QadProbeError::Quic { source }))?; + + let mut watcher = conn.observed_external_addr(); + + // wait for an addr + let addr = watcher + .next() + .await + .ok_or_else(|| e!(QadProbeError::ReceiverDropped))?; + let report = QadProbeReport { + relay: relay.url.clone(), + addr: SocketAddr::new(addr.ip().to_canonical(), addr.port()), + latency: conn.rtt(PathId::ZERO).unwrap_or_default(), + }; + + let observer = Watchable::new(None); + let endpoint = relay.url.clone(); + let handle = task::spawn(shutdown_token.run_until_cancelled_owned({ + let conn = conn.clone(); + let observer = observer.clone(); + async move { + while let Some(val) = watcher.next().await { + // if we've sent to an ipv4 address, but received an observed address + // that is ivp6 then the address is an [IPv4-Mapped IPv6 Addresses](https://doc.rust-lang.org/beta/std/net/struct.Ipv6Addr.html#ipv4-mapped-ipv6-addresses) + let val = SocketAddr::new(val.ip().to_canonical(), val.port()); + let latency = conn.rtt(PathId::ZERO).unwrap_or_default(); + observer + .set(Some(QadProbeReport { + relay: endpoint.clone(), + addr: val, + latency, + })) + .ok(); + } + } + })); + let handle = AbortOnDropHandle::new(handle); + + Ok(( + report, + QadConn { + conn, + observer, + _handle: handle, + }, + )) +} + +#[cfg(not(wasm_browser))] +async fn run_probe_v6( + relay: Arc, + quic_client: QuicClient, + dns_resolver: DnsResolver, + shutdown_token: CancellationToken, +) -> n0_error::Result<(QadProbeReport, QadConn), QadProbeError> { + use noq_proto::PathId; + + let relay_addr = reportgen::get_relay_addr_ipv6(&dns_resolver, &relay) + .await + .map_err(|source| e!(QadProbeError::GetRelayAddr { source }))?; + + trace!(?relay_addr, "resolved relay server address"); + let host = relay + .url + .host_str() + .ok_or_else(|| e!(QadProbeError::MissingHost))?; + let conn = quic_client + .create_conn(relay_addr.into(), host) + .await + .map_err(|source| e!(QadProbeError::Quic { source }))?; + + let mut watcher = conn.observed_external_addr(); + + // wait for an addr + let addr = watcher + .next() + .await + .ok_or_else(|| e!(QadProbeError::ReceiverDropped))?; + let report = QadProbeReport { + relay: relay.url.clone(), + addr: SocketAddr::new(addr.ip().to_canonical(), addr.port()), + latency: conn.rtt(PathId::ZERO).unwrap_or_default(), + }; + + let observer = Watchable::new(None); + let endpoint = relay.url.clone(); + let handle = task::spawn(shutdown_token.run_until_cancelled_owned({ + let observer = observer.clone(); + let conn = conn.clone(); + async move { + while let Some(val) = watcher.next().await { + // if we've sent to an ipv4 address, but received an observed address + // that is ivp6 then the address is an [IPv4-Mapped IPv6 Addresses](https://doc.rust-lang.org/beta/std/net/struct.Ipv6Addr.html#ipv4-mapped-ipv6-addresses) + let val = SocketAddr::new(val.ip().to_canonical(), val.port()); + let latency = conn.rtt(PathId::ZERO).unwrap_or_default(); + observer + .set(Some(QadProbeReport { + relay: endpoint.clone(), + addr: val, + latency, + })) + .ok(); + } + } + })); + let handle = AbortOnDropHandle::new(handle); + + Ok(( + report, + QadConn { + conn, + observer, + _handle: handle, + }, + )) +} + +#[cfg(test)] +mod test_utils { + //! Creates a relay server against which to perform tests + + use iroh_relay::{RelayConfig, RelayQuicConfig, server}; + + pub(crate) async fn relay() -> (server::Server, RelayConfig) { + let server = server::Server::spawn(server::testing::server_config()) + .await + .expect("should serve relay"); + let quic = Some(RelayQuicConfig::new( + server.quic_addr().expect("server should run quic").port(), + )); + let endpoint_desc = + RelayConfig::new(server.https_url().expect("should work as relay"), quic); + + (server, endpoint_desc) + } + + /// Create a [`crate::RelayMap`] of the given size. + /// + /// This function uses [`relay`]. Note that the returned map uses internal order that will + /// often _not_ match the order of the servers. + pub(crate) async fn relay_map(relays: usize) -> (Vec, crate::RelayMap) { + let mut servers = Vec::with_capacity(relays); + let mut endpoints = Vec::with_capacity(relays); + for _ in 0..relays { + let (relay_server, endpoint) = relay().await; + servers.push(relay_server); + endpoints.push(endpoint); + } + (servers, crate::RelayMap::from_iter(endpoints)) + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::net::{Ipv4Addr, SocketAddr}; + + use iroh_base::RelayUrl; + use iroh_dns::dns::DnsResolver; + use iroh_relay::tls::{CaTlsConfig, default_provider}; + use n0_error::{Result, StdResultExt}; + use n0_tracing_test::traced_test; + use tokio_util::sync::CancellationToken; + + use super::*; + use crate::net_report::probes::Probe; + + #[tokio::test(flavor = "multi_thread")] + #[traced_test] + async fn test_basic() -> Result<()> { + let (server, relay) = test_utils::relay().await; + let client_config = iroh_relay::tls::make_dangerous_client_config(); + let ep = noq::Endpoint::client(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)).anyerr()?; + let quic_addr_disc = QuicConfig { + ep: ep.clone(), + client_config, + ipv4: true, + ipv6: true, + }; + let relay_map = RelayMap::from(relay); + + let resolver = DnsResolver::new(); + let tls_config = CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"); + let opts = Options::new(tls_config).quic_config(Some(quic_addr_disc.clone())); + let mut client = Client::new( + resolver.clone(), + relay_map.clone(), + opts.clone(), + Default::default(), + ); + let if_state = IfStateDetails::fake(); + + // Note that the ProbePlan will change with each iteration. + for i in 0..5 { + let cancel = CancellationToken::new(); + println!("--round {i}"); + let r = client + .get_report(if_state.clone(), false, cancel.child_token()) + .await; + + assert!(r.has_udp(), "want UDP"); + dbg!(&r); + assert!( + !r.relay_latency.is_empty(), + "expected at least 1 key in RelayLatency; got none", + ); + assert!( + r.relay_latency.iter().next().is_some(), + "expected key 1 in RelayLatency; got {:?}", + r.relay_latency + ); + assert!(r.global_v4.is_some(), "expected globalV4 set"); + assert!(r.preferred_relay.is_some(),); + cancel.cancel(); + } + + drop(client); + ep.wait_idle().await; + server.shutdown().await?; + + Ok(()) + } + + #[tokio::test(flavor = "current_thread", start_paused = true)] + async fn test_add_report_history_set_preferred_relay() -> Result { + fn relay_url(i: u16) -> RelayUrl { + format!("http://{i}.com").parse().unwrap() + } + + // report returns a *Report from (relay host, Duration)+ pairs. + fn report(a: impl IntoIterator) -> Option { + let mut report = Report::default(); + for (s, d) in a { + assert!(s.starts_with('d'), "invalid relay server key"); + let id: u16 = s[1..].parse().unwrap(); + report.relay_latency.update_relay( + relay_url(id), + Duration::from_secs(d), + Probe::QadIpv4, + ); + } + + Some(report) + } + struct Step { + /// Delay in seconds + after: u64, + r: Option, + } + struct Test { + name: &'static str, + steps: Vec, + /// want PreferredRelay on final step + want_relay: Option, + // wanted len(c.prev) + want_prev_len: usize, + } + + let tests = [ + Test { + name: "first_reading", + steps: vec![Step { + after: 0, + r: report([("d1", 2), ("d2", 3)]), + }], + want_prev_len: 1, + want_relay: Some(relay_url(1)), + }, + Test { + name: "with_two", + steps: vec![ + Step { + after: 0, + r: report([("d1", 2), ("d2", 3)]), + }, + Step { + after: 1, + r: report([("d1", 4), ("d2", 3)]), + }, + ], + want_prev_len: 2, + want_relay: Some(relay_url(1)), // t0's d1 of 2 is still best + }, + Test { + name: "but_now_d1_gone", + steps: vec![ + Step { + after: 0, + r: report([("d1", 2), ("d2", 3)]), + }, + Step { + after: 1, + r: report([("d1", 4), ("d2", 3)]), + }, + Step { + after: 2, + r: report([("d2", 3)]), + }, + ], + want_prev_len: 3, + want_relay: Some(relay_url(2)), // only option + }, + Test { + name: "d1_is_back", + steps: vec![ + Step { + after: 0, + r: report([("d1", 2), ("d2", 3)]), + }, + Step { + after: 1, + r: report([("d1", 4), ("d2", 3)]), + }, + Step { + after: 2, + r: report([("d2", 3)]), + }, + Step { + after: 3, + r: report([("d1", 4), ("d2", 3)]), + }, // same as 2 seconds ago + ], + want_prev_len: 4, + want_relay: Some(relay_url(1)), // t0's d1 of 2 is still best + }, + Test { + name: "things_clean_up", + steps: vec![ + Step { + after: 0, + r: report([("d1", 1), ("d2", 2)]), + }, + Step { + after: 1, + r: report([("d1", 1), ("d2", 2)]), + }, + Step { + after: 2, + r: report([("d1", 1), ("d2", 2)]), + }, + Step { + after: 3, + r: report([("d1", 1), ("d2", 2)]), + }, + Step { + after: 10 * 60, + r: report([("d3", 3)]), + }, + ], + want_prev_len: 1, // t=[0123]s all gone. (too old, older than 10 min) + want_relay: Some(relay_url(3)), // only option + }, + Test { + name: "preferred_relay_hysteresis_no_switch", + steps: vec![ + Step { + after: 0, + r: report([("d1", 4), ("d2", 5)]), + }, + Step { + after: 1, + r: report([("d1", 4), ("d2", 3)]), + }, + ], + want_prev_len: 2, + want_relay: Some(relay_url(1)), // 2 didn't get fast enough + }, + Test { + name: "preferred_relay_hysteresis_do_switch", + steps: vec![ + Step { + after: 0, + r: report([("d1", 4), ("d2", 5)]), + }, + Step { + after: 1, + r: report([("d1", 4), ("d2", 1)]), + }, + ], + want_prev_len: 2, + want_relay: Some(relay_url(2)), // 2 got fast enough + }, + ]; + let resolver = DnsResolver::new(); + let tls_config = CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"); + for mut tt in tests { + println!("test: {}", tt.name); + let relay_map = RelayMap::empty(); + let opts = Options::new(tls_config.clone()); + let mut client = Client::new(resolver.clone(), relay_map, opts, Default::default()); + for s in &mut tt.steps { + // trigger the timer + tokio::time::advance(Duration::from_secs(s.after)).await; + client.add_report_history_and_set_preferred_relay(s.r.as_mut().unwrap()); + } + let last_report = tt.steps.last().unwrap().r.clone().unwrap(); + let got = client.reports.prev.len(); + let want = tt.want_prev_len; + assert_eq!(got, want, "prev length"); + let got = &last_report.preferred_relay; + let want = &tt.want_relay; + assert_eq!(got, want, "preferred_relay"); + } + + Ok(()) + } +} diff --git a/vendor/iroh/src/net_report/defaults.rs b/vendor/iroh/src/net_report/defaults.rs new file mode 100644 index 0000000..ac06b95 --- /dev/null +++ b/vendor/iroh/src/net_report/defaults.rs @@ -0,0 +1,39 @@ +//! Default values used in net_report. + +/// Contains all timeouts that we use in `iroh-net-report`. +pub(crate) mod timeouts { + use n0_future::time::Duration; + + // Timeouts for net_report + + /// The maximum amount of time, in seconds, the net_report will spend gathering a single report. + // This is separated from `OVERALL_REPORT_TIMEOUT` to use as a reference + // in documentation outside of this crate. `OVERALL_REPORT_TIMEOUT` is a + // duration and rustdoc cannot calculate it at runtime, and so cannot be + // used directly for documentation purposes. + pub const TIMEOUT: u64 = 5; + + /// The maximum amount of time net_report will spend gathering a single report. + pub(crate) const OVERALL_REPORT_TIMEOUT: Duration = Duration::from_secs(TIMEOUT); + + /// The total time we wait for all the probes. + /// + /// This includes the QAD and HTTPS probes, which will all + /// start at different times based on the ProbePlan. + pub(crate) const PROBES_TIMEOUT: Duration = Duration::from_secs(3); + + /// How long to await for a captive-portal result. + /// + /// This delay is chosen so it starts after good-working QAD probes + /// would have finished, but not too long so the delay is bearable if + /// UDP/QAD is blocked. + pub(crate) const CAPTIVE_PORTAL_DELAY: Duration = Duration::from_millis(200); + + /// Timeout for captive portal checks + /// + /// Must be lower than [`OVERALL_REPORT_TIMEOUT`] minus + /// [`CAPTIVE_PORTAL_DELAY`]. + pub(crate) const CAPTIVE_PORTAL_TIMEOUT: Duration = Duration::from_secs(2); + + pub(crate) const DNS_TIMEOUT: Duration = Duration::from_secs(3); +} diff --git a/vendor/iroh/src/net_report/metrics.rs b/vendor/iroh/src/net_report/metrics.rs new file mode 100644 index 0000000..7a8238a --- /dev/null +++ b/vendor/iroh/src/net_report/metrics.rs @@ -0,0 +1,17 @@ +use iroh_metrics::{Counter, MetricsGroup}; +use serde::{Deserialize, Serialize}; + +/// Enum of metrics for the module +#[derive(Debug, Default, MetricsGroup, Serialize, Deserialize)] +#[metrics(name = "net_report")] +#[non_exhaustive] +pub struct Metrics { + /// Number of reports executed by net_report, including full reports. + pub reports: Counter, + /// Number of full reports executed by net_report + pub reports_full: Counter, + /// Number of port mapping attempts. + pub portmap_attempts: Counter, + /// Number of times an external address was obtained via port mapping. + pub portmap_external_address_updated: Counter, +} diff --git a/vendor/iroh/src/net_report/options.rs b/vendor/iroh/src/net_report/options.rs new file mode 100644 index 0000000..04abdd3 --- /dev/null +++ b/vendor/iroh/src/net_report/options.rs @@ -0,0 +1,120 @@ +//! Options for creating a report gen client. + +pub(crate) use imp::Options; + +#[cfg(not(wasm_browser))] +mod imp { + use std::collections::BTreeSet; + + use url::Url; + + use crate::net_report::{NetReportConfig, QuicConfig, probes::Probe}; + + /// Options for running probes + /// + /// By default, will run Https probes. + /// + /// Use [`Options::quic_config`] to enable QUIC address discovery. + #[derive(Debug, Clone)] + pub(crate) struct Options { + /// The configuration needed to launch QUIC address discovery probes. + /// + /// If not provided, will not run QUIC address discovery. + pub(crate) quic_config: Option, + /// TLS config for HTTPS probes. + pub(crate) tls_config: rustls::ClientConfig, + /// Proxy to send the HTTP(S) based probes through. + /// + /// If not provided, the probes connect directly. + pub(crate) proxy_url: Option, + /// User-facing configuration. + pub(crate) user_config: NetReportConfig, + } + + impl Options { + pub(crate) fn new(tls_config: rustls::ClientConfig) -> Self { + Self { + quic_config: None, + tls_config, + proxy_url: None, + user_config: NetReportConfig::default(), + } + } + /// Enable quic probes + pub(crate) fn quic_config(mut self, quic_config: Option) -> Self { + self.quic_config = quic_config; + self + } + + /// Sets the proxy to send the HTTP(S) based probes through. + pub(crate) fn proxy_url(mut self, proxy_url: Option) -> Self { + self.proxy_url = proxy_url; + self + } + + /// Set the net report configuration. + pub(crate) fn net_report_config(mut self, config: NetReportConfig) -> Self { + self.user_config = config; + self + } + + /// Turn the options into set of valid protocols + pub(crate) fn as_protocols(&self) -> BTreeSet { + let mut protocols = BTreeSet::new(); + if let Some(ref quic) = self.quic_config { + if quic.ipv4 { + protocols.insert(Probe::QadIpv4); + } + if quic.ipv6 { + protocols.insert(Probe::QadIpv6); + } + } + if self.user_config.https_probes { + protocols.insert(Probe::Https); + } + protocols + } + } +} + +#[cfg(wasm_browser)] +mod imp { + use std::collections::BTreeSet; + + use crate::net_report::{NetReportConfig, Probe}; + + /// Options for running probes (in browsers). + /// + /// Only HTTPS probes are supported in browsers. + /// These are run by default. + #[derive(Debug, Clone)] + pub(crate) struct Options { + /// User-facing configuration. + pub(crate) user_config: NetReportConfig, + } + + impl Default for Options { + fn default() -> Self { + Self { + user_config: NetReportConfig::default(), + } + } + } + + impl Options { + /// Set the net report configuration. + pub(crate) fn net_report_config(mut self, config: NetReportConfig) -> Self { + self.user_config = config; + self + } + + /// Turn the options into set of valid protocols + pub(crate) fn as_protocols(&self) -> BTreeSet { + let mut protocols = BTreeSet::new(); + if self.user_config.https_probes { + protocols.insert(Probe::Https); + } + protocols + } + } +} diff --git a/vendor/iroh/src/net_report/probes.rs b/vendor/iroh/src/net_report/probes.rs new file mode 100644 index 0000000..ecf507e --- /dev/null +++ b/vendor/iroh/src/net_report/probes.rs @@ -0,0 +1,266 @@ +//! The relay probes. +//! +//! All the probes try and establish the latency to the relay servers. Preferably the QAD +//! probes work and we also learn about our public IP addresses and ports. But fallback +//! probes for HTTPS exist as well. + +use std::{collections::BTreeSet, fmt, sync::Arc}; + +use iroh_relay::{RelayConfig, RelayMap}; +use n0_future::time::Duration; + +use crate::net_report::Report; + +/// The retransmit interval used. +const DEFAULT_INITIAL_RETRANSMIT: Duration = Duration::from_millis(100); + +/// The delay before starting HTTPS probes. +const HTTPS_OFFSET: Duration = Duration::from_millis(200); + +/// The protocol used to time an endpoint's latency. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, derive_more::Display)] +#[repr(u8)] +#[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] +#[non_exhaustive] +pub enum Probe { + /// HTTPS + Https, + /// QUIC Address Discovery Ipv4 + #[cfg(not(wasm_browser))] + QadIpv4, + /// QUIC Address Discovery Ipv6 + #[cfg(not(wasm_browser))] + QadIpv6, +} + +/// A probe set is a sequence of similar [`Probe`]s with delays between them. +/// +/// The probes are to the same Relayer and of the same [`Probe`] but will have different +/// delays. The delays are effectively retries, though they do not wait for the previous +/// probe to be finished. The first successful probe will cancel all other probes in the +/// set. +/// +/// This is a lot of type-safety by convention. It would be so much nicer to have this +/// compile-time checked but that introduces a giant mess of generics and traits and +/// associated exploding types. +/// +/// A [`ProbeSet`] implements [`IntoIterator`] similar to how [`Vec`] does. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(super) struct ProbeSet { + /// The [`Probe`] all the probes in this set have. + proto: Probe, + /// The data in the set. + probes: Vec<(Duration, Arc)>, +} + +impl ProbeSet { + fn new(proto: Probe) -> Self { + Self { + probes: Vec::new(), + proto, + } + } + + pub(super) fn proto(&self) -> Probe { + self.proto + } + + fn push(&mut self, delay: Duration, endpoint: Arc) { + self.probes.push((delay, endpoint)); + } + + fn is_empty(&self) -> bool { + self.probes.is_empty() + } + + pub(super) fn params(&self) -> impl Iterator)> { + self.probes.iter() + } +} + +/// A probe plan. +/// +/// A probe plan contains a number of [`ProbeSet`]s containing probes to be executed. +/// Generally the first probe of of a set which completes aborts the remaining probes of a +/// set. Sometimes a failing probe can also abort the remaining probes of a set. +/// +/// The [`reportgen`] actor will also abort all the remaining [`ProbeSet`]s once it has +/// sufficient information for a report. +/// +/// [`reportgen`]: crate::net_report::reportgen +#[derive(Debug, Default, PartialEq, Eq)] +pub(super) struct ProbePlan { + set: BTreeSet, +} + +impl ProbePlan { + /// Creates an initial probe plan + pub(super) fn initial(relay_map: &RelayMap, protocols: &BTreeSet) -> Self { + let mut plan = Self::default(); + + for relay in relay_map.relays::>() { + let mut https_probes = ProbeSet::new(Probe::Https); + + for attempt in 0u32..3 { + let delay = HTTPS_OFFSET + DEFAULT_INITIAL_RETRANSMIT * attempt; + https_probes.push(delay, relay.clone()); + } + + plan.add_if_enabled(protocols, https_probes); + } + plan + } + + /// Creates a follow up probe plan using a previous net_report report in browsers. + /// + /// This will only schedule HTTPS probes. + pub(super) fn with_last_report( + relay_map: &RelayMap, + last_report: &Report, + protocols: &BTreeSet, + ) -> Self { + if last_report.relay_latency.is_empty() { + return Self::initial(relay_map, protocols); + } + + // TODO: is this good? + Self::default() + } + + /// Returns an iterator over the [`ProbeSet`]s in this plan. + pub(super) fn iter(&self) -> impl Iterator { + self.set.iter() + } + + /// Adds a [`ProbeSet`] if it contains probes and the protocol indicated in + /// the [`ProbeSet] matches a protocol in our set of [`Probe`]s. + fn add_if_enabled(&mut self, protocols: &BTreeSet, set: ProbeSet) { + if !set.is_empty() && protocols.contains(&set.proto) { + self.set.insert(set); + } + } +} + +impl fmt::Display for ProbePlan { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "ProbePlan {{")?; + for probe_set in self.set.iter() { + writeln!(f, r#" ProbeSet("{}") {{"#, probe_set.proto)?; + for (delay, endpoint) in probe_set.probes.iter() { + writeln!(f, " {delay:?} to {endpoint},")?; + } + writeln!(f, " }}")?; + } + writeln!(f, "}}") + } +} + +impl FromIterator for ProbePlan { + fn from_iter>(iter: T) -> Self { + Self { + set: iter.into_iter().collect(), + } + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + use crate::net_report::test_utils; + + /// Shorthand which declares a new ProbeSet. + /// + /// `$kind`: The `Probe`. + /// `$endpoint`: Expression which will be an `Arc`. + /// `$delays`: A `Vec` of the delays for this probe. + macro_rules! probeset { + (proto: Probe::$kind:ident, relay: $endpoint:expr, delays: $delays:expr,) => { + ProbeSet { + proto: Probe::$kind, + probes: $delays.iter().map(|delay| (*delay, $endpoint)).collect(), + } + }; + } + + fn default_protocols() -> BTreeSet { + BTreeSet::from([Probe::QadIpv4, Probe::QadIpv6, Probe::Https]) + } + + #[tokio::test] + async fn test_initial_probeplan() { + let (_servers, relay_map) = test_utils::relay_map(2).await; + let relay_1 = &relay_map.relays::>()[0]; + let relay_2 = &relay_map.relays::>()[1]; + let plan = ProbePlan::initial(&relay_map, &default_protocols()); + + let expected_plan: ProbePlan = [ + probeset! { + proto: Probe::Https, + relay: relay_1.clone(), + delays: [ + Duration::from_millis(200), + Duration::from_millis(300), + Duration::from_millis(400) + ], + }, + probeset! { + proto: Probe::Https, + relay: relay_2.clone(), + delays: [ + Duration::from_millis(200), + Duration::from_millis(300), + Duration::from_millis(400) + ], + }, + ] + .into_iter() + .collect(); + + println!("expected:"); + println!("{expected_plan}"); + println!("actual:"); + println!("{plan}"); + // The readable error: + assert_eq!(plan.to_string(), expected_plan.to_string()); + // Just in case there's a bug in the Display impl: + assert_eq!(plan, expected_plan); + } + + #[tokio::test] + async fn test_initial_probeplan_some_protocols() { + let (_servers, relay_map) = test_utils::relay_map(2).await; + let relay_1 = &relay_map.relays::>()[0]; + let relay_2 = &relay_map.relays::>()[1]; + let plan = ProbePlan::initial(&relay_map, &BTreeSet::from([Probe::Https])); + + let expected_plan: ProbePlan = [ + probeset! { + proto: Probe::Https, + relay: relay_1.clone(), + delays: [Duration::from_millis(200), + Duration::from_millis(300), + Duration::from_millis(400)], + }, + probeset! { + proto: Probe::Https, + relay: relay_2.clone(), + delays: [Duration::from_millis(200), + Duration::from_millis(300), + Duration::from_millis(400)], + }, + ] + .into_iter() + .collect(); + + println!("expected:"); + println!("{expected_plan}"); + println!("actual:"); + println!("{plan}"); + // The readable error: + assert_eq!(plan.to_string(), expected_plan.to_string()); + // Just in case there's a bug in the Display impl: + assert_eq!(plan, expected_plan); + } +} diff --git a/vendor/iroh/src/net_report/report.rs b/vendor/iroh/src/net_report/report.rs new file mode 100644 index 0000000..4810cab --- /dev/null +++ b/vendor/iroh/src/net_report/report.rs @@ -0,0 +1,221 @@ +use std::{ + collections::BTreeMap, + fmt, + net::{SocketAddr, SocketAddrV4, SocketAddrV6}, + time::Duration, +}; + +use iroh_base::RelayUrl; +use serde::{Deserialize, Serialize}; +use tracing::{trace, warn}; + +use super::{ProbeReport, probes::Probe}; + +/// A net_report report. +#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] +#[non_exhaustive] +pub struct Report { + /// A QAD IPv4 round trip completed. + pub udp_v4: bool, + /// A QAD IPv6 round trip completed. + pub udp_v6: bool, + /// Whether the reported public address differs when probing different servers (on IPv4). + pub mapping_varies_by_dest_ipv4: Option, + /// Whether the reported public address differs when probing different servers (on IPv6). + pub mapping_varies_by_dest_ipv6: Option, + /// The relay server with the lowest latency, if any. + pub preferred_relay: Option, + /// The measured latency to each relay, keyed by relay URL. + pub relay_latency: RelayLatencies, + /// The discovered global IPv4 address and port, if any. + pub global_v4: Option, + /// The discovered global IPv6 address and port, if any. + pub global_v6: Option, + /// CaptivePortal is set when we think there's a captive portal that is + /// intercepting HTTP traffic. + pub captive_portal: Option, +} + +impl fmt::Display for Report { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self, f) + } +} + +impl Report { + /// Do we have any indication that UDP is working? + #[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] + pub fn has_udp(&self) -> bool { + self.udp_v4 || self.udp_v6 + } + + /// Whether the reported public address differs when probing different servers. + #[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] + pub fn mapping_varies_by_dest(&self) -> Option { + match ( + self.mapping_varies_by_dest_ipv4, + self.mapping_varies_by_dest_ipv6, + ) { + (Some(v4), Some(v6)) => Some(v4 || v6), + (None, Some(v6)) => Some(v6), + (Some(v4), None) => Some(v4), + (None, None) => None, + } + } + + /// Updates a net_report [`Report`] with a new [`ProbeReport`]. + pub(super) fn update(&mut self, report: &ProbeReport) { + match report { + ProbeReport::Https(report) => { + self.relay_latency + .update_relay(report.relay.clone(), report.latency, Probe::Https); + } + #[cfg(not(wasm_browser))] + ProbeReport::QadIpv4(report) => { + self.relay_latency.update_relay( + report.relay.clone(), + report.latency, + Probe::QadIpv4, + ); + let SocketAddr::V4(ipp) = report.addr else { + warn!("received IPv6 address from IPv4 QAD: {}", report.addr); + return; + }; + + self.udp_v4 = true; + + if let Some(global) = self.global_v4 { + if global == ipp { + if self.mapping_varies_by_dest_ipv4.is_none() { + self.mapping_varies_by_dest_ipv4 = Some(false); + } + } else { + self.mapping_varies_by_dest_ipv4 = Some(true); + warn!("IPv4 address detected by QAD varies by destination"); + } + } else { + self.global_v4 = Some(ipp); + } + trace!(?self.global_v4, ?self.mapping_varies_by_dest_ipv4, %ipp, "stored report"); + } + #[cfg(not(wasm_browser))] + ProbeReport::QadIpv6(report) => { + self.relay_latency.update_relay( + report.relay.clone(), + report.latency, + Probe::QadIpv6, + ); + let SocketAddr::V6(ipp) = report.addr else { + warn!("received IPv4 address from IPv6 QAD: {}", report.addr); + return; + }; + + self.udp_v6 = true; + if let Some(global) = self.global_v6 { + if global == ipp { + if self.mapping_varies_by_dest_ipv6.is_none() { + self.mapping_varies_by_dest_ipv6 = Some(false); + } + } else { + self.mapping_varies_by_dest_ipv6 = Some(true); + warn!("IPv6 address detected by QAD varies by destination"); + } + } else { + self.global_v6 = Some(ipp); + } + trace!(?self.global_v6, ?self.mapping_varies_by_dest_ipv6, %ipp, "stored report"); + } + } + } +} + +/// Latencies per relay endpoint. +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] +pub struct RelayLatencies { + #[cfg(not(wasm_browser))] + ipv4: BTreeMap, + #[cfg(not(wasm_browser))] + ipv6: BTreeMap, + https: BTreeMap, +} + +impl RelayLatencies { + /// Updates a relay's latency, if it is faster than before. + pub(super) fn update_relay(&mut self, url: RelayUrl, latency: Duration, probe: Probe) { + let list = match probe { + Probe::Https => &mut self.https, + #[cfg(not(wasm_browser))] + Probe::QadIpv4 => &mut self.ipv4, + #[cfg(not(wasm_browser))] + Probe::QadIpv6 => &mut self.ipv6, + }; + let old_latency = list.entry(url).or_insert(latency); + if latency < *old_latency { + *old_latency = latency; + } + } + + /// Merges another [`RelayLatencies`] into this one. + /// + /// For each relay the latency is updated using [`RelayLatencies::update_relay`]. + pub(super) fn merge(&mut self, other: &RelayLatencies) { + for (url, latency) in other.https.iter() { + self.update_relay(url.clone(), *latency, Probe::Https); + } + #[cfg(not(wasm_browser))] + for (url, latency) in other.ipv4.iter() { + self.update_relay(url.clone(), *latency, Probe::QadIpv4); + } + #[cfg(not(wasm_browser))] + for (url, latency) in other.ipv6.iter() { + self.update_relay(url.clone(), *latency, Probe::QadIpv6); + } + } + + /// Returns an iterator over all the relays and their latencies. + #[cfg(not(wasm_browser))] + #[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] + pub fn iter(&self) -> impl Iterator + '_ { + self.https + .iter() + .map(|(url, l)| (Probe::Https, url, *l)) + .chain(self.ipv4.iter().map(|(url, l)| (Probe::QadIpv4, url, *l))) + .chain(self.ipv6.iter().map(|(url, l)| (Probe::QadIpv6, url, *l))) + } + + /// Returns an iterator over all the relays and their latencies. + #[cfg(wasm_browser)] + #[cfg_attr(not(feature = "unstable-net-report"), allow(unreachable_pub))] + pub fn iter(&self) -> impl Iterator + '_ { + self.https.iter().map(|(k, v)| (Probe::Https, k, *v)) + } + + #[cfg(not(wasm_browser))] + pub(super) fn is_empty(&self) -> bool { + self.https.is_empty() && self.ipv4.is_empty() && self.ipv6.is_empty() + } + + #[cfg(wasm_browser)] + pub(super) fn is_empty(&self) -> bool { + self.https.is_empty() + } + + /// Returns the lowest latency across records. + pub(super) fn get(&self, url: &RelayUrl) -> Option { + let mut list = Vec::with_capacity(3); + if let Some(val) = self.https.get(url) { + list.push(*val); + } + #[cfg(not(wasm_browser))] + if let Some(val) = self.ipv4.get(url) { + list.push(*val); + } + #[cfg(not(wasm_browser))] + if let Some(val) = self.ipv6.get(url) { + list.push(*val); + } + list.into_iter().min() + } +} diff --git a/vendor/iroh/src/net_report/reportgen.rs b/vendor/iroh/src/net_report/reportgen.rs new file mode 100644 index 0000000..6c39bb9 --- /dev/null +++ b/vendor/iroh/src/net_report/reportgen.rs @@ -0,0 +1,997 @@ +//! The reportgen actor is responsible for generating a single net_report report. +//! +//! It is implemented as an actor with [`Client`] as handle. +//! +//! The actor starts generating the report as soon as it is created, it does not receive any +//! messages from the client. It follows roughly these steps: +//! +//! - Determines host IPv6 support. +//! - Creates portmapper future. +//! - Creates captive portal detection future. +//! - Creates Probe Set futures. +//! - These send messages to the reportgen actor. +//! - Loops driving the futures and handling actor messages: +//! - Disables futures as they are completed or aborted. +//! - Stop if there are no outstanding tasks/futures, or on timeout. +//! - Sends the completed report to the net_report actor. + +#[cfg(not(wasm_browser))] +use std::net::{SocketAddrV4, SocketAddrV6}; +use std::{ + collections::BTreeSet, + net::{IpAddr, SocketAddr}, + sync::Arc, +}; + +use http::StatusCode; +use iroh_base::RelayUrl; +#[cfg(not(wasm_browser))] +use iroh_dns::dns::{DnsError, DnsResolver, StaggeredError}; +#[cfg(not(wasm_browser))] +use iroh_relay::quic::QuicClient; +use iroh_relay::{ + RelayConfig, RelayMap, defaults::DEFAULT_RELAY_QUIC_PORT, http::RELAY_PROBE_PATH, +}; +use n0_error::{e, stack_error}; +#[cfg(wasm_browser)] +use n0_future::future::Pending; +use n0_future::{ + StreamExt as _, + task::{self, AbortOnDropHandle, JoinSet}, + time::{self, Duration, Instant}, +}; +use rand::seq::IteratorRandom; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::{Instrument, debug, error, info_span, trace, warn}; +#[cfg(not(wasm_browser))] +use url::Url; + +#[cfg(not(wasm_browser))] +use super::defaults::timeouts::DNS_TIMEOUT; +use super::{ + Report, + probes::{Probe, ProbePlan}, +}; +#[cfg(not(wasm_browser))] +use crate::address_lookup::DNS_STAGGERING_MS; +use crate::{ + net_report::defaults::timeouts::{ + CAPTIVE_PORTAL_DELAY, CAPTIVE_PORTAL_TIMEOUT, OVERALL_REPORT_TIMEOUT, PROBES_TIMEOUT, + }, + util::reqwest_client_builder, +}; + +/// Holds the state for a single report generation. +/// +/// Dropping this will cancel the actor and stop the report generation. +#[derive(Debug)] +pub(super) struct Client { + _drop_guard: AbortOnDropHandle<()>, +} + +/// Some details required from the interface state of the device. +#[derive(Debug, Clone, Default)] +pub(crate) struct IfStateDetails { + /// Do we have IPv4 capbilities + pub(crate) have_v4: bool, + /// Do we have IPv6 capbilities + pub(crate) have_v6: bool, +} + +impl IfStateDetails { + #[cfg(test)] + pub(super) fn fake() -> Self { + IfStateDetails { + have_v4: true, + have_v6: true, + } + } +} + +impl From for IfStateDetails { + fn from(value: netwatch::netmon::State) -> Self { + IfStateDetails { + have_v4: value.have_v4, + have_v6: value.have_v6, + } + } +} + +/// Any state that depends on sockets being available in the current environment. +/// +/// Factored out so it can be disabled easily in browsers. +#[cfg(not(wasm_browser))] +#[derive(Debug, Clone)] +pub(super) struct SocketState { + /// QUIC client to do QUIC address Discovery + pub(super) quic_client: Option, + /// The DNS resolver to use for probes that need to resolve DNS records. + pub(super) dns_resolver: DnsResolver, + /// The proxy to send the HTTP(S) based probes through, if any. + pub(super) proxy_url: Option, +} + +impl Client { + /// Creates a new actor generating a single report. + /// + /// The actor starts running immediately and only generates a single report, after which + /// it shuts down. Dropping this handle will abort the actor. + #[allow(clippy::too_many_arguments)] + pub(super) fn new( + last_report: Option, + relay_map: RelayMap, + protocols: BTreeSet, + captive_portal_check: bool, + if_state: IfStateDetails, + shutdown_token: CancellationToken, + #[cfg(not(wasm_browser))] socket_state: SocketState, + #[cfg(not(wasm_browser))] tls_config: rustls::ClientConfig, + ) -> (Self, mpsc::Receiver) { + let (msg_tx, msg_rx) = mpsc::channel(32); + let actor = Actor { + msg_tx, + last_report, + relay_map, + protocols, + captive_portal_check, + #[cfg(not(wasm_browser))] + socket_state, + #[cfg(not(wasm_browser))] + tls_config, + if_state, + }; + let task = task::spawn( + actor + .run(shutdown_token) + .instrument(info_span!("reportgen-actor")), + ); + ( + Self { + _drop_guard: AbortOnDropHandle::new(task), + }, + msg_rx, + ) + } +} + +/// The reportstate actor. +/// +/// This actor starts, generates a single report and exits. +#[derive(Debug)] +struct Actor { + msg_tx: mpsc::Sender, + + // Provided state + /// The previous report, if it exists. + last_report: Option, + /// The relay configuration. + relay_map: RelayMap, + + // Internal state. + /// Protocols we should attempt to create probes for, if we have the correct + /// configuration for that protocol. + protocols: BTreeSet, + + /// Whether to check for captive portals. + captive_portal_check: bool, + + /// Any socket-related state that doesn't exist/work in browsers + #[cfg(not(wasm_browser))] + socket_state: SocketState, + #[cfg(not(wasm_browser))] + tls_config: rustls::ClientConfig, + if_state: IfStateDetails, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub(super) enum ProbesError { + #[error("Probe failed")] + ProbeFailure { source: ProbeError }, + #[error("All probes failed")] + AllProbesFailed, + #[error("Probe cancelled")] + Cancelled, + #[error("Probe timed out")] + Timeout, +} + +#[derive(Debug)] +pub(super) enum ProbeFinished { + Regular(Result), + #[cfg(not(wasm_browser))] + CaptivePortal(Option), +} + +impl Actor { + async fn run(self, shutdown_token: CancellationToken) { + shutdown_token + .run_until_cancelled_owned(async { + match time::timeout(OVERALL_REPORT_TIMEOUT, self.run_inner()).await { + Ok(()) => trace!("reportgen actor finished"), + Err(time::Elapsed { .. }) => { + debug!("reportgen timed out"); + } + } + }) + .await; + } + + /// Runs the main reportgen actor logic. + /// + /// This actor runs by: + /// + /// - Creates a captive portal future. + /// - Creates ProbeSet futures in a group of futures. + /// - Runs a main loop: + /// - Drives all the above futures. + /// - Receives actor messages (sent by those futures). + /// - Updates the report, cancels unneeded futures. + /// - Sends the report to the net_report actor. + async fn run_inner(self) { + trace!("reportgen actor starting"); + + let mut probes = JoinSet::default(); + + let _probes_token = self.spawn_probes_task(self.if_state.clone(), &mut probes); + let mut num_probes = probes.len(); + + let captive_token = self.prepare_captive_portal_task(&mut probes); + + // any reports of working UDP/QUIC? + let mut have_udp = false; + + // Check for probes finishing. + while let Some(probe_result) = probes.join_next().await { + trace!(?probe_result, num_probes, "processing finished probe"); + match probe_result { + Ok(report) => { + #[cfg_attr(wasm_browser, allow(irrefutable_let_patterns))] + if let ProbeFinished::Regular(report) = &report { + have_udp |= report.as_ref().map(|r| r.is_udp()).unwrap_or_default(); + num_probes -= 1; + + // If all probes are done & we have_udp cancel captive + if num_probes == 0 { + trace!("all regular probes done"); + debug_assert!(probes.len() <= 1, "{} probes", probes.len()); + + if have_udp { + captive_token.cancel(); + } + } + } + self.msg_tx.send(report).await.ok(); + } + Err(e) => { + if e.is_panic() { + error!("Task panicked {:?}", e); + break; + } + warn!("probes task join error: {:?}", e); + } + } + } + } + + /// Creates the future which will perform the captive portal check. + fn prepare_captive_portal_task(&self, tasks: &mut JoinSet) -> CancellationToken { + let token = CancellationToken::new(); + + // If we're doing a full probe, also check for a captive portal. We + // delay by a bit to wait for UDP QAD to finish, to avoid the probe if + // it's unnecessary. + #[cfg(not(wasm_browser))] + if self.captive_portal_check && self.last_report.is_none() { + // Even if we're doing a non-incremental update, we may want to try our + // preferred relay for captive portal detection. + let preferred_relay = self + .last_report + .as_ref() + .and_then(|l| l.preferred_relay.clone()); + + let dns_resolver = self.socket_state.dns_resolver.clone(); + let proxy_url = self.socket_state.proxy_url.clone(); + let dm = self.relay_map.clone(); + let token = token.clone(); + #[cfg(not(wasm_browser))] + let tls_config = self.tls_config.clone(); + tasks.spawn( + async move { + let res = token + .run_until_cancelled_owned(async move { + time::sleep(CAPTIVE_PORTAL_DELAY).await; + trace!("check started after {CAPTIVE_PORTAL_DELAY:?}"); + time::timeout( + CAPTIVE_PORTAL_TIMEOUT, + check_captive_portal( + &dns_resolver, + &dm, + preferred_relay, + tls_config, + proxy_url.as_ref(), + ), + ) + .await + }) + .await; + let res = match res { + Some(Ok(Ok(found))) => Some(found), + Some(Ok(Err(err))) => { + match err { + CaptivePortalError::CreateReqwestClient { source, .. } + | CaptivePortalError::HttpRequest { source, .. } + if source.is_connect() => + { + debug!("check_captive_portal failed: {source:#}"); + } + err => debug!("check_captive_portal error: {err:#}"), + } + None + } + Some(Err(time::Elapsed { .. })) => { + debug!("probe timed out"); + None + } + None => { + trace!("probe cancelled"); + None + } + }; + ProbeFinished::CaptivePortal(res) + } + .instrument(info_span!("captive-portal")), + ); + } + token + } + + /// Prepares the future which will run all the probes as per generated ProbePlan. + /// + /// Probes operate like the following: + /// + /// - A future is created for each probe in all probe sets. + /// - All probes in a set are grouped in [`JoinSet`]. + /// - All those probe sets are grouped in one overall [`JoinSet`]. + /// - This future is polled by the main actor loop to make progress. + /// - Once a probe future is polled: + /// - Many probes start with a delay, they sleep during this time. + /// - When a probe starts it first asks the reportgen [`Actor`] if it is still useful + /// to run. If not it aborts the entire probe set. + /// - When a probe finishes, its [`ProbeReport`] is yielded to the reportgen actor. + /// - Probes get aborted in several ways: + /// - A running it can fail and abort the entire probe set if it deems the + /// failure permanent. Probes in a probe set are essentially retries. + /// - Once there are [`ProbeReport`]s from enough relays, all remaining probes are + /// aborted. That is, the main actor loop stops polling them. + fn spawn_probes_task( + &self, + if_state: IfStateDetails, + probes: &mut JoinSet, + ) -> CancellationToken { + trace!(?if_state, "local interface details"); + let plan = match self.last_report { + Some(ref report) => { + ProbePlan::with_last_report(&self.relay_map, report, &self.protocols) + } + None => ProbePlan::initial(&self.relay_map, &self.protocols), + }; + trace!(%plan, "probe plan"); + + let token = CancellationToken::new(); + + for probe_set in plan.iter() { + let set_token = token.child_token(); + let proto = probe_set.proto(); + for (delay, relay) in probe_set.params() { + let probe_token = set_token.child_token(); + + let fut = probe_token.run_until_cancelled_owned(time::timeout( + PROBES_TIMEOUT, + proto.run( + *delay, + relay.clone(), + #[cfg(not(wasm_browser))] + self.socket_state.clone(), + #[cfg(not(wasm_browser))] + self.tls_config.clone(), + ), + )); + probes.spawn( + async move { + let res = fut.await; + let res = match res { + Some(Ok(Ok(report))) => Ok(report), + Some(Ok(Err(err))) => { + debug!("probe failed: {:#}", err); + Err(e!(ProbesError::ProbeFailure, err)) + } + Some(Err(time::Elapsed { .. })) => Err(e!(ProbesError::Timeout)), + None => Err(e!(ProbesError::Cancelled)), + }; + ProbeFinished::Regular(res) + } + .instrument(info_span!( + "run-probe", + ?proto, + ?delay, + relay=%relay.url, + )), + ); + } + } + + token + } +} + +/// The result of running a probe. +#[derive(Debug, Clone)] +pub(super) enum ProbeReport { + #[cfg(not(wasm_browser))] + QadIpv4(QadProbeReport), + #[cfg(not(wasm_browser))] + QadIpv6(QadProbeReport), + Https(HttpsProbeReport), +} + +impl ProbeReport { + #[cfg(not(wasm_browser))] + pub(super) fn is_udp(&self) -> bool { + matches!(self, Self::QadIpv4(_) | Self::QadIpv6(_)) + } + + #[cfg(wasm_browser)] + pub(super) fn is_udp(&self) -> bool { + false + } +} + +#[cfg(not(wasm_browser))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct QadProbeReport { + /// The relay that was probed + pub(super) relay: RelayUrl, + /// The latency to the relay. + pub(super) latency: Duration, + /// The discovered public address. + pub(super) addr: SocketAddr, +} + +#[derive(Debug, Clone)] +pub(super) struct HttpsProbeReport { + /// The relay that was probed + pub(super) relay: RelayUrl, + /// The latency to the relay. + pub(super) latency: Duration, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub(super) enum ProbeError { + #[error("Client is gone")] + ClientGone, + #[error("Probe is no longer useful")] + NotUseful, + #[error("Failed to run HTTPS probe")] + Https { source: MeasureHttpsLatencyError }, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub(super) enum QuicError { + #[error("No relay available")] + NoRelay, + #[error("URL must have 'host' to use QUIC address discovery probes")] + InvalidUrl, +} + +/// Pieces needed to do QUIC address discovery. +#[derive(derive_more::Debug, Clone)] +pub(crate) struct QuicConfig { + /// A QUIC Endpoint + #[debug("noq::Endpoint")] + pub(crate) ep: noq::Endpoint, + /// A client config. + pub(crate) client_config: rustls::ClientConfig, + /// Enable ipv4 QUIC address discovery probes + pub(crate) ipv4: bool, + /// Enable ipv6 QUIC address discovery probes + pub(crate) ipv6: bool, +} + +impl Probe { + /// Executes this particular [`Probe`], including using a delayed start if needed. + async fn run( + self, + delay: Duration, + relay: Arc, + #[cfg(not(wasm_browser))] socket_state: SocketState, + #[cfg(not(wasm_browser))] tls_config: rustls::ClientConfig, + ) -> Result { + if !delay.is_zero() { + trace!("delaying probe"); + time::sleep(delay).await; + } + trace!("starting probe"); + + let report = match self { + Probe::Https => { + match run_https_probe( + #[cfg(not(wasm_browser))] + &socket_state.dns_resolver, + relay.url.clone(), + #[cfg(not(wasm_browser))] + tls_config, + #[cfg(not(wasm_browser))] + socket_state.proxy_url.as_ref(), + ) + .await + { + Ok(report) => Ok(ProbeReport::Https(report)), + Err(err) => Err(e!(ProbeError::Https, err)), + } + } + #[cfg(not(wasm_browser))] + Probe::QadIpv4 | Probe::QadIpv6 => unreachable!("must not be used"), + }; + debug!(?report, "probe finished"); + report + } +} + +#[cfg(not(wasm_browser))] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +enum CaptivePortalError { + #[error(transparent)] + DnsLookup { + #[error(from)] + source: StaggeredError, + }, + #[error("Creating HTTP client failed")] + CreateReqwestClient { + #[error(std_err)] + source: reqwest::Error, + }, + #[error("HTTP request failed")] + HttpRequest { + #[error(std_err)] + source: reqwest::Error, + }, +} + +/// Reports whether or not we think the system is behind a +/// captive portal, detected by making a request to a URL that we know should +/// return a "204 No Content" response and checking if that's what we get. +/// +/// The boolean return is whether we think we have a captive portal. +#[cfg(not(wasm_browser))] +async fn check_captive_portal( + dns_resolver: &DnsResolver, + dm: &RelayMap, + preferred_relay: Option, + tls_config: rustls::ClientConfig, + proxy_url: Option<&Url>, +) -> Result { + // If we have a preferred relay and we can use it for non-QAD requests, try that; + // otherwise, pick a random one suitable for non-STUN requests. + + use crate::util::reqwest_client_builder; + + let preferred_relay = preferred_relay.and_then(|url| dm.get(&url).map(|_| url)); + + let url = match preferred_relay { + Some(url) => url, + None => { + let urls: Vec<_> = dm.urls(); + if urls.is_empty() { + trace!("No suitable relay for captive portal check"); + return Ok(false); + } + + let i = (0..urls.len()).choose(&mut rand::rng()).unwrap_or_default(); + urls[i].clone() + } + }; + + let mut builder = reqwest_client_builder(tls_config, dns_resolver.clone()) + .redirect(reqwest::redirect::Policy::none()); + if let Some(proxy_url) = proxy_url { + let proxy = reqwest::Proxy::all(proxy_url.clone()) + .map_err(|err| e!(CaptivePortalError::CreateReqwestClient, err))?; + builder = builder.proxy(proxy); + } + + let client = builder + .build() + .map_err(|err| e!(CaptivePortalError::CreateReqwestClient, err))?; + + // Note: the set of valid characters in a challenge and the total + // length is limited; see is_challenge_char in bin/iroh-relay for more + // details. + + let host_name = url.host_str().unwrap_or_default(); + let challenge = format!("ts_{host_name}"); + let portal_url = format!("http://{host_name}/generate_204"); + let res = client + .request(reqwest::Method::GET, portal_url) + .header("X-Iroh-Challenge", &challenge) + .send() + .await + .map_err(|err| e!(CaptivePortalError::HttpRequest, err))?; + + let expected_response = format!("response {challenge}"); + let is_valid_response = res + .headers() + .get("X-Iroh-Response") + .map(|s| s.to_str().unwrap_or_default()) + == Some(&expected_response); + + trace!( + "check_captive_portal url={} status_code={} valid_response={}", + res.url(), + res.status(), + is_valid_response, + ); + let has_captive = res.status() != 204 || !is_valid_response; + + Ok(has_captive) +} + +/// Returns the proper port based on the protocol of the probe. +#[cfg(not(wasm_browser))] +fn get_quic_port(relay: &RelayConfig) -> Option { + if let Some(ref quic) = relay.quic { + if quic.port == 0 { + Some(DEFAULT_RELAY_QUIC_PORT) + } else { + Some(quic.port) + } + } else { + None + } +} + +#[cfg(not(wasm_browser))] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub(super) enum GetRelayAddrError { + #[error("No valid hostname in the relay URL")] + InvalidHostname, + #[error("No suitable relay address found for {url} ({addr_type})")] + NoAddrFound { + url: RelayUrl, + addr_type: &'static str, + }, + #[error("DNS lookup failed")] + DnsLookup { source: StaggeredError }, + #[error("Relay is not suitable")] + UnsupportedRelay, + #[error("HTTPS probes are not implemented")] + UnsupportedHttps, + #[error("No port available for this protocol")] + MissingPort, +} + +/// Returns the IP address to use to communicate to this relay for quic. +#[cfg(not(wasm_browser))] +pub(super) async fn get_relay_addr_ipv4( + dns_resolver: &DnsResolver, + relay: &RelayConfig, +) -> Result { + let port = get_quic_port(relay).ok_or_else(|| e!(GetRelayAddrError::MissingPort))?; + relay_lookup_ipv4_staggered(dns_resolver, relay, port).await +} + +#[cfg(not(wasm_browser))] +pub(super) async fn get_relay_addr_ipv6( + dns_resolver: &DnsResolver, + relay: &RelayConfig, +) -> Result { + let port = get_quic_port(relay).ok_or_else(|| e!(GetRelayAddrError::MissingPort))?; + relay_lookup_ipv6_staggered(dns_resolver, relay, port).await +} + +/// Do a staggared ipv4 DNS lookup based on [`RelayConfig`] +/// +/// `port` is combined with the resolved [`std::net::Ipv4Addr`] to return a [`SocketAddr`] +#[cfg(not(wasm_browser))] +async fn relay_lookup_ipv4_staggered( + dns_resolver: &DnsResolver, + relay: &RelayConfig, + port: u16, +) -> Result { + match relay.url.host() { + Some(url::Host::Domain(hostname)) => { + trace!(%hostname, "Performing DNS A lookup for relay addr"); + match dns_resolver + .lookup_ipv4_staggered(hostname, DNS_TIMEOUT, DNS_STAGGERING_MS) + .await + { + Ok(mut addrs) => addrs + .next() + .map(|ip| ip.to_canonical()) + .map(|addr| match addr { + IpAddr::V4(ip) => SocketAddrV4::new(ip, port), + IpAddr::V6(_) => unreachable!("bad DNS lookup: {:?}", addr), + }) + .ok_or_else(|| { + e!(GetRelayAddrError::NoAddrFound { + url: relay.url.clone(), + addr_type: "A", + }) + }), + Err(err) => Err(e!(GetRelayAddrError::DnsLookup, err)), + } + } + Some(url::Host::Ipv4(addr)) => Ok(SocketAddrV4::new(addr, port)), + Some(url::Host::Ipv6(_addr)) => Err(e!(GetRelayAddrError::NoAddrFound { + url: relay.url.clone(), + addr_type: "A", + })), + None => Err(e!(GetRelayAddrError::InvalidHostname)), + } +} + +/// Do a staggared ipv6 DNS lookup based on [`RelayConfig`] +/// +/// `port` is combined with the resolved [`std::net::Ipv6Addr`] to return a [`SocketAddr`] +#[cfg(not(wasm_browser))] +async fn relay_lookup_ipv6_staggered( + dns_resolver: &DnsResolver, + relay: &RelayConfig, + port: u16, +) -> Result { + match relay.url.host() { + Some(url::Host::Domain(hostname)) => { + trace!(%hostname, "Performing DNS AAAA lookup for relay addr"); + match dns_resolver + .lookup_ipv6_staggered(hostname, DNS_TIMEOUT, DNS_STAGGERING_MS) + .await + { + Ok(mut addrs) => addrs + .next() + .map(|addr| match addr { + IpAddr::V4(_) => unreachable!("bad DNS lookup: {:?}", addr), + IpAddr::V6(ip) => SocketAddrV6::new(ip, port, 0, 0), + }) + .ok_or_else(|| { + e!(GetRelayAddrError::NoAddrFound { + url: relay.url.clone(), + addr_type: "AAAA", + }) + }), + Err(err) => Err(e!(GetRelayAddrError::DnsLookup, err)), + } + } + Some(url::Host::Ipv4(_addr)) => Err(e!(GetRelayAddrError::NoAddrFound { + url: relay.url.clone(), + addr_type: "AAAA", + })), + Some(url::Host::Ipv6(addr)) => Ok(SocketAddrV6::new(addr, port, 0, 0)), + None => Err(e!(GetRelayAddrError::InvalidHostname)), + } +} + +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub(super) enum MeasureHttpsLatencyError { + #[error(transparent)] + InvalidUrl { + #[error(std_err, from)] + source: url::ParseError, + }, + #[cfg(not(wasm_browser))] + #[error(transparent)] + DnsLookup { + #[error(from)] + source: StaggeredError, + }, + #[error("Creating HTTP client failed")] + CreateReqwestClient { + #[error(std_err)] + source: reqwest::Error, + }, + #[error("HTTP request failed")] + HttpRequest { + #[error(std_err)] + source: reqwest::Error, + }, + #[error("Error response from server {status}: {:?}", status.canonical_reason())] + InvalidResponse { status: StatusCode }, +} + +/// Executes an HTTPS probe. +/// +/// If `certs` is provided they will be added to the trusted root certificates, allowing the +/// use of self-signed certificates for servers. Currently this is used for testing. +#[allow(clippy::unused_async)] +async fn run_https_probe( + #[cfg(not(wasm_browser))] dns_resolver: &DnsResolver, + relay: RelayUrl, + #[cfg(not(wasm_browser))] tls_config: rustls::ClientConfig, + #[cfg(not(wasm_browser))] proxy_url: Option<&Url>, +) -> Result { + trace!("HTTPS probe start"); + let url = relay.join(RELAY_PROBE_PATH)?; + + // This should also use same connection establishment as relay client itself, which + // needs to be more configurable so users can do more crazy things: + // https://github.com/n0-computer/iroh/issues/2901 + #[cfg(not(wasm_browser))] + let mut builder = reqwest_client_builder(tls_config, dns_resolver.clone()) + .redirect(reqwest::redirect::Policy::none()); + #[cfg(wasm_browser)] + let builder = reqwest_client_builder(); + + #[cfg(not(wasm_browser))] + if let Some(proxy_url) = proxy_url { + let proxy = reqwest::Proxy::all(proxy_url.clone()) + .map_err(|err| e!(MeasureHttpsLatencyError::CreateReqwestClient, err))?; + builder = builder.proxy(proxy); + } + + let client = builder + .build() + .map_err(|err| e!(MeasureHttpsLatencyError::CreateReqwestClient, err))?; + + let start = Instant::now(); + let response = client + .request(reqwest::Method::GET, url) + .send() + .await + .map_err(|err| e!(MeasureHttpsLatencyError::HttpRequest, err))?; + let latency = start.elapsed(); + if response.status().is_success() { + // Drain the response body to be nice to the server, up to a limit. + const MAX_BODY_SIZE: usize = 8 << 10; // 8 KiB + let mut body_size = 0; + let mut stream = response.bytes_stream(); + // ignore failing frames + while let Some(Ok(chunk)) = stream.next().await { + body_size += chunk.len(); + if body_size >= MAX_BODY_SIZE { + break; + } + } + + Ok(HttpsProbeReport { relay, latency }) + } else { + Err(e!(MeasureHttpsLatencyError::InvalidResponse { + status: response.status() + })) + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::net::Ipv4Addr; + + use iroh_dns::dns::DnsResolver; + use iroh_relay::tls::{CaTlsConfig, default_provider}; + use n0_error::{Result, StdResultExt}; + use n0_tracing_test::traced_test; + use tokio::{io::AsyncReadExt, sync::oneshot}; + + use super::{super::test_utils, *}; + + #[tokio::test] + async fn test_measure_https_latency() -> Result { + let (_server, relay) = test_utils::relay().await; + let dns_resolver = DnsResolver::new(); + tracing::info!(relay_url = ?relay.url , "RELAY_URL"); + let report = run_https_probe( + &dns_resolver, + relay.url, + CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"), + None, + ) + .await?; + + assert!(report.latency > Duration::ZERO); + + Ok(()) + } + + /// Spawns a fake HTTP proxy which captures the request line of the first connection + /// it receives and then hangs up. + /// + /// Returns the URL to configure as proxy and a receiver for the captured request line. + async fn capturing_proxy() -> Result<(Url, oneshot::Receiver)> { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .anyerr()?; + let url: Url = format!("http://{}", listener.local_addr().anyerr()?) + .parse() + .anyerr()?; + let (tx, rx) = oneshot::channel(); + task::spawn(async move { + let Ok((mut stream, _)) = listener.accept().await else { + return; + }; + let mut line = Vec::new(); + let mut byte = [0u8; 1]; + while let Ok(1) = stream.read(&mut byte).await { + if byte[0] == b'\n' { + break; + } + line.push(byte[0]); + } + tx.send(String::from_utf8_lossy(&line).trim().to_string()) + .ok(); + }); + Ok((url, rx)) + } + + #[tokio::test] + #[traced_test] + async fn test_measure_https_latency_via_proxy() -> Result { + let (_server, relay) = test_utils::relay().await; + let dns_resolver = DnsResolver::new(); + let (proxy_url, request_line) = capturing_proxy().await?; + let target = format!( + "{}:{}", + relay.url.host_str().expect("relay url has a host"), + relay.url.port().expect("relay url has a port") + ); + + // The probe cannot succeed: the fake proxy never completes the CONNECT tunnel. + // What matters is that the probe was attempted via the proxy rather than + // connecting to the relay directly. + let res = run_https_probe( + &dns_resolver, + relay.url, + CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"), + Some(&proxy_url), + ) + .await; + assert!( + res.is_err(), + "probe must not succeed through a proxy that refuses to tunnel, got {res:?}" + ); + + let request_line = time::timeout(Duration::from_secs(10), request_line) + .await + .expect("proxy did not receive a request, the probe bypassed it") + .anyerr()?; + assert!( + request_line.starts_with(&format!("CONNECT {target} ")), + "expected the probe to tunnel to {target}, proxy received: {request_line}" + ); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_qad_probe_v4() -> Result { + let (server, relay) = test_utils::relay().await; + let relay = Arc::new(relay); + let client_config = iroh_relay::tls::make_dangerous_client_config(); + let ep = noq::Endpoint::client(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)).anyerr()?; + let client_addr = ep.local_addr().anyerr()?; + + let quic_client = iroh_relay::quic::QuicClient::new(ep.clone(), client_config); + let dns_resolver = DnsResolver::default(); + + let (report, conn) = + super::super::run_probe_v4(relay, quic_client, dns_resolver, CancellationToken::new()) + .await + .unwrap(); + + assert_eq!(report.addr, client_addr); + drop(conn); + ep.wait_idle().await; + server.shutdown().await?; + Ok(()) + } +} diff --git a/vendor/iroh/src/portmapper.rs b/vendor/iroh/src/portmapper.rs new file mode 100644 index 0000000..c19c3b6 --- /dev/null +++ b/vendor/iroh/src/portmapper.rs @@ -0,0 +1,100 @@ +//! Portmapper integration. +//! +//! Wraps the real [`portmapper`] crate when the `portmapper` feature is enabled, +//! or provides a no-op stub otherwise. + +use std::net::SocketAddrV4; + +use tokio::sync::watch; + +/// Configuration for the portmapper service (UPnP, PCP, NAT-PMP). +/// +/// Port mapping asks the local router to open an external port so peers can +/// reach this endpoint directly, improving connectivity behind NATs. The +/// discovery step (UPnP uses SSDP multicast) can, however, trigger firewall +/// prompts on some networks — see [`PortmapperConfig::Disabled`]. +/// +/// Used with [`crate::endpoint::Builder::portmapper_config`]. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum PortmapperConfig { + /// Enable portmapping with default settings. + /// + /// This is the default. + #[non_exhaustive] + Enabled {}, + /// Disable portmapping. + /// + /// Skips the UPnP/PCP/NAT-PMP gateway probing. Use this to avoid the + /// SSDP multicast discovery that can raise firewall dialogs (notably on + /// macOS), at the cost of potentially worse direct connectivity behind + /// some NATs. + Disabled, +} + +impl Default for PortmapperConfig { + fn default() -> Self { + PortmapperConfig::Enabled {} + } +} + +pub(crate) fn create_client(config: &PortmapperConfig) -> Client { + match config { + #[cfg(all(not(wasm_browser), feature = "portmapper"))] + PortmapperConfig::Enabled {} => Client::Enabled(::portmapper::Client::default()), + _ => { + let (tx, rx) = watch::channel(None); + Client::Disabled { _tx: tx, rx } + } + } +} + +/// Portmapper client: either the real implementation or a no-op. +/// +/// The disabled variant is used when the `portmapper` feature is off, on wasm, +/// or when portmapping is disabled via [`PortmapperConfig::Disabled`]. +#[derive(Debug)] +pub(crate) enum Client { + /// The real portmapper client (requires the `portmapper` feature). + #[cfg(all(not(wasm_browser), feature = "portmapper"))] + Enabled(::portmapper::Client), + /// No-op: keeps the sender alive so the receiver never closes. + Disabled { + _tx: watch::Sender>, + rx: watch::Receiver>, + }, +} + +impl Client { + pub(crate) fn procure_mapping(&self) { + match self { + #[cfg(all(not(wasm_browser), feature = "portmapper"))] + Client::Enabled(c) => c.procure_mapping(), + Client::Disabled { .. } => {} + } + } + + pub(crate) fn update_local_port(&self, _port: std::num::NonZeroU16) { + match self { + #[cfg(all(not(wasm_browser), feature = "portmapper"))] + Client::Enabled(c) => c.update_local_port(_port), + Client::Disabled { .. } => {} + } + } + + pub(crate) fn deactivate(&self) { + match self { + #[cfg(all(not(wasm_browser), feature = "portmapper"))] + Client::Enabled(c) => c.deactivate(), + Client::Disabled { .. } => {} + } + } + + pub(crate) fn watch_external_address(&self) -> watch::Receiver> { + match self { + #[cfg(all(not(wasm_browser), feature = "portmapper"))] + Client::Enabled(c) => c.watch_external_address(), + Client::Disabled { rx, .. } => rx.clone(), + } + } +} diff --git a/vendor/iroh/src/protocol.rs b/vendor/iroh/src/protocol.rs new file mode 100644 index 0000000..397b87d --- /dev/null +++ b/vendor/iroh/src/protocol.rs @@ -0,0 +1,1132 @@ +//! Tools for spawning an accept loop that routes incoming requests to the right protocol. +//! +//! ## Example +//! +//! ```no_run +//! # #[cfg(with_crypto_provider)] +//! # use iroh::{ +//! # endpoint::{BindError, presets}, +//! # protocol::Router, +//! # Endpoint, +//! # }; +//! # use iroh::{ +//! # endpoint::Connection, +//! # protocol::{AcceptError, ProtocolHandler}, +//! # }; +//! # +//! # #[cfg(with_crypto_provider)] +//! # async fn test_compile() -> Result<(), BindError> { +//! let endpoint = Endpoint::bind(presets::N0).await?; +//! +//! let router = Router::builder(endpoint).accept(b"/my/alpn", Echo).spawn(); +//! # Ok(()) +//! # } +//! +//! // The protocol definition: +//! #[derive(Debug, Clone)] +//! struct Echo; +//! +//! impl ProtocolHandler for Echo { +//! async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { +//! let (mut send, mut recv) = connection.accept_bi().await?; +//! +//! // Echo any bytes received back directly. +//! let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; +//! +//! send.finish()?; +//! connection.closed().await; +//! +//! Ok(()) +//! } +//! } +//! ``` +use std::{ + collections::HashMap, + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +use n0_error::{AnyError, e, stack_error}; +use n0_future::{ + join_all, + task::{self, AbortOnDropHandle, JoinSet}, +}; +use tokio_util::sync::CancellationToken; +use tracing::{Instrument, debug, error, field::Empty, info_span, trace, warn}; + +use crate::{ + Endpoint, + endpoint::{Accepting, Connection, RemoteEndpointIdError, quic}, +}; + +/// The built router. +/// +/// Construct this using [`Router::builder`]. +/// +/// When dropped, this will abort listening the tasks, so make sure to store it. +/// +/// Even with this abort-on-drop behaviour, it's recommended to call and await +/// [`Router::shutdown`] before ending the process. +/// +/// As an example for graceful shutdown, e.g. for tests or CLI tools, +/// wait for [`tokio::signal::ctrl_c()`]: +/// +/// ```no_run +/// # #[cfg(with_crypto_provider)] +/// # { +/// # use std::sync::Arc; +/// # use n0_error::StdResultExt; +/// # use iroh::{endpoint::{Connecting, presets}, protocol::{ProtocolHandler, Router}, Endpoint, EndpointAddr}; +/// # +/// # async fn test_compile() -> n0_error::Result<()> { +/// let endpoint = Endpoint::bind(presets::N0).await?; +/// +/// let router = Router::builder(endpoint) +/// // .accept(&ALPN, ) +/// .spawn(); +/// +/// // wait until the user wants to +/// tokio::signal::ctrl_c().await.std_context("ctrl+c")?; +/// router.shutdown().await.std_context("shutdown")?; +/// # Ok(()) +/// # } +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct Router { + endpoint: Endpoint, + // `Router` needs to be `Clone + Send`, and we need to `task.await` in its `shutdown()` impl. + task: Arc>>>, + cancel_token: CancellationToken, +} + +/// Builder for creating a [`Router`] for accepting protocols. +#[derive(derive_more::Debug)] +pub struct RouterBuilder { + endpoint: Endpoint, + protocols: ProtocolMap, + #[debug(skip)] + incoming_filter: Option, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta, from_sources, std_sources)] +#[non_exhaustive] +pub enum AcceptError { + #[error(transparent)] + Connecting { + source: crate::endpoint::ConnectingError, + }, + #[error(transparent)] + Connection { + source: crate::endpoint::ConnectionError, + }, + #[error(transparent)] + MissingRemoteEndpointId { source: RemoteEndpointIdError }, + #[error("Not allowed.")] + NotAllowed {}, + #[error(transparent)] + User { source: AnyError }, +} + +impl AcceptError { + /// Creates a new user error from an arbitrary error type. + // TODO(Frando): Rename to `from_std` + #[track_caller] + pub fn from_err(value: T) -> Self { + e!(AcceptError::User { + source: AnyError::from_std(value) + }) + } + + /// Creates a new user error from an arbitrary boxed error. + #[track_caller] + pub fn from_boxed(value: Box) -> Self { + e!(AcceptError::User { + source: AnyError::from_std_box(value) + }) + } +} + +impl From for AcceptError { + fn from(err: std::io::Error) -> Self { + Self::from_err(err) + } +} + +impl From for AcceptError { + fn from(err: quic::ClosedStream) -> Self { + Self::from_err(err) + } +} + +/// Verdict from a [`IncomingFilter`] for an incoming connection. +/// +/// The filter can accept the connection, send a retry token to validate +/// the source address, actively refuse the connection, or silently drop it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum IncomingFilterOutcome { + /// Accept the connection. + Accept, + /// Tell the remote to retry with a token (a QUIC `RETRY` packet). + /// + /// What this does depends on the connection type: + /// + /// - **Direct (UDP) connections**: this is QUIC source address + /// validation. If the socket address was spoofed, the retry token is + /// sent to the spoofed address, so we never hear from the attacker + /// again. If the address was real, the client repeats the connection + /// attempt with the token, and the next [`Incoming`] for that flow has + /// [`Incoming::remote_addr_validated`] set to `true`. The token is + /// bound to the source address. + /// + /// - **Relay connections**: there is no source address to validate + /// (the relay already vouches for the packet origin), so the + /// "validation" itself has no security meaning. However, the retry + /// still imposes a real cost on the client: an extra round trip + /// through the relay plus the work of sending a fresh ClientHello + /// with the token. This filters out adversarial clients that + /// don't bother to handle retry tokens, and adds latency before + /// we get to the more expensive part of the handshake. The next + /// [`Incoming`] for that flow will also have + /// [`Incoming::remote_addr_validated`] set to `true`, but again, + /// that just means the client cooperated with the retry. + /// + /// In short: for direct connections, `Retry` is address validation; + /// for relay connections, it is a cost-imposition mechanism. + /// + /// [`Incoming`]: crate::endpoint::Incoming + /// [`Incoming::remote_addr_validated`]: crate::endpoint::Incoming::remote_addr_validated + Retry, + /// Actively refuse the connection. The remote will receive a + /// CONNECTION_REFUSED error immediately. + Reject, + /// Ignore the connection entirely. The remote gets no response and will + /// eventually time out. + Ignore, +} + +/// Filter predicate used for early filtering of incoming connections before the handshake completes. +/// +/// See [`RouterBuilder::incoming_filter`] for more details. +pub type IncomingFilter = + Arc IncomingFilterOutcome + Send + Sync + 'static>; + +/// Handler for incoming connections. +/// +/// A router accepts connections for arbitrary ALPN protocols. +/// +/// With this trait, you can handle incoming connections for any protocol. +/// +/// Implement this trait on a struct that should handle incoming connections. +/// The protocol handler must then be registered on the endpoint for an ALPN protocol with +/// [`crate::protocol::RouterBuilder::accept`]. +/// +/// See the [module documentation](crate::protocol) for an example. +pub trait ProtocolHandler: Send + Sync + std::fmt::Debug + 'static { + /// Optional interception point to handle the `Accepting` state. + /// + /// Can be implemented as `async fn on_accepting(&self, accepting: Accepting) -> Result`. + /// + /// Typically, this method is used as an early interception point to accept + /// or reject a connection. + /// + /// However, this method can also be used to implement the accept side of a + /// 0-RTT connection. + /// + /// ## 0-RTT + /// + /// `ProtocolHandler::on_accepting` allows you to take over the connection + /// state machine early in the handshake processes, by calling [`Accepting::into_0rtt`]. + /// + /// When working with 0-RTT, you may want to implement all of your protocol + /// logic in `on_accepting`. This is fine because `on_accepting` can handle + /// long-running processes. In this case, the [`ProtocolHandler::accept`] method + /// can simply return `Ok(())`. + fn on_accepting( + &self, + accepting: Accepting, + ) -> impl Future> + Send { + async move { + let conn = accepting.await?; + Ok(conn) + } + } + + /// Handle an incoming connection. + /// + /// Can be implemented as `async fn accept(&self, connection: Connection) -> Result<()>`. + /// + /// The returned future runs on a freshly spawned tokio task so it can be long-running. Once + /// `accept()` returns, the connection is dropped. This means that it will be closed + /// if there are no other clones of the connection. If there is a protocol error, you + /// can use [`Connection::close`] to send an error code to the remote peer. Returning + /// an `Err` will also drop the connection and log a warning, but no + /// dedicated error code will be sent to the peer, so it's recommended to explicitly + /// close the connection within your accept handler. + /// + /// When [`Router::shutdown`] is called, no further connections will be accepted, and + /// the futures returned by [`Self::accept`] will be aborted after the future returned + /// from [`ProtocolHandler::shutdown`] completes. + fn accept( + &self, + connection: Connection, + ) -> impl Future> + Send; + + /// Called when the router shuts down. + /// + /// Can be implemented as `async fn shutdown(&self)`. + /// + /// This is called from [`Router::shutdown`]. The returned future is awaited before + /// the router closes the endpoint. + fn shutdown(&self) -> impl Future + Send { + async move {} + } +} + +impl ProtocolHandler for Arc { + async fn on_accepting(&self, accepting: Accepting) -> Result { + self.as_ref().on_accepting(accepting).await + } + + async fn accept(&self, conn: Connection) -> Result<(), AcceptError> { + self.as_ref().accept(conn).await + } + + async fn shutdown(&self) { + self.as_ref().shutdown().await + } +} + +impl ProtocolHandler for Box { + async fn on_accepting(&self, accepting: Accepting) -> Result { + self.as_ref().on_accepting(accepting).await + } + + async fn accept(&self, conn: Connection) -> Result<(), AcceptError> { + self.as_ref().accept(conn).await + } + + async fn shutdown(&self) { + self.as_ref().shutdown().await + } +} + +impl From for Box { + fn from(value: T) -> Self { + Box::new(value) + } +} + +/// A dyn-compatible version of [`ProtocolHandler`] that returns boxed futures. +/// +/// Any type that implements [`ProtocolHandler`] automatically also implements [`DynProtocolHandler`]. +/// There is a also [`From`] impl to turn any type that implements [`ProtocolHandler`] into a +/// `Box`. +// +// We are not using [`n0_future::boxed::BoxFuture] because we don't need a `'static` bound +// on these futures. +pub trait DynProtocolHandler: Send + Sync + std::fmt::Debug + 'static { + /// See [`ProtocolHandler::on_accepting`]. + fn on_accepting( + &self, + accepting: Accepting, + ) -> Pin> + Send + '_>> { + Box::pin(async move { + let conn = accepting.await?; + Ok(conn) + }) + } + + /// See [`ProtocolHandler::accept`]. + fn accept( + &self, + connection: Connection, + ) -> Pin> + Send + '_>>; + + /// See [`ProtocolHandler::shutdown`]. + fn shutdown(&self) -> Pin + Send + '_>> { + Box::pin(async move {}) + } +} + +impl DynProtocolHandler for P { + fn accept( + &self, + connection: Connection, + ) -> Pin> + Send + '_>> { + Box::pin(::accept(self, connection)) + } + + fn on_accepting( + &self, + accepting: Accepting, + ) -> Pin> + Send + '_>> { + Box::pin(::on_accepting(self, accepting)) + } + + fn shutdown(&self) -> Pin + Send + '_>> { + Box::pin(::shutdown(self)) + } +} + +/// A typed map of protocol handlers, mapping them from ALPNs. +#[derive(Debug, Default)] +pub(crate) struct ProtocolMap { + /// List of ALPNs in insertion order. + alpns: Vec>, + /// Map of protocol handlers by ALPN. + map: HashMap, Box>, +} + +impl ProtocolMap { + /// Returns the registered protocol handler for an ALPN as a [`Arc`]. + pub(crate) fn get(&self, alpn: &[u8]) -> Option<&dyn DynProtocolHandler> { + self.map.get(alpn).map(|p| &**p) + } + + /// Inserts a protocol handler. + pub(crate) fn insert(&mut self, alpn: Vec, handler: Box) { + self.alpns.push(alpn.clone()); + self.map.insert(alpn, handler); + } + + /// Returns an iterator of all registered ALPN protocol identifiers. + pub(crate) fn alpns(&self) -> impl Iterator> { + self.alpns.iter() + } + + /// Shuts down all protocol handlers. + /// + /// Calls and awaits [`ProtocolHandler::shutdown`] for all registered handlers concurrently. + pub(crate) async fn shutdown(&self) { + let handlers = self.map.values().map(|p| p.shutdown()); + join_all(handlers).await; + } +} + +impl Router { + /// Creates a new [`Router`] using given [`Endpoint`]. + pub fn builder(endpoint: Endpoint) -> RouterBuilder { + RouterBuilder::new(endpoint) + } + + /// Returns the [`Endpoint`] stored in this router. + pub fn endpoint(&self) -> &Endpoint { + &self.endpoint + } + + /// Checks if the router is already shutdown. + pub fn is_shutdown(&self) -> bool { + self.cancel_token.is_cancelled() + } + + /// Shuts down the accept loop cleanly. + /// + /// When this function returns, all [`ProtocolHandler`]s will be shutdown and + /// `Endpoint::close` will have been called. + /// + /// If already shutdown, it returns `Ok`. + /// + /// If some [`ProtocolHandler`] panicked in the accept loop, this will propagate + /// that panic into the result here. + pub async fn shutdown(&self) -> Result<(), n0_future::task::JoinError> { + if self.is_shutdown() { + return Ok(()); + } + + // Trigger shutdown of the main run task by activating the cancel token. + self.cancel_token.cancel(); + + // Wait for the main task to terminate. + + // MutexGuard is not held across await point + let task = self.task.lock().expect("poisoned").take(); + if let Some(task) = task { + task.await?; + } + + Ok(()) + } +} + +impl RouterBuilder { + /// Creates a new router builder using given [`Endpoint`]. + pub fn new(endpoint: Endpoint) -> Self { + Self { + endpoint, + protocols: ProtocolMap::default(), + incoming_filter: None, + } + } + + /// Sets a filter that decides whether to accept an incoming connection before the + /// TLS handshake completes. + /// + /// The filter is called with the raw [`Incoming`] for each connection attempt + /// and returns an [`IncomingFilterOutcome`] that determines what happens next. + /// + /// Implementers have full access to the [`Incoming`] and can use any of its + /// methods (including [`Incoming::decrypt`]) to make their decision. Note that + /// `decrypt()` is relatively expensive, so filters should reject based on + /// cheaper signals (e.g. remote address) first. + /// + /// [`Incoming`]: crate::endpoint::Incoming + /// [`Incoming::decrypt`]: crate::endpoint::Incoming::decrypt + pub fn incoming_filter(mut self, filter: IncomingFilter) -> Self { + self.incoming_filter = Some(filter); + self + } + + /// Configures the router to accept the [`ProtocolHandler`] when receiving a connection + /// with this `alpn`. + /// + /// `handler` can either be a type that implements [`ProtocolHandler`] or a + /// [`Box`]. + /// + /// The protocols registered on the router are passed to [`Endpoint::set_alpns`] in the order + /// of the calls to [`Self::accept`]. Ordering matters for protocol negotiation. When an incoming + /// connection offers multiple ALPNs, the first matching ALPN will be chosen. This means that + /// your preferred protocol should be registered first on the router builder. + /// + /// [`Box`]: DynProtocolHandler + pub fn accept( + mut self, + alpn: impl AsRef<[u8]>, + handler: impl Into>, + ) -> Self { + self.protocols + .insert(alpn.as_ref().to_vec(), handler.into()); + self + } + + /// Returns the [`Endpoint`] stored in this builder. + pub fn endpoint(&self) -> &Endpoint { + &self.endpoint + } + + /// Spawns an accept loop and returns a handle to it encapsulated as the [`Router`]. + #[must_use = "Router aborts when dropped, use Router::shutdown to shut the router down cleanly"] + pub fn spawn(self) -> Router { + // Update the endpoint with our alpns. + let alpns = self + .protocols + .alpns() + .map(|alpn| alpn.to_vec()) + .collect::>(); + + let protocols = Arc::new(self.protocols); + let incoming_filter = self.incoming_filter; + self.endpoint.set_alpns(alpns); + + let mut join_set = JoinSet::new(); + let endpoint = self.endpoint.clone(); + + // Our own shutdown works with a cancellation token. + let cancel = CancellationToken::new(); + let cancel_token = cancel.clone(); + + let run_loop_fut = async move { + // Make sure to cancel the token, if this future ever exits. + let _cancel_guard = cancel_token.clone().drop_guard(); + // We create a separate cancellation token to stop any `ProtocolHandler::accept` futures + // that are still running after `ProtocolHandler::shutdown` was called. + let handler_cancel_token = CancellationToken::new(); + + loop { + tokio::select! { + biased; + _ = cancel_token.cancelled() => { + break; + }, + // handle task terminations and quit on panics. + Some(res) = join_set.join_next() => { + match res { + Err(outer) => { + if outer.is_panic() { + error!("Task panicked: {outer:?}"); + break; + } else if outer.is_cancelled() { + trace!("Task cancelled: {outer:?}"); + } else { + error!("Task failed: {outer:?}"); + break; + } + } + Ok(Some(())) => { + trace!("Task finished"); + } + Ok(None) => { + trace!("Task cancelled"); + } + } + }, + + // handle incoming p2p connections. + incoming = endpoint.accept() => { + let Some(incoming) = incoming else { + break; // Endpoint is closed. + }; + + if let Some(filter) = &incoming_filter { + match filter(&incoming) { + IncomingFilterOutcome::Accept => {} + IncomingFilterOutcome::Retry => { + if incoming.remote_addr_validated() { + debug!( + "filter returned Retry for an already validated connection", + ); + } + if let Err(err) = incoming.retry() { + err.into_incoming().refuse(); + } + continue; + } + IncomingFilterOutcome::Reject => { + incoming.refuse(); + continue; + } + IncomingFilterOutcome::Ignore => { + incoming.ignore(); + continue; + } + } + } + + let protocols = protocols.clone(); + let token = handler_cancel_token.child_token(); + let span = info_span!("router.accept", me=%endpoint.id().fmt_short(), remote=Empty, alpn=Empty); + join_set.spawn(async move { + token.run_until_cancelled(handle_connection(incoming, protocols)).await + }.instrument(span)); + }, + } + } + + // We first shutdown the protocol handlers to give them a chance to close connections gracefully. + protocols.shutdown().await; + // We now cancel the remaining `ProtocolHandler::accept` futures. + handler_cancel_token.cancel(); + // Now we close the endpoint. This will force-close all connections that are not yet closed. + endpoint.close().await; + // Finally, we abort the remaining accept tasks. This should be a noop because we already cancelled + // the futures above. + tracing::debug!("Shutting down remaining tasks"); + join_set.abort_all(); + while let Some(res) = join_set.join_next().await { + match res { + Err(err) if err.is_panic() => error!("Task panicked: {err:?}"), + _ => {} + } + } + }; + let task = task::spawn(run_loop_fut.instrument(tracing::Span::current())); + let task = AbortOnDropHandle::new(task); + + Router { + endpoint: self.endpoint, + task: Arc::new(Mutex::new(Some(task))), + cancel_token: cancel, + } + } +} + +async fn handle_connection(incoming: crate::endpoint::Incoming, protocols: Arc) { + let mut accepting = match incoming.accept() { + Ok(conn) => conn, + Err(err) => { + warn!("Ignoring connection: accepting failed: {err:#}"); + return; + } + }; + let alpn = match accepting.alpn().await { + Ok(alpn) => alpn, + Err(err) => { + warn!("Ignoring connection: invalid handshake: {err:#}"); + return; + } + }; + tracing::Span::current().record("alpn", String::from_utf8_lossy(&alpn).to_string()); + + let Some(handler) = protocols.get(&alpn) else { + warn!("Ignoring connection: unsupported ALPN protocol"); + return; + }; + match handler.on_accepting(accepting).await { + Ok(connection) => { + tracing::Span::current().record( + "remote", + tracing::field::display(connection.remote_id().fmt_short()), + ); + + if let Err(err) = handler.accept(connection).await { + warn!("Handling incoming connection ended with error: {err}"); + } + } + Err(err) => { + warn!("Accepting incoming connection ended with error: {err}"); + } + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::{sync::Mutex, time::Duration}; + + use n0_error::{Result, StdResultExt}; + use n0_tracing_test::traced_test; + + use super::*; + use crate::endpoint::{ + ApplicationClose, BeforeConnectOutcome, ConnectError, ConnectOptions, ConnectWithOptsError, + ConnectionError, EndpointHooks, presets, + }; + + #[tokio::test] + async fn test_shutdown() -> Result { + let endpoint = Endpoint::bind(presets::Minimal).await?; + let router = Router::builder(endpoint.clone()).spawn(); + + assert!(!router.is_shutdown()); + assert!(!endpoint.is_closed()); + + router.shutdown().await.anyerr()?; + + assert!(router.is_shutdown()); + assert!(endpoint.is_closed()); + + Ok(()) + } + + // The protocol definition: + #[derive(Debug, Clone)] + struct Echo; + + const ECHO_ALPN: &[u8] = b"/iroh/echo/1"; + + impl ProtocolHandler for Echo { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + println!("accepting echo"); + let (mut send, mut recv) = connection.accept_bi().await?; + + // Echo any bytes received back directly. + let _bytes_sent = tokio::io::copy(&mut recv, &mut send).await?; + + send.finish()?; + connection.closed().await; + + Ok(()) + } + } + + #[tokio::test] + async fn test_limiter_hook() -> Result { + // tracing_subscriber::fmt::try_init().ok(); + #[derive(Debug, Default)] + struct LimitHook; + impl EndpointHooks for LimitHook { + async fn before_connect<'a>( + &'a self, + _remote_addr: &'a iroh_base::EndpointAddr, + alpn: &'a [u8], + ) -> BeforeConnectOutcome { + assert_eq!(alpn, ECHO_ALPN); + + // deny all access + BeforeConnectOutcome::Reject + } + } + + let e1 = Endpoint::bind(presets::Minimal).await?; + + let r1 = Router::builder(e1.clone()).accept(ECHO_ALPN, Echo).spawn(); + + let addr1 = r1.endpoint().addr(); + dbg!(&addr1); + let e2 = Endpoint::builder(presets::Minimal) + .hooks(LimitHook) + .bind() + .await?; + + println!("connecting"); + let conn_err = e2.connect(addr1, ECHO_ALPN).await.unwrap_err(); + + assert!(matches!( + conn_err, + ConnectError::Connect { + source: ConnectWithOptsError::LocallyRejected { .. }, + .. + } + )); + + r1.shutdown().await.anyerr()?; + e2.close().await; + + Ok(()) + } + + /// Test that `Accepting::remote_addr()` is consistent with `Incoming::remote_addr()`. + #[tokio::test] + #[traced_test] + async fn test_accepting_remote_addr() -> Result { + use crate::endpoint::{IncomingAddr, presets}; + + let e1 = Endpoint::builder(presets::Minimal) + .alpns(vec![ECHO_ALPN.to_vec()]) + .bind() + .await?; + let addr1 = e1.addr(); + + let e2 = Endpoint::bind(presets::Minimal).await?; + + // Spawn the client connect so it runs concurrently with accept. + let connect_task = tokio::spawn({ + let addr1 = addr1.clone(); + let e2 = e2.clone(); + async move { e2.connect(addr1, ECHO_ALPN).await } + }); + + let incoming = e1.accept().await.expect("accept"); + let incoming_addr = incoming.remote_addr(); + assert!(matches!(incoming_addr, IncomingAddr::Ip(_))); + + let accepting = incoming.accept().anyerr()?; + assert_eq!(incoming_addr, accepting.remote_addr()); + + // Clean up. + drop(accepting); + drop(connect_task); + e1.close().await; + e2.close().await; + Ok(()) + } + + mod incoming_filter { + use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering::Relaxed}, + }, + time::Duration, + }; + + use n0_error::{Result, StdResultExt}; + use n0_tracing_test::traced_test; + + use crate::{ + Endpoint, EndpointAddr, + endpoint::presets, + protocol::{ + IncomingFilterOutcome, Router, + tests::{ECHO_ALPN, Echo}, + }, + }; + + /// Two direct endpoints with a filtered router on the first. + /// + /// Binds to IPv4 loopback only so retry-token validation works on + /// multi-homed CI hosts (tokens are tied to the source address). + async fn direct_pair(filter: F) -> Result<(Router, Endpoint, EndpointAddr)> + where + F: Fn(&crate::endpoint::Incoming) -> IncomingFilterOutcome + Send + Sync + 'static, + { + let e1 = Endpoint::builder(presets::Minimal) + .clear_ip_transports() + .bind_addr((std::net::Ipv4Addr::LOCALHOST, 0)) + .anyerr()? + .bind() + .await?; + let r1 = Router::builder(e1.clone()) + .incoming_filter(Arc::new(filter)) + .accept(ECHO_ALPN, Echo) + .spawn(); + let addr = r1.endpoint().addr(); + let e2 = Endpoint::builder(presets::Minimal) + .clear_ip_transports() + .bind_addr((std::net::Ipv4Addr::LOCALHOST, 0)) + .anyerr()? + .bind() + .await?; + Ok((r1, e2, addr)) + } + + /// Two relay-only endpoints with a filtered router on the first. + async fn relay_pair( + filter: F, + ) -> Result<(Router, Endpoint, EndpointAddr, impl std::any::Any)> + where + F: Fn(&crate::endpoint::Incoming) -> IncomingFilterOutcome + Send + Sync + 'static, + { + let (_relay_map, relay_url, guard) = + crate::test_utils::run_relay_server().await.anyerr()?; + let relay_mode = crate::RelayMode::Custom(crate::RelayMap::from(relay_url.clone())); + + let e1 = Endpoint::builder(presets::Minimal) + .relay_mode(relay_mode.clone()) + .ca_tls_config(crate::tls::CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + let r1 = Router::builder(e1.clone()) + .incoming_filter(Arc::new(filter)) + .accept(ECHO_ALPN, Echo) + .spawn(); + let addr = EndpointAddr::new(e1.id()).with_relay_url(relay_url); + let e2 = Endpoint::builder(presets::Minimal) + .relay_mode(relay_mode) + .ca_tls_config(crate::tls::CaTlsConfig::insecure_skip_verify()) + .bind() + .await?; + Ok((r1, e2, addr, guard)) + } + + #[tokio::test] + #[traced_test] + async fn addr_retry() -> Result { + let (r1, e2, addr) = direct_pair(|incoming| { + if !incoming.remote_addr_validated() { + IncomingFilterOutcome::Retry + } else { + IncomingFilterOutcome::Accept + } + }) + .await?; + // Server sends retry (unvalidated), then accepts once validated. + assert!(e2.connect(addr, ECHO_ALPN).await.is_ok()); + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn addr_reject() -> Result { + let (r1, e2, addr) = direct_pair(|_| IncomingFilterOutcome::Reject).await?; + assert!(e2.connect(addr, ECHO_ALPN).await.is_err()); + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn addr_ignore() -> Result { + let (r1, e2, addr) = direct_pair(|_| IncomingFilterOutcome::Ignore).await?; + // No response at all — connect times out. + let result = + tokio::time::timeout(Duration::from_millis(500), e2.connect(addr, ECHO_ALPN)).await; + assert!(result.is_err(), "expected timeout"); + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn relay_reject() -> Result { + let (r1, e2, addr, _guard) = relay_pair(|_| IncomingFilterOutcome::Reject).await?; + assert!(e2.connect(addr, ECHO_ALPN).await.is_err()); + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn relay_ignore() -> Result { + let (r1, e2, addr, _guard) = relay_pair(|_| IncomingFilterOutcome::Ignore).await?; + let result = + tokio::time::timeout(Duration::from_millis(500), e2.connect(addr, ECHO_ALPN)).await; + assert!(result.is_err(), "expected timeout"); + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + + /// Verify that returning `Retry` for a direct connection causes the + /// remote to retry with a token, after which `validated` is true. + #[tokio::test] + #[traced_test] + async fn addr_retry_then_validated() -> Result { + let saw_validated = Arc::::default(); + let saw_unvalidated = Arc::::default(); + let (sv, su) = (saw_validated.clone(), saw_unvalidated.clone()); + + let (r1, e2, addr) = direct_pair(move |incoming| { + if incoming.remote_addr_validated() { + sv.store(true, Relaxed); + IncomingFilterOutcome::Accept + } else { + su.store(true, Relaxed); + IncomingFilterOutcome::Retry + } + }) + .await?; + + // The connection should now succeed: first attempt returns Retry, + // the client retries with the token, the second attempt is + // validated and accepted. + let _conn = e2.connect(addr, ECHO_ALPN).await?; + + assert!(saw_unvalidated.load(Relaxed)); + assert!(saw_validated.load(Relaxed)); + + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + + /// Verify that returning `Retry` for a relay connection also causes + /// the remote to retry with a token. The "validation" has no + /// security meaning over a relay, but it does impose a round-trip + /// cost on the client. + #[tokio::test] + #[traced_test] + async fn relay_retry_then_validated() -> Result { + let saw_validated = Arc::::default(); + let saw_unvalidated = Arc::::default(); + let (sv, su) = (saw_validated.clone(), saw_unvalidated.clone()); + + let (r1, e2, addr, _guard) = relay_pair(move |incoming| { + if incoming.remote_addr_validated() { + sv.store(true, Relaxed); + IncomingFilterOutcome::Accept + } else { + su.store(true, Relaxed); + IncomingFilterOutcome::Retry + } + }) + .await?; + + let _conn = e2.connect(addr, ECHO_ALPN).await?; + + assert!( + saw_unvalidated.load(Relaxed), + "expected unvalidated incoming" + ); + assert!( + saw_validated.load(Relaxed), + "expected validated incoming after retry" + ); + + r1.shutdown().await.anyerr()?; + e2.close().await; + Ok(()) + } + } + + #[tokio::test] + async fn test_graceful_shutdown() -> Result { + #[derive(Debug, Clone, Default)] + struct TestProtocol { + connections: Arc>>, + } + + const TEST_ALPN: &[u8] = b"/iroh/test/1"; + + impl ProtocolHandler for TestProtocol { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + self.connections.lock().expect("poisoned").push(connection); + Ok(()) + } + + async fn shutdown(&self) { + tokio::time::sleep(Duration::from_millis(100)).await; + let mut connections = self.connections.lock().expect("poisoned"); + for conn in connections.drain(..) { + conn.close(42u32.into(), b"shutdown"); + } + } + } + + eprintln!("creating ep1"); + let endpoint = Endpoint::bind(presets::Minimal).await?; + let router = Router::builder(endpoint) + .accept(TEST_ALPN, TestProtocol::default()) + .spawn(); + eprintln!("waiting for endpoint addr"); + let addr = router.endpoint().addr(); + + eprintln!("creating ep2"); + let endpoint2 = Endpoint::bind(presets::Minimal).await?; + eprintln!("connecting to {addr:?}"); + let conn = endpoint2.connect(addr, TEST_ALPN).await?; + + eprintln!("starting shutdown"); + router.shutdown().await.anyerr()?; + + eprintln!("waiting for closed conn"); + let reason = conn.closed().await; + assert_eq!( + reason, + ConnectionError::ApplicationClosed(ApplicationClose { + error_code: 42u32.into(), + reason: b"shutdown".to_vec().into() + }) + ); + Ok(()) + } + + #[tokio::test] + async fn protocol_negotiation() -> n0_error::Result<()> { + // We define two ALPNs. + const ALPN_1: &[u8] = b"myproto/1"; + const ALPN_2: &[u8] = b"myproto/2"; + + #[derive(Debug, Clone)] + struct Handler1; + + #[derive(Debug, Clone)] + struct Handler2; + + impl ProtocolHandler for Handler1 { + async fn accept(&self, _connection: Connection) -> Result<(), AcceptError> { + Ok(()) + } + } + + impl ProtocolHandler for Handler2 { + async fn accept(&self, _connection: Connection) -> Result<(), AcceptError> { + Ok(()) + } + } + + let server = Endpoint::bind(presets::N0).await?; + let server_addr = server.addr(); + + let router = Router::builder(server) + // Ordering is preferred-first. + .accept(ALPN_2, Handler2) + .accept(ALPN_1, Handler1) + .spawn(); + + let client = Endpoint::bind(presets::N0).await?; + + // We expect ALPN_2 to be negotiated. + let conn = client + .connect_with_opts( + server_addr.clone(), + ALPN_1, + ConnectOptions::new().with_additional_alpns(vec![ALPN_2.to_vec()]), + ) + .await? + .await?; + assert_eq!(conn.alpn(), ALPN_2); + + // Ordering is server-side, so this must yield ALPN_2 as well. + let conn = client + .connect_with_opts( + server_addr, + ALPN_2, + ConnectOptions::new().with_additional_alpns(vec![ALPN_1.to_vec()]), + ) + .await? + .await?; + assert_eq!(conn.alpn(), ALPN_2); + + client.close().await; + router.shutdown().await.unwrap(); + Ok(()) + } +} diff --git a/vendor/iroh/src/runtime.rs b/vendor/iroh/src/runtime.rs new file mode 100644 index 0000000..3cb842f --- /dev/null +++ b/vendor/iroh/src/runtime.rs @@ -0,0 +1,135 @@ +use std::pin::Pin; + +use iroh_base::EndpointId; +use portable_atomic::{AtomicU64, Ordering}; +use tokio_util::sync::CancellationToken; +#[cfg(not(wasm_browser))] +use tokio_util::task::TaskTracker; + +#[derive(Debug)] +pub(crate) struct Runtime { + id: EndpointId, + #[cfg(not(wasm_browser))] + tasks: TaskTracker, + #[cfg(not(wasm_browser))] + cancel: CancellationToken, + #[cfg(not(wasm_browser))] + task_counter: AtomicU64, +} + +impl Runtime { + /// Create a new [`Runtime`] that manages shutting down tasks properly, + /// whether gracefully or un-gracefully. + pub(crate) fn new(id: EndpointId) -> Self { + Self { + id, + #[cfg(not(wasm_browser))] + tasks: TaskTracker::new(), + #[cfg(not(wasm_browser))] + cancel: CancellationToken::new(), + #[cfg(not(wasm_browser))] + task_counter: AtomicU64::new(0), + } + } + + /// Shutdown the runtime gracefully. + /// + /// Closes the task tracker and waits for all spawned tasks to finish naturally. + #[cfg(not(wasm_browser))] + pub(crate) async fn shutdown(&self) { + self.abort(); + // Waits for all tasks to stop (and thus drop all of their futures). + // If the task tracker had already been closed and tasks have all been cleaned up, + // this returns immediately. + self.tasks.wait().await; + } + + /// Shutdown the runtime ASAP, not waiting for any graceful closing of tasks. + #[cfg(not(wasm_browser))] + pub(crate) fn abort(&self) { + // Signal all spawned tasks to stop immediately. + self.cancel.cancel(); + // Signal that the runtime should be closed. + self.tasks.close(); + // Does not wait for the tasks to return. + } + + /// No-op on wasm. There is no task tracker to close or wait on. + #[cfg(wasm_browser)] + pub(crate) async fn shutdown(&self) {} + + /// No-op on wasm. There is no task tracker or cancellation to perform. + #[cfg(wasm_browser)] + pub(crate) fn abort(&self) {} +} + +impl noq::Runtime for Runtime { + #[cfg(not(wasm_browser))] + fn new_timer(&self, i: std::time::Instant) -> Pin> { + noq::TokioRuntime.new_timer(i) + } + + #[cfg(wasm_browser)] + fn new_timer(&self, deadline: n0_future::time::Instant) -> Pin> { + Box::pin(web::Timer(n0_future::time::sleep_until(deadline))) + } + + #[cfg(not(wasm_browser))] + fn spawn(&self, future: Pin + Send>>) { + // Do not allow spawning more tasks if the runtime should be closed. + if self.tasks.is_closed() { + tracing::debug!(me = %self.id.fmt_short(), "runtime closed, dropping spawned task"); + return; + } + + use tracing::{Instrument, trace_span}; + + let task_id = self.task_counter.fetch_add(1, Ordering::Relaxed); + let cancel = self.cancel.clone(); + let span = trace_span!("runtime", me = %self.id.fmt_short(), task_id); + self.tasks.spawn(async move { + // wrapping the future in a cancellation token is what allows + // us to "abort" tasks in the event the runtime is meant to + // close quickly and ungracefully + cancel.run_until_cancelled(future.instrument(span)).await; + }); + } + + #[cfg(wasm_browser)] + fn spawn(&self, future: Pin + Send>>) { + wasm_bindgen_futures::spawn_local(future); + } + + // We're not actually using this function in iroh + #[cfg(not(wasm_browser))] + fn wrap_udp_socket( + &self, + t: std::net::UdpSocket, + ) -> std::io::Result> { + noq::TokioRuntime.wrap_udp_socket(t) + } +} + +#[cfg(wasm_browser)] +mod web { + use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + }; + + use n0_future::time; + + #[derive(Debug)] + pub(crate) struct Timer(pub(crate) time::Sleep); + + impl noq::AsyncTimer for Timer { + fn reset(mut self: Pin<&mut Self>, deadline: time::Instant) { + Pin::new(&mut self.0).reset(deadline) + } + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + Pin::new(&mut self.0).poll(cx) + } + } +} diff --git a/vendor/iroh/src/socket.rs b/vendor/iroh/src/socket.rs new file mode 100644 index 0000000..2b5711f --- /dev/null +++ b/vendor/iroh/src/socket.rs @@ -0,0 +1,2867 @@ +//! Implements a socket that can change its communication path while in use, actively searching for the best way to communicate. +//! +//! +//! ### `RelayOnly` path selection: +//! When set this will force all packets to be sent over +//! the relay connection, regardless of whether or +//! not we have a direct UDP address for the given endpoint. +//! +//! The intended use is for testing the relay protocol inside the Socket +//! to ensure that we can rely on the relay to send packets when two endpoints +//! are unable to find direct UDP connections to each other. +//! +//! This also prevent this endpoint from attempting to hole punch and prevents it +//! from responding to any hole punching attempts. This endpoint will still, +//! however, read any packets that come off the UDP sockets. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Display, + io, + net::{IpAddr, SocketAddr}, + sync::{ + Arc, Mutex, RwLock, + atomic::{AtomicBool, Ordering}, + }, +}; + +use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr}; +use iroh_relay::{RelayConfig, RelayMap}; +use mapped_addrs::MultipathMappedAddr; +use n0_error::{AnyError, anyerr, bail, e, stack_error}; +use n0_future::{ + MaybeFuture, + task::{self, AbortOnDropHandle}, + time::{self, Duration, Instant}, +}; +use n0_watcher::{self, Watchable, Watcher}; +use netwatch::netmon; +#[cfg(not(wasm_browser))] +use netwatch::{ + interfaces::{IpNet, Ipv6AddrFlags}, + ip::LocalAddresses, +}; +use noq::{ + NetworkChangeHint, TokenStore, + crypto::rustls::{QuicClientConfig, QuicServerConfig}, +}; +use rand::RngExt; +use rustc_hash::FxHashSet; +use tokio::sync::{ + Mutex as AsyncMutex, + mpsc::{self}, + oneshot, +}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; +use tracing::{Instrument, Level, Span, debug, error, event, info_span, instrument, trace, warn}; +use transports::{LocalAddrsWatch, Transport, TransportConfig}; +use url::Url; + +use self::{ + remote_map::{RemoteMap, RemoteStateMessage}, + transports::{RelayActorConfig, Transports}, +}; +#[cfg(not(wasm_browser))] +use crate::dns::DnsResolver; +#[cfg(not(wasm_browser))] +use crate::net_report::QuicConfig; +use crate::{ + address_lookup::{self, AddressLookupFailed, EndpointData, UserData}, + defaults::timeouts::NET_REPORT_TIMEOUT, + endpoint::{ + LocalTransportAddr, RelayStatus, hooks::EndpointHooksList, quic::QuicTransportConfig, + }, + metrics::EndpointMetrics, + net_report::{self, IfStateDetails, Report}, + portmapper, + runtime::Runtime, + socket::{ + concurrent_read_map::ReadOnlyMap, + remote_map::{MappedAddrs, PathSelector, PathStateReceiver, RemoteInfo}, + transports::{HomeRelayWatch, HomeRelayWatcher}, + }, + tls::{ + self, + misc::{Blake3HmacKey, RustlsTokenKey}, + }, +}; + +mod metrics; + +pub(crate) mod biased_rtt_path_selector; +pub(crate) mod concurrent_read_map; +pub(crate) mod mapped_addrs; +pub(crate) mod remote_map; +pub(crate) mod transports; + +use self::mapped_addrs::{EndpointIdMappedAddr, MappedAddr}; +pub use self::metrics::Metrics; + +// TODO: Use this +// /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically +// /// expire at 30 seconds, so this is a few seconds shy of that. +// const ENDPOINTS_FRESH_ENOUGH_DURATION: Duration = Duration::from_secs(27); + +/// The duration in which we send keep-alives. +/// +/// If a path is idle for this long, a PING frame will be sent to keep the connection +/// alive. +pub(crate) const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); + +/// The maximum time a path can stay idle before being closed. +/// +/// 15s gives 3x [`HEARTBEAT_INTERVAL`] (5s) for multiple retry chances, and enough +/// margin for real-world outages (WiFi reconnect 2-5s, cellular handoff 2-10s). +/// iroh 0.35 used 10s at the QUIC level; tailscale uses 45s at the WireGuard session +/// level with 3s heartbeats. +pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(15); + +/// The maximum time a relay path can stay idle before being closed. +/// +/// Relay paths need a longer idle timeout than direct paths because the relay actor +/// manages the WebSocket connection and transparently reconnects after network changes +/// or relay server restarts. During network outages the interface may be down for +/// 5-15s, during which no relay traffic flows. Once the interface recovers, the relay +/// actor reconnects (DNS + TCP + TLS + WebSocket upgrade), which adds another 1-2s. +/// +/// Set to match the connection-level idle timeout (30s) so the relay path survives +/// as long as the connection itself. +pub(crate) const RELAY_PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30); + +/// Maximum number of concurrent QUIC multipath paths per connection. +/// +/// We expect 1 relay path, and then leave space for ~3 IP and custom transport paths. +/// On top of that, when we expect a network change, we might be closing these paths +/// (except for the relay path) and open new ones, and give us 3 more paths to spare. +/// And finally we round that up to 8 for good measure. +pub(crate) const MAX_MULTIPATH_PATHS: u32 = 8; + +/// Maximum number of n0 QUIC NAT Traversal addresses that the QUIC stack should allow. +/// +/// This needs to be big enough to accommodate for machines which have lots of network +/// interfaces enabled. We've seen MacOS machines with >25 interfaces in the wild +/// (mostly due to VPN TUN and docket interfaces), so this seems like a reasonable +/// value. +pub(crate) const MAX_QNT_ADDRESSES: u8 = 32; + +/// Error returned when the endpoint state actor stopped while waiting for a reply. +#[stack_error(add_meta, derive)] +#[error("endpoint state actor stopped")] +#[derive(Clone)] +pub(crate) struct RemoteStateActorStoppedError; + +impl From> for RemoteStateActorStoppedError { + #[track_caller] + fn from(_value: mpsc::error::SendError) -> Self { + Self::new() + } +} + +/// Contains options for `Socket::listen`. +#[derive(derive_more::Debug)] +pub(crate) struct Options { + /// The configuration for the different transports. + pub(crate) transports: Vec, + + /// Secret key for this endpoint. + pub(crate) secret_key: SecretKey, + + /// Optional user-defined Address Lookup data. + pub(crate) address_lookup_user_data: Option, + + /// A DNS resolver to use for resolving relay URLs. + /// + /// You can use [`crate::dns::DnsResolver::new`] for a resolver + /// that uses the system's DNS configuration. + #[cfg(not(wasm_browser))] + pub(crate) dns_resolver: DnsResolver, + + /// Proxy configuration. + pub(crate) proxy_url: Option, + + /// TLS configuration for HTTPS and non-iroh-QUIC connections. + pub(crate) tls_config: rustls::ClientConfig, + + /// ServerConfig for the internal QUIC endpoint + pub(crate) server_config: noq_proto::ServerConfig, + + pub(crate) metrics: EndpointMetrics, + pub(crate) hooks: EndpointHooksList, + pub(crate) path_selector: Arc, + pub(crate) portmapper_config: portmapper::PortmapperConfig, + pub(crate) net_report_config: crate::net_report::NetReportConfig, + + /// Static configuration for the endpoint. + pub(crate) static_config: StaticConfig, + + /// Explicitly configured external addresses to advertise. + pub(crate) configured_addrs: BTreeSet, +} + +/// Inner state for an iroh [`crate::Endpoint`]. +/// +/// Dereferences to [`Socket`], and handles closing. +#[derive(Debug, derive_more::Deref)] +pub(crate) struct EndpointInner { + #[deref(forward)] + sock: Arc, + // empty when shutdown + actor_task: Mutex>>, + /// Channel to send to the internal actor. + actor_sender: mpsc::Sender, + // noq endpoint + endpoint: noq::Endpoint, + // Runtime used by noq + runtime: Arc, + /// Static configuration for the endpoint. + pub(crate) static_config: StaticConfig, +} + +impl Drop for EndpointInner { + fn drop(&mut self) { + if self.sock.is_closed() { + return; + } + tracing::error!( + "Endpoint dropped without calling `Endpoint::close`. Aborting ungracefully." + ); + self.abort(); + } +} + +/// Configuration for a [`noq::Endpoint`] that cannot be changed at runtime. +#[derive(derive_more::Debug)] +pub(crate) struct StaticConfig { + pub(crate) tls_config: tls::TlsConfig, + #[debug("QuicServerConifg")] + pub(crate) server_config: QuicServerConfig, + #[debug("QuicClientConfig")] + pub(crate) client_config: QuicClientConfig, + #[debug("Arc")] + pub(crate) token_key: Arc, + #[debug("Arc")] + pub(crate) token_store: Arc, + pub(crate) transport_config: QuicTransportConfig, +} + +impl StaticConfig { + /// Create a [`noq_proto::ServerConfig`] with the specified ALPN protocols. + pub(crate) fn create_server_config( + &self, + alpn_protocols: Vec>, + ) -> noq_proto::ServerConfig { + let mut quic_server_config = self.server_config.clone(); + quic_server_config.set_alpn_protocols(alpn_protocols); + let mut inner = + noq::ServerConfig::new(Arc::new(quic_server_config), self.token_key.clone()); + inner.transport_config(self.transport_config.to_inner_arc()); + inner + } + + /// Create a [`noq_proto::ClientConfig`] with the specified ALPN protocols. + pub(crate) fn create_client_config( + &self, + alpn_protocols: Vec>, + transport_config: Arc, + ) -> noq_proto::ClientConfig { + let mut quic_client_config = self.client_config.clone(); + quic_client_config.set_alpn_protocols(alpn_protocols); + let mut inner = noq::ClientConfig::new(Arc::new(quic_client_config)); + inner.transport_config(transport_config); + inner.token_store(self.token_store.clone()); + inner + } +} + +/// This coordinates the shutdown of the [`Socket`] and all its tasks. +/// +/// It also tightly binds to the [`EndpointInner`] and [`Actor`] closing as that is where +/// most of the logic lives. +#[derive(Debug)] +struct ShutdownState { + /// Token that is cancelled at the moment [`crate::Endpoint::close`] is called. + /// + /// Currently cancelled from [`EndpointInner::close`]. + at_close_start: CancellationToken, + /// Token that is cancelled once the [`noq::Endpoint`] is drained. + /// + /// Only 100ms after this is cancelled will the [`Actor`] task be cancelled, it should + /// have exited already by then as it is considered an error if it was still running. + at_endpoint_closed: CancellationToken, + /// Set if the endpoint is closed and all tasks are stopped. + /// + /// This is only set once both [`Self::at_close_start`] and [`Self::at_endpoint_closed`] + /// are cancelled **and** the [`Actor`] task is no longer running. + closed: AtomicBool, +} + +impl Default for ShutdownState { + fn default() -> Self { + Self { + at_close_start: CancellationToken::new(), + at_endpoint_closed: CancellationToken::new(), + closed: AtomicBool::new(false), + } + } +} + +impl ShutdownState { + /// Whether the endpoint has started closing, or is already closed. + /// + /// This is true once [`crate::Endpoint::close`] is called, and remains true forever + /// after. Tasks might still be shutting down. + fn is_closing(&self) -> bool { + self.at_close_start.is_cancelled() + } + + /// Whether the endpoint is fully closed and all tasks stopped. + /// + /// The endpoint will be drained, all transports and sockets will be closed. + fn is_closed(&self) -> bool { + self.closed.load(Ordering::Relaxed) + } +} + +/// Iroh connectivity layer. +/// +/// This is responsible for routing packets to endpoints based on endpoint IDs, it will initially +/// route packets via a relay and transparently try and establish an endpoint-to-endpoint +/// connection and upgrade to it. It will also keep looking for better connections as the +/// network details of both endpoints change. +/// +/// It is usually only necessary to use a single [`Socket`] instance in an application, it +/// means any QUIC endpoints on top will be sharing as much information about endpoints as +/// possible. +#[derive(Debug)] +pub(crate) struct Socket { + /// Read-only view of the per-remote `RemoteStateActor` inboxes. + /// + /// Lets callers send to an existing `RemoteStateActor` without going through + /// the socket actor. + /// + /// A missing entry means no actor is running for that remote. Spawning new + /// `RemoteStateActor`s must go through the socket actor channel. + remote_actors: ReadOnlyMap>, + + // - Shutdown Management + shutdown: ShutdownState, + + // - Networking Info + /// Our discovered direct addresses. + direct_addrs: DiscoveredDirectAddrs, + /// Our latest net-report + net_report: Watchable<(Option, UpdateReason)>, + /// If the last net_report report, reports IPv6 to be available. + ipv6_reported: Arc, + /// Maps for resolving mapped addrs to/from IP and relay addresses. + mapped_addrs: MappedAddrs, + + /// Local addresses + local_addrs_watch: LocalAddrsWatch, + home_relay_watch: HomeRelayWatcher, + /// Currently bound IP addresses of all sockets + #[cfg(not(wasm_browser))] + ip_bind_addrs: Vec, + /// The DNS resolver to be used in this socket. + #[cfg(not(wasm_browser))] + dns_resolver: DnsResolver, + relay_map: RelayMap, + + /// Optional Address Lookup + address_lookup: address_lookup::AddressLookupServices, + /// Optional user-defined discover data. + address_lookup_user_data: RwLock>, + /// Explicitly configured external addresses to advertise. + configured_addrs: RwLock>, + + pub(crate) tls_config: rustls::ClientConfig, + + /// Metrics + pub(crate) metrics: EndpointMetrics, + pub(crate) hooks: EndpointHooksList, + /// Tracing span for this endpoint. + pub(crate) span: Span, +} + +impl Socket { + /// Returns the relay endpoint we are connected to, that has the best latency. + /// + /// If `None`, then we are not connected to any relay endpoints. + pub(crate) fn my_relay(&self) -> Option { + self.local_addr().into_iter().find_map(|a| { + if let transports::Addr::Relay(url, _) = a { + Some(url) + } else { + None + } + }) + } + + /// Whether the iroh endpoint is closed and all its actors stopped. + pub(crate) fn is_closed(&self) -> bool { + self.shutdown.is_closed() + } + + /// Whether [`crate::Endpoint::close`] has been called. + fn is_closing(&self) -> bool { + self.shutdown.is_closing() + } + + /// Returns a future that resolves once endpoint shutdown has started. + pub(crate) fn closed(&self) -> WaitForCancellationFutureOwned { + self.shutdown.at_close_start.clone().cancelled_owned() + } + + /// Get the cached version of addresses. + pub(crate) fn local_addr(&self) -> Vec { + self.local_addrs_watch.clone().get() + } + + #[cfg(not(wasm_browser))] + fn ip_bind_addrs(&self) -> &[SocketAddr] { + &self.ip_bind_addrs + } + + fn ip_local_addrs(&self) -> impl Iterator + use<> { + self.local_addr() + .into_iter() + .filter_map(|addr| addr.into_socket_addr()) + } + + /// Tries to send a [`RemoteStateMessage`] to the `RemoteStateActor` for given [`EndpointId`]. + /// + /// Returns an error if there currently is no remote state actor running for this, or when it + /// is currently shutting down. + pub(crate) fn try_send_remote_state_msg( + &self, + endpoint_id: EndpointId, + message: RemoteStateMessage, + ) -> Result<(), RemoteStateMessage> { + let Some(sender) = self.remote_actors.get(&endpoint_id) else { + return Err(message); + }; + sender.try_send(message).map_err(|err| err.into_inner()) + } + + /// Returns a [`Watcher`] for this socket's direct addresses. + /// + /// The [`Socket`] continuously monitors the direct addresses, the network addresses + /// it might be able to be contacted on, for changes. Whenever changes are detected + /// this [`Watcher`] will yield a new list of addresses. + /// + /// Upon the first creation on the [`Socket`] it may not yet have completed a first + /// net report to discover IP addresses, in this case the current item in this [`Watcher`] will be + /// [`None`]. Once the first set of ip addresses are discovered the [`Watcher`] will + /// store [`Some`] set of addresses. + /// + /// To get the current direct addresses, use [`Watcher::initialized`]. + /// + /// [`Watcher`]: n0_watcher::Watcher + /// [`Watcher::initialized`]: n0_watcher::Watcher::initialized + pub(crate) fn ip_addrs(&self) -> n0_watcher::Direct> { + self.direct_addrs.addrs.watch() + } + + /// Returns a [`Watcher`] for this socket's net-report. + /// + /// The [`Socket`] continuously monitors the network conditions for changes. + /// Whenever changes are detected this [`Watcher`] will yield a new report. + /// + /// Upon the first creation on the [`Socket`] it may not yet have completed + /// a first net-report. In this case, the current item in this [`Watcher`] will + /// be [`None`]. Once the first report has been run, the [`Watcher`] will + /// store [`Some`] report. + /// + /// To get the current `net-report`, use [`Watcher::initialized`]. + /// + /// [`Watcher`]: n0_watcher::Watcher + /// [`Watcher::initialized`]: n0_watcher::Watcher::initialized + #[cfg(feature = "unstable-net-report")] + pub(crate) fn net_report(&self) -> impl Watcher> + use<> { + self.net_report.watch().map(|(r, _)| r) + } + + /// Watch for changes to the home relay. + /// + /// Note that this can be used to wait for the initial home relay to be known using + /// [`Watcher::initialized`]. + pub(crate) fn home_relay(&self) -> impl Watcher> + use<> { + self.local_addrs_watch.clone().map(|addrs| { + addrs + .into_iter() + .filter_map(|addr| { + if let transports::Addr::Relay(url, _) = addr { + Some(url) + } else { + None + } + }) + .collect() + }) + } + + pub(crate) fn home_relay_status(&self) -> impl Watcher> + use<> { + self.home_relay_watch.clone() + } + + /// Stores a new set of direct addresses. + /// + /// If the direct addresses have changed from the previous set, they are published to + /// the address lookup system. + fn store_direct_addresses(&self, addrs: BTreeSet) { + let updated = self.direct_addrs.update(addrs); + if updated { + self.publish_my_addr(); + } + } + + /// Get a reference to the DNS resolver used in this [`Socket`]. + #[cfg(not(wasm_browser))] + pub(crate) fn dns_resolver(&self) -> &DnsResolver { + &self.dns_resolver + } + + /// Translates a possible IP-mapped [`SocketAddr`] into a [`transports::Addr`]. + /// + /// For regular IP addresses this returns `Addr::Ip`. For mapped addresses this performs + /// a reverse lookup. + pub(crate) fn to_transport_addr(&self, addr: SocketAddr) -> Option { + self.mapped_addrs.to_transport_addr(addr) + } + + pub(crate) fn to_local_transport_addr( + &self, + local_ip: Option, + remote_addr: SocketAddr, + ) -> LocalTransportAddr { + let remote_addr = self.to_transport_addr(remote_addr).unwrap_or_else(|| { + error!( + mapped_addr = ?remote_addr, + "Socket::to_local_transport_addr: invalid mapped address", + ); + transports::Addr::Ip(remote_addr) + }); + LocalTransportAddr::from_noq_local_ip( + local_ip, + &remote_addr, + &self.mapped_addrs.custom_addrs, + ) + } + + /// Reference to the internal Address Lookup + pub(crate) fn address_lookup(&self) -> &address_lookup::AddressLookupServices { + &self.address_lookup + } + + /// Updates the user-defined Address Lookup data for this endpoint. + pub(crate) fn set_user_data_for_address_lookup(&self, user_data: Option) { + let mut guard = self + .address_lookup_user_data + .write() + .expect("lock poisened"); + if *guard != user_data { + *guard = user_data; + drop(guard); + self.publish_my_addr(); + } + } + + /// Process datagrams received from all the transports. + /// + /// All the `bufs` and `metas` should have initialized packets in them. + /// + /// This fixes up the datagrams to use the correct [`MultipathMappedAddr`] and extracts + /// DISCO packets, processing them inside the socket. + /// + /// [`MultipathMappedAddr`]: mapped_addrs::MultipathMappedAddr + fn process_datagrams( + &self, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + recv_infos: &[transports::RecvInfo], + ) { + assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + assert_eq!( + bufs.len(), + recv_infos.len(), + "non matching bufs & recv_infos" + ); + + // zip is slow :( + for i in 0..metas.len() { + let noq_meta = &mut metas[i]; + let recv_info = &recv_infos[i]; + let remote_addr = recv_info.remote(); + let datagram_count = if noq_meta.stride == 0 { + if noq_meta.len > 0 { + warn!( + src = ?remote_addr, + len = noq_meta.len, + "received datagram with stride=0 but len>0", + ); + // fix the weird len + noq_meta.len = 0; + } + // one empty datagram + 1 + } else { + noq_meta.len.div_ceil(noq_meta.stride) + }; + self.metrics + .socket + .recv_datagrams + .inc_by(datagram_count as _); + if noq_meta.len > noq_meta.stride { + trace!( + src = ?remote_addr, + len = noq_meta.len, + stride = %noq_meta.stride, + datagram_count, + "GRO datagram received", + ); + self.metrics.socket.recv_gro_datagrams.inc(); + } else { + trace!(src = ?remote_addr, len = noq_meta.len, "datagram received"); + } + match remote_addr { + transports::Addr::Ip(SocketAddr::V4(..)) => { + self.metrics.socket.recv_data_ipv4.inc_by(noq_meta.len as _); + } + transports::Addr::Ip(SocketAddr::V6(..)) => { + self.metrics.socket.recv_data_ipv6.inc_by(noq_meta.len as _); + } + transports::Addr::Relay(src_url, src_node) => { + self.metrics + .socket + .recv_data_relay + .inc_by(noq_meta.len as _); + + // Fill in the correct mapped address + let mapped_addr = self + .mapped_addrs + .relay_addrs + .get(&(src_url.clone(), *src_node)); + noq_meta.addr = mapped_addr.private_socket_addr(); + } + transports::Addr::Custom(remote) => { + self.metrics + .socket + .recv_data_custom + .inc_by(noq_meta.len as _); + // Fill in the correct mapped address + let mapped_addr = self.mapped_addrs.custom_addrs.get(remote); + noq_meta.addr = mapped_addr.private_socket_addr(); + if let Some(local) = recv_info.local() { + let local_mapped = self.mapped_addrs.custom_addrs.get(local); + noq_meta.dst_ip = Some(local_mapped.private_socket_addr().ip()); + } + } + } + } + } + + /// Publishes our address to an address lookup service, if configured. + /// + /// Called whenever our addresses or home relay endpoint changes. + fn publish_my_addr(&self) { + let relay_url = self.my_relay(); + let mut addrs: Vec<_> = self + .direct_addrs + .sockaddrs() + .map(TransportAddr::Ip) + .collect(); + + let user_data = self + .address_lookup_user_data + .read() + .expect("lock poisened") + .clone(); + if relay_url.is_none() && addrs.is_empty() && user_data.is_none() { + // do not bother publishing if we don't have any information + return; + } + if let Some(url) = relay_url { + addrs.push(TransportAddr::Relay(url)); + } + + let mut data = EndpointData::new(addrs); + data.set_user_data(user_data); + self.address_lookup.publish(&data); + } +} + +/// Manages currently running net reports to learn this endpoint's IP addresses. +/// +/// Invariants: +/// - only one direct addr update must be running at a time +/// - if an update is scheduled while another one is running, remember that +/// and start a new one when the current one has finished +#[derive(Debug)] +struct DirectAddrUpdateState { + /// If set, start a new update as soon as the current one is finished. + want_update: Option, + sock: Arc, + port_mapper: portmapper::Client, + /// The prober that discovers local network conditions, including the closest relay relay and NAT mappings. + net_reporter: Arc>, + relay_map: RelayMap, + run_done: mpsc::Sender<()>, + shutdown_token: CancellationToken, +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +enum UpdateReason { + /// Initial state + #[default] + None, + Periodic, + PortmapUpdated, + LinkChangeMajor, + LinkChangeMinor, + RelayMapChange, +} + +impl UpdateReason { + fn is_major(self) -> bool { + matches!(self, Self::LinkChangeMajor | Self::RelayMapChange) + } +} + +impl DirectAddrUpdateState { + fn new( + sock: Arc, + port_mapper: portmapper::Client, + net_reporter: Arc>, + relay_map: RelayMap, + run_done: mpsc::Sender<()>, + shutdown_token: CancellationToken, + ) -> Self { + DirectAddrUpdateState { + want_update: Default::default(), + port_mapper, + net_reporter, + sock, + relay_map, + run_done, + shutdown_token, + } + } + + /// Schedules a new run, either starting it immediately if none is running or + /// scheduling it for later. + fn schedule_run(&mut self, why: UpdateReason, if_state: IfStateDetails) { + match self.net_reporter.clone().try_lock_owned() { + Ok(net_reporter) => { + self.run(why, if_state, net_reporter); + } + Err(_) => { + let _ = self.want_update.insert(why); + } + } + } + + /// If another run is needed, triggers this run, otherwise does nothing. + fn try_run(&mut self, if_state: IfStateDetails) { + match self.net_reporter.clone().try_lock_owned() { + Ok(net_reporter) => { + if let Some(why) = self.want_update.take() { + self.run(why, if_state, net_reporter); + } + } + Err(_) => { + // do nothing + } + } + } + + /// Trigger a new run. + fn run( + &mut self, + why: UpdateReason, + if_state: IfStateDetails, + mut net_reporter: tokio::sync::OwnedMutexGuard, + ) { + debug!("starting direct addr update ({:?})", why); + // Don't start a net report probe if we know + // we are shutting down + if self.shutdown_token.is_cancelled() { + debug!("skipping net_report, socket is shutting down"); + // deactivate portmapper + self.port_mapper.deactivate(); + return; + } + if self.relay_map.is_empty() { + debug!("skipping net_report, empty RelayMap"); + self.sock.net_report.set((None, why)).ok(); + return; + } + + self.sock.metrics.net_report.portmap_attempts.inc(); + self.port_mapper.procure_mapping(); + + trace!("requesting net_report report"); + let sock = self.sock.clone(); + + let run_done = self.run_done.clone(); + + // Ensure that reports are cancelled when we shutdown + let token = self.shutdown_token.child_token(); + let inner_token = token.child_token(); + task::spawn( + async move { + let fut = token.run_until_cancelled(time::timeout( + NET_REPORT_TIMEOUT, + net_reporter.get_report(if_state, why.is_major(), inner_token), + )); + + match fut.await { + Some(Ok(report)) => { + sock.net_report.set((Some(report), why)).ok(); + } + Some(Err(time::Elapsed { .. })) => { + warn!("net_report report timed out"); + } + None => { + trace!("net_report cancelled"); + } + } + + // mark run as finished + debug!("direct addr update done ({:?})", why); + run_done.send(()).await.ok(); + } + .instrument(tracing::Span::current()), + ); + } +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +#[non_exhaustive] +pub enum BindError { + #[error("Failed to bind sockets")] + Sockets { source: io::Error }, + #[error("Failed to create internal QUIC endpoint")] + CreateQuicEndpoint { source: io::Error }, + #[error("Failed to create netmon monitor")] + CreateNetmonMonitor { source: AnyError }, + #[error("Invalid transport configuration")] + InvalidTransportConfig, + #[error("Invalid CA root configuration")] + InvalidCaRootConfig { source: io::Error }, + #[error("Failed to create an address lookup service")] + AddressLookup { + #[error(from)] + source: crate::address_lookup::AddressLookupBuilderError, + }, + #[error("Missing or incompatible rustls crypto provider configured")] + InvalidCryptoProvider, + #[error("Error constructing TLS configuration")] + TlsConfigError { + #[error(from)] + source: tls::TlsConfigError, + }, +} + +impl EndpointInner { + /// Creates a [`EndpointInner`]. + pub(crate) async fn bind(opts: Options) -> Result { + // Use the current span as the main span for all tasks spawned in this endpoint. + // `EndpointInner::bind` is not public and only called from `crate::endpoint::Builder::bind`, + // which instruments the call with a span created for this purpose. + let span = tracing::Span::current(); + + let Options { + secret_key, + transports: transport_configs, + address_lookup_user_data, + #[cfg(not(wasm_browser))] + dns_resolver, + proxy_url, + server_config, + tls_config, + metrics, + hooks, + path_selector, + portmapper_config, + net_report_config, + static_config, + configured_addrs, + } = opts; + + let address_lookup = + address_lookup::AddressLookupServices::with_metrics(metrics.address_lookup.clone()); + let port_mapper = portmapper::create_client(&portmapper_config); + + let relay_transport_configs: Vec<_> = transport_configs + .iter() + .filter(|t| matches!(t, TransportConfig::Relay { .. })) + .collect(); + + // Currently we only support a single relay transport + if relay_transport_configs.len() > 1 { + bail!(BindError::InvalidTransportConfig); + } + let relay_map = relay_transport_configs + .iter() + .filter_map(|t| { + #[allow(irrefutable_let_patterns)] + if let TransportConfig::Relay { relay_map, .. } = t { + Some(relay_map.clone()) + } else { + None + } + }) + .next() + .unwrap_or_else(RelayMap::empty); + + let ipv6_reported = Arc::new(AtomicBool::new(false)); + + let relay_actor_config = RelayActorConfig { + my_relay: HomeRelayWatch::default(), + secret_key: secret_key.clone(), + #[cfg(not(wasm_browser))] + dns_resolver: dns_resolver.clone(), + proxy_url: proxy_url.clone(), + ipv6_reported: ipv6_reported.clone(), + tls_config: tls_config.clone(), + metrics: metrics.socket.clone(), + relay_map: relay_map.clone(), + }; + + let shutdown_state = ShutdownState::default(); + let shutdown_token = shutdown_state.at_endpoint_closed.child_token(); + + let transports = Transports::bind( + &transport_configs, + relay_actor_config, + &metrics, + shutdown_token.child_token(), + ) + .map_err(|err| e!(BindError::Sockets, err))?; + + if let Some(v4_port) = transports.local_addrs().into_iter().find_map(|t| { + if let transports::Addr::Ip(SocketAddr::V4(addr)) = t { + Some(addr.port()) + } else { + None + } + }) { + // NOTE: we can end up with a zero port if `netwatch::UdpSocket::socket_addr` fails + match v4_port.try_into() { + Ok(non_zero_port) => { + port_mapper.update_local_port(non_zero_port); + } + Err(_zero_port) => debug!("Skipping port mapping with zero local port"), + } + } + + let (actor_sender, actor_receiver) = mpsc::channel(256); + + #[cfg(not(wasm_browser))] + let has_ipv6_transport = transports + .ip_bind_addrs() + .into_iter() + .any(|addr| addr.is_ipv6()); + + #[cfg(not(wasm_browser))] + let has_ip_transports = !transports.ip_bind_addrs().is_empty(); + + let direct_addrs = DiscoveredDirectAddrs::default(); + + let remote_map = { + RemoteMap::new( + metrics.socket.clone(), + direct_addrs.addrs.watch(), + address_lookup.clone(), + shutdown_token.child_token(), + path_selector, + span.clone(), + ) + }; + + let home_relay_watch = transports.home_relay_watch(); + + let sock = Arc::new(Socket { + remote_actors: remote_map.senders(), + shutdown: shutdown_state, + ipv6_reported, + mapped_addrs: remote_map.mapped_addrs.clone(), + address_lookup, + relay_map: relay_map.clone(), + address_lookup_user_data: RwLock::new(address_lookup_user_data), + configured_addrs: RwLock::new(configured_addrs), + direct_addrs, + net_report: Watchable::new((None, UpdateReason::None)), + #[cfg(not(wasm_browser))] + dns_resolver: dns_resolver.clone(), + metrics: metrics.clone(), + local_addrs_watch: transports.local_addrs_watch(), + home_relay_watch, + #[cfg(not(wasm_browser))] + ip_bind_addrs: transports.ip_bind_addrs(), + tls_config: tls_config.clone(), + hooks, + span: span.clone(), + }); + + let mut endpoint_config = + noq::EndpointConfig::new(Arc::new(Blake3HmacKey::new(&mut rand::rng()))); + // Setting this to false means that noq will ignore packets that have the QUIC fixed bit + // set to 0. The fixed bit is the 3rd bit of the first byte of a packet. + // For performance reasons and to not rewrite buffers we pass non-QUIC UDP packets straight + // through to noq. We set the first byte of the packet to zero, which makes noq ignore + // the packet if grease_quic_bit is set to false. + endpoint_config.grease_quic_bit(false); + + let local_addrs_watch = transports.local_addrs_watch(); + let transports_network_change = transports.create_network_change_sender(); + + let runtime = Arc::new(Runtime::new(secret_key.public())); + + let endpoint = noq::Endpoint::new_with_abstract_socket( + endpoint_config, + Some(server_config), + Box::new(Transport::new(sock.clone(), transports)), + runtime.clone(), + ) + .map_err(|err| e!(BindError::CreateQuicEndpoint, err))?; + + let network_monitor = netmon::Monitor::new() + .await + .map_err(|err| e!(BindError::CreateNetmonMonitor, anyerr!(err)))?; + + #[cfg(not(wasm_browser))] + let net_report_config = { + // Set a `QuicConfig` for address discovery (QAD), but only if we have IP transports. + // + // If there are no IP transports configured, then we don't set a QuicConfig. + // If we would, the `noq::Endpoint` passed along will not have IP connectivity, + // and the QAD probes that connect to the relay's QUIC endpoints would time out + // because all outgoing packets to IP destinations would be dropped. + let qad_config = has_ip_transports.then(|| QuicConfig { + ep: endpoint.clone(), + client_config: tls_config.clone(), + ipv4: true, + ipv6: has_ipv6_transport, + }); + net_report::Options::new(tls_config.clone()) + .quic_config(qad_config) + .proxy_url(proxy_url.clone()) + .net_report_config(net_report_config) + }; + + #[cfg(wasm_browser)] + let net_report_config = net_report::Options::default().net_report_config(net_report_config); + + let net_reporter = net_report::Client::new( + #[cfg(not(wasm_browser))] + dns_resolver, + relay_map.clone(), + net_report_config, + metrics.net_report.clone(), + ); + + let (direct_addr_done_tx, direct_addr_done_rx) = mpsc::channel(8); + let direct_addr_update_state = DirectAddrUpdateState::new( + sock.clone(), + port_mapper, + Arc::new(AsyncMutex::new(net_reporter)), + relay_map, + direct_addr_done_tx, + sock.shutdown.at_close_start.child_token(), + ); + + let local_interfaces_watcher = network_monitor.interface_state(); + + #[cfg_attr(not(wasm_browser), allow(unused_mut))] + let mut actor = Actor { + endpoint: endpoint.clone(), + sock: sock.clone(), + remote_map, + periodic_re_stun_timer: new_re_stun_timer(false), + network_monitor, + local_interfaces_watcher, + direct_addr_update_state, + transports_network_change, + direct_addr_done_rx, + call_notify_quic_network_change: None, + }; + // Initialize addresses + #[cfg(not(wasm_browser))] + actor.update_direct_addresses(None); + + let actor_task = task::spawn( + actor + .run( + actor_receiver, + shutdown_token.child_token(), + local_addrs_watch, + ) + .instrument(info_span!(parent: span, "actor")), + ); + + let actor_task = Mutex::new(Some(AbortOnDropHandle::new(actor_task))); + + Ok(EndpointInner { + sock, + actor_sender, + actor_task, + endpoint, + runtime, + static_config, + }) + } + + /// Returns a reference to the underlying [`noq::Endpoint`]. + pub(crate) fn noq_endpoint(&self) -> &noq::Endpoint { + &self.endpoint + } + + /// Closes the iroh endpoint. + /// + /// Only the first close does anything. Any later closes return nil. Polling the socket + /// ([`noq::AsyncUdpSocket::poll_recv`]) will return [`Poll::Pending`] indefinitely + /// after this call. + /// + /// [`Poll::Pending`]: std::task::Poll::Pending + #[instrument(skip_all, parent = self.sock.span.clone())] + pub(crate) async fn close(&self) { + if self.sock.is_closed() || self.sock.is_closing() { + return; + } + trace!("socket closing..."); + + // Cancel at_close_start token, which cancels running netreports. + self.sock.shutdown.at_close_start.cancel(); + + // Remove address lookup services + self.sock.address_lookup().clear(); + + // Initiate closing all connections, and refuse future connections. + self.noq_endpoint().close(0u16.into(), b""); + + // In the history of this code, this call had been + // - removed: https://github.com/n0-computer/iroh/pull/1753 + // - then added back in: https://github.com/n0-computer/iroh/pull/2227/files#diff-ba27e40e2986a3919b20f6b412ad4fe63154af648610ea5d9ed0b5d5b0e2d780R573 + // - then removed again: https://github.com/n0-computer/iroh/pull/3165 + // and finally added back in together with this comment. + // So before removing this call, please consider carefully. + // Among other things, this call tries its best to make sure that any queued close frames + // (e.g. via the call to `endpoint.close(...)` above), are flushed out to the sockets + // *and acknowledged* (or time out with the "probe timeout" of usually 3 seconds). + // This allows the other endpoints for these connections to be notified to release + // their resources, or - depending on the protocol - that all data was received. + // With the current noq API, this is the only way to ensure protocol code can use + // connection close codes, and close the endpoint properly. + // If this call is skipped, then connections that protocols close just shortly before the + // call to `Endpoint::close` will in most cases cause connection time-outs on remote ends. + trace!("wait_all_draining start"); + self.noq_endpoint().wait_all_draining().await; + trace!("wait_all_draining done"); + + // Start cancellation of all actors. + self.sock.shutdown.at_endpoint_closed.cancel(); + + // MutexGuard is not held across await points + let task = self.actor_task.lock().expect("poisoned").take(); + if let Some(task) = task { + // give the tasks a moment to shutdown cleanly + let shutdown_done = time::timeout(Duration::from_millis(100), async move { + if let Err(err) = task.await { + warn!("unexpected error in task shutdown: {:?}", err); + } + }) + .await; + match shutdown_done { + Ok(_) => trace!("tasks finished in time, shutdown complete"), + Err(time::Elapsed { .. }) => { + // Dropping the task will abort it + warn!("tasks didn't finish in time, aborting"); + } + } + } + + // Waits for the EndpointDriver and all ConnectionDrivers to shut down + // Expects that the `noq::Endpoint` has been closed before this call, + // otherwise, the runtime will never shutdown. + self.runtime.shutdown().await; + + self.sock.shutdown.closed.store(true, Ordering::SeqCst); + + trace!("socket closed"); + } + + /// Aborts the endpoint ungracefully: + /// + /// - Calls cancellation token that stops running net reports + /// - Removes all address lookup services + /// - Calls cancellation token that stops all the Socket actors + /// - Aborts the runtime + /// - Drops the actor task + /// - Sets the `Socket::is_closed` state to true + /// + /// This does not wait for any current connections or tasks to close gracefully. + /// + /// This should only be called in the `iroh::Endpoint` `Drop` impl when the + /// `iroh::Endpoint` is dropped without first calling `Endpoint::close`. + #[instrument(skip_all, parent = self.sock.span.clone())] + pub(crate) fn abort(&self) { + if self.sock.is_closed() || self.sock.is_closing() { + return; + } + trace!("socket aborting..."); + + // Cancel at_close_start token, which cancels running netreports. + self.sock.shutdown.at_close_start.cancel(); + + self.sock.address_lookup().clear(); + + // Cancel all actors. + self.sock.shutdown.at_endpoint_closed.cancel(); + + // Aborts all tasks, not waiting for any to close gracefully. + self.runtime.abort(); + + self.actor_task.lock().expect("poisoned").take(); + + self.sock.shutdown.closed.store(true, Ordering::SeqCst); + trace!("socket closed"); + } + + pub(crate) async fn insert_relay( + &self, + relay: RelayUrl, + endpoint: Arc, + ) -> Option> { + let res = self.relay_map.insert(relay, endpoint); + self.actor_sender + .send(ActorMessage::RelayMapChange) + .await + .ok(); + res + } + + pub(crate) async fn remove_relay(&self, relay: &RelayUrl) -> Option> { + let res = self.relay_map.remove(relay); + self.actor_sender + .send(ActorMessage::RelayMapChange) + .await + .ok(); + res + } + + /// Adds an external address to advertise to peers. + pub(crate) async fn add_external_addr(&self, addr: SocketAddr) { + self.sock + .configured_addrs + .write() + .expect("poisoned") + .insert(addr); + self.actor_sender + .send(ActorMessage::DirectAddrRefresh) + .await + .ok(); + } + + /// Removes a configured external address. Returns `true` if it was present. + pub(crate) async fn remove_external_addr(&self, addr: &SocketAddr) -> bool { + let removed = self + .sock + .configured_addrs + .write() + .expect("poisoned") + .remove(addr); + if removed { + self.actor_sender + .send(ActorMessage::DirectAddrRefresh) + .await + .ok(); + } + removed + } + + /// Call to notify the system of potential network changes. + pub(crate) async fn network_change(&self) { + self.actor_sender + .send(ActorMessage::NetworkChange) + .await + .ok(); + } + + #[cfg(all(test, with_crypto_provider))] + async fn force_network_change(&self, is_major: bool) { + self.actor_sender + .send(ActorMessage::ForceNetworkChange(is_major)) + .await + .ok(); + } + + /// Resolves an [`EndpointAddr`] to an [`EndpointIdMappedAddr`] to connect to via [`EndpointInner::endpoint`]. + /// + /// This starts a `RemoteStateActor` for the remote if not running already, and then checks + /// if the actor has any known paths to the remote. If not, it starts address lookup and waits for + /// at least one result to arrive. + /// + /// Returns `Ok(Ok(EndpointIdMappedAddr))` if there is a known path or Address Lookup produced + /// at least one result. This does not mean there is a working path, only that we have at least + /// one transport address we can try to connect to. + /// + /// Returns `Ok(Err(address_lookup_error))` if there are no known paths to the remote and Address Lookup + /// failed or produced no results. This means that we don't have any transport address for + /// the remote, thus there is no point in trying to connect over the noq endpoint. + /// + /// Returns `Err(RemoteStateActorStoppedError)` if the `RemoteStateActor` for the remote has stopped, + /// which may never happen and thus is a bug if it does. + pub(crate) async fn resolve_remote( + &self, + addr: EndpointAddr, + ) -> Result, RemoteStateActorStoppedError> + { + let (tx, rx) = oneshot::channel(); + let remote_id = addr.id; + self.actor_sender + .send(ActorMessage::ResolveRemote(addr, tx)) + .await + .ok(); + let reply = rx.await.map_err(|_| RemoteStateActorStoppedError::new())?; + match reply { + Ok(()) => Ok(Ok(self.mapped_addrs.endpoint_addrs.get(&remote_id))), + Err(err) => Ok(Err(err)), + } + } + + /// Fetches the [`RemoteInfo`] about a remote from the `RemoteStateActor`. + /// + /// Returns `None` if no actor is running for the remote. + pub(crate) async fn remote_info(&self, id: EndpointId) -> Option { + let (tx, rx) = oneshot::channel(); + self.remote_actors + .get(&id)? + .send(RemoteStateMessage::RemoteInfo(tx)) + .await + .ok()?; + rx.await.ok() + } + + /// Registers the connection in the `RemoteStateActor`. + /// + /// The actor is responsible for holepunching and opening additional paths to this + /// connection. + /// + /// Returns a future that resolves to a [`PathStateReceiver`] for the new connection. + /// + /// The returned future is `'static`, so it can be stored without being lifetime-bound to `&self`. + pub(crate) fn register_connection( + &self, + remote: EndpointId, + conn: noq::Connection, + ) -> impl Future> + Send + 'static + { + let (tx, rx) = oneshot::channel(); + let sender = self.actor_sender.clone(); + async move { + sender + .send(ActorMessage::AddConnection(remote, conn, tx)) + .await + .map_err(|_| RemoteStateActorStoppedError::new())?; + rx.await.map_err(|_| RemoteStateActorStoppedError::new()) + } + } +} + +#[derive(derive_more::Debug)] +#[allow(clippy::enum_variant_names)] +enum ActorMessage { + NetworkChange, + RelayMapChange, + #[debug("ResolveRemote(..)")] + ResolveRemote( + EndpointAddr, + oneshot::Sender>, + ), + #[debug("AddConnection(..)")] + AddConnection( + EndpointId, + noq::Connection, + oneshot::Sender, + ), + /// Re-evaluate direct addresses, e.g. after configured external addresses changed. + DirectAddrRefresh, + #[cfg(all(test, with_crypto_provider))] + ForceNetworkChange(bool), +} + +/// State for polling until a default route is available after a network change. +/// +/// When a network change is detected but no default route exists yet (e.g., +/// interface just came up but gateway not assigned), we poll with exponential +/// backoff until the gateway appears. This avoids the fixed 2s delay that was +/// too slow for interface recovery scenarios. +struct PendingNetworkChangeNotify { + /// Next time to check for default route. + next_check: Instant, + /// Current backoff interval. + interval: Duration, + /// Whether this was a major change. + is_major: bool, + /// When we started polling (to enforce a max wait). + started: Instant, +} + +impl PendingNetworkChangeNotify { + const INITIAL_INTERVAL: Duration = Duration::from_millis(100); + const MAX_INTERVAL: Duration = Duration::from_secs(1); + const MAX_WAIT: Duration = Duration::from_secs(5); + + fn new(is_major: bool) -> Self { + Self { + next_check: Instant::now() + Self::INITIAL_INTERVAL, + interval: Self::INITIAL_INTERVAL, + is_major, + started: Instant::now(), + } + } + + /// Advance to the next check interval (exponential backoff, capped). + fn advance(&mut self) { + self.interval = (self.interval * 2).min(Self::MAX_INTERVAL); + self.next_check = Instant::now() + self.interval; + } + + /// Whether we've exceeded the maximum wait time. + fn expired(&self) -> bool { + self.started.elapsed() >= Self::MAX_WAIT + } +} + +struct Actor { + /// A clone of the quinn Endpoint. + /// + /// The task of this actor is currently owned by the [`crate::Endpoint`] and wrapped in + /// an [`AbortOnDropHandle`]. When [`crate::Endpoint::close`] is called various + /// subsystems are being stopped. Then, when [`ShutdownState::at_endpoint_closed`] is + /// called by [`crate::Endpoint::close`], this actor itself is stopped via it's + /// [`CancellationToken`] and we will drop this clone of the endpoint. The endpoint is + /// then finally dropped when the [`crate::Endpoint`] itself is dropped. + /// + /// All of this to say: keeping the quinn endpoint alive here does not impact the + /// lifetime of it since it's lifetime is shorter than that one that's stored in the + /// [`crate::Endpoint`]. + endpoint: noq::Endpoint, + /// Shared state between an awful lot of iroh subsystems. + /// + /// In particular both the [`EndpointInner`] as well as this actor itself have a + /// copy. But also other subsystems that consequently have access to way to much state. + sock: Arc, + /// Tracks the networkmap endpoint entity for each endpoint discovery key. + remote_map: RemoteMap, + /// When set, is an AfterFunc timer that will call Socket::do_periodic_stun. + periodic_re_stun_timer: time::Interval, + /// An actor watching the local network interfaces. + /// + /// The monitored changes are emitted via [`Self::local_interfaces_watcher`]. + network_monitor: netmon::Monitor, + /// Watcher for changes to the local network interfaces, IP addresses and routes. + local_interfaces_watcher: n0_watcher::Direct, + transports_network_change: transports::NetworkChangeSender, + /// Indicates the direct addr update state. + direct_addr_update_state: DirectAddrUpdateState, + direct_addr_done_rx: mpsc::Receiver<()>, + /// Polling state for [`Actor::notify_quic_network_change`]. + /// + /// When a network change is detected but no default route is available yet, + /// we poll with exponential backoff (100ms, 200ms, 400ms, 800ms, 1s, 1s, ...) + /// until the gateway appears. Once it does, we notify immediately. + /// After 5s total we notify anyway even without a gateway. + call_notify_quic_network_change: Option, +} + +impl Actor { + async fn run( + mut self, + mut msg_receiver: mpsc::Receiver, + shutdown_token: CancellationToken, + mut local_addrs_watcher: impl Watcher> + Send + Sync, + ) { + // Setup network monitoring + let mut current_netmon_state = self.local_interfaces_watcher.get(); + + let mut portmap_watcher = self + .direct_addr_update_state + .port_mapper + .watch_external_address(); + + let mut receiver_closed = false; + let mut portmap_watcher_closed = false; + + let mut net_report_watcher = self.sock.net_report.watch(); + + // ensure we are doing an initial publish of our addresses + self.sock.publish_my_addr(); + + while !shutdown_token.is_cancelled() { + self.sock.metrics.socket.actor_tick_main.inc(); + let portmap_watcher_changed = portmap_watcher.changed(); + + let notify_quic_network_change = match &self.call_notify_quic_network_change { + Some(pending) => { + MaybeFuture::Some(n0_future::time::sleep_until(pending.next_check)) + } + None => MaybeFuture::None, + }; + n0_future::pin!(notify_quic_network_change); + + tokio::select! { + _ = shutdown_token.cancelled() => { + debug!("tick: shutting down"); + return; + } + msg = msg_receiver.recv(), if !receiver_closed => { + let Some(msg) = msg else { + trace!("tick: socket receiver closed"); + self.sock.metrics.socket.actor_tick_other.inc(); + receiver_closed = true; + continue; + }; + + trace!(?msg, "tick: msg"); + self.sock.metrics.socket.actor_tick_msg.inc(); + self.handle_actor_message(msg).await; + } + tick = self.periodic_re_stun_timer.tick() => { + trace!("tick: re_stun {:?}", tick); + self.sock.metrics.socket.actor_tick_re_stun.inc(); + self.re_stun(UpdateReason::Periodic); + } + new_addr = local_addrs_watcher.updated() => { + match new_addr { + Ok(addrs) => { + if !addrs.is_empty() { + trace!(?addrs, "local addrs"); + self.sock.publish_my_addr(); + } + } + Err(_) => { + warn!("local addr watcher stopped"); + } + } + } + report = net_report_watcher.updated() => { + match report { + Ok((report, _)) => { + self.handle_net_report_report(report); + #[cfg(not(wasm_browser))] + { + self.periodic_re_stun_timer = new_re_stun_timer(true); + } + } + Err(_) => { + warn!("net report watcher stopped"); + } + } + } + reason = self.direct_addr_done_rx.recv() => { + match reason { + Some(()) => { + // check if a new run needs to be scheduled + let state = self.local_interfaces_watcher.get(); + self.direct_addr_update_state.try_run(state.into()); + } + None => { + warn!("direct addr watcher died"); + } + } + } + change = portmap_watcher_changed, if !portmap_watcher_closed => { + if change.is_err() { + trace!("tick: portmap watcher closed"); + self.sock.metrics.socket.actor_tick_other.inc(); + + portmap_watcher_closed = true; + continue; + } + + trace!("tick: portmap changed"); + self.sock.metrics.socket.actor_tick_portmap_changed.inc(); + let new_external_address = *portmap_watcher.borrow(); + if new_external_address.is_some() { + self.sock.metrics.net_report.portmap_external_address_updated.inc(); + } + debug!("external address updated: {new_external_address:?}"); + self.re_stun(UpdateReason::PortmapUpdated); + }, + state = self.local_interfaces_watcher.updated() => { + let Ok(state) = state else { + trace!("tick: link change receiver closed"); + self.sock.metrics.socket.actor_tick_other.inc(); + continue; + }; + let is_major = state.is_major_change(¤t_netmon_state); + event!( + target: "iroh::_events::link_change", + Level::DEBUG, + ?state, + is_major + ); + current_netmon_state = state; + self.sock.metrics.socket.actor_link_change.inc(); + self.handle_network_change(is_major); + } + _remote_id = self.remote_map.cleanup() => {}, + _ = &mut notify_quic_network_change => { + let has_network = self.has_usable_network(); + let Some(pending) = self.call_notify_quic_network_change.as_mut() else { + continue; + }; + if has_network || pending.expired() { + // Gateway appeared or we've waited long enough, notify now. + let is_major = pending.is_major; + self.call_notify_quic_network_change = None; + self.notify_quic_network_change(is_major); + } else { + // No gateway yet, back off and try again. + trace!( + interval = ?pending.interval, + elapsed = ?pending.started.elapsed(), + "no default route yet, retrying" + ); + pending.advance(); + } + } + else => { + trace!("tick: else"); + } + } + } + } + + /// Whether the local network has a default route and at least one IP address. + fn has_usable_network(&mut self) -> bool { + #[cfg(target_family = "wasm")] + { + true + } + #[cfg(not(target_family = "wasm"))] + { + let interfaces = self.local_interfaces_watcher.get(); + interfaces.default_route_interface.is_some() + && (interfaces.have_v4 || interfaces.have_v6) + } + } + + /// Handles a change detected in the local network conditions. + /// + /// This is triggered when the netmon actor detects a change in the local network + /// interfaces, assigned IP addresses and routes. + fn handle_network_change(&mut self, is_major: bool) { + debug!(is_major, "link change detected"); + + if is_major { + if let Err(err) = self.transports_network_change.rebind() { + warn!("failed to rebind transports: {err:?}"); + } + self.transports_network_change.check_relay_connection(); + + #[cfg(not(wasm_browser))] + self.sock.dns_resolver.reset(); + self.re_stun(UpdateReason::LinkChangeMajor); + } else { + self.re_stun(UpdateReason::LinkChangeMinor); + } + + if self.has_usable_network() { + // This is considered a usable network change, propagate it to the QUIC stack + // right away. + self.call_notify_quic_network_change = None; + self.notify_quic_network_change(is_major); + } else { + // No default route yet (e.g., interface just came up but gateway not + // assigned). Poll with exponential backoff until the gateway appears. + match &mut self.call_notify_quic_network_change { + Some(pending) => { + // Update is_major if this change is more severe. + pending.is_major |= is_major; + } + None => { + self.call_notify_quic_network_change = + Some(PendingNetworkChangeNotify::new(is_major)); + } + } + } + } + + /// Notifies the QUIC stack of the network change we observed. + /// + /// This is decoupled from receiving the network change, because we try to debounce + /// network changes as they often arrive in groups. + fn notify_quic_network_change(&mut self, is_major: bool) { + #[derive(Debug)] + struct Hint { + local_addrs: FxHashSet, + } + + impl NetworkChangeHint for Hint { + fn is_path_recoverable( + &self, + _path_id: noq::PathId, + network_path: noq_proto::FourTuple, + ) -> bool { + match MultipathMappedAddr::from(network_path.remote()) { + MultipathMappedAddr::Mixed(_) => { + // This address is only ever used to send an Initial packet to, it + // should never appear as an established path. + error!("A mixed address can not be used for network changes"); + false + } + MultipathMappedAddr::Relay(_) => { + // We pretend the relay path is never affected by link changes. The + // relay actor transparently reconnects and the addresses never + // change. + true + } + MultipathMappedAddr::Ip(_) => { + // If we no longer have a valid interface to send from for a local + // IP then it can not be recovered. + match network_path.local_ip() { + Some(local_ip) => self.local_addrs.contains(&local_ip), + None => true, + } + } + MultipathMappedAddr::Custom(_) => { + // Assume it is unrecoverable for now + false + } + } + } + } + + let hint = Hint { + #[cfg(not(wasm_browser))] + local_addrs: { + let interfaces = self.local_interfaces_watcher.get(); + interfaces + .local_addresses + .regular + .iter() + .chain(interfaces.local_addresses.loopback.iter()) + .copied() + .collect() + }, + #[cfg(wasm_browser)] + local_addrs: Default::default(), + }; + + self.endpoint.handle_network_change(Some(Arc::new(hint))); + self.remote_map.on_network_change(is_major); + } + + fn handle_relay_map_change(&mut self) { + self.re_stun(UpdateReason::RelayMapChange); + } + + fn re_stun(&mut self, why: UpdateReason) { + let state = self.local_interfaces_watcher.get(); + self.direct_addr_update_state + .schedule_run(why, state.into()); + } + + /// Processes an incoming actor message. + /// + /// Returns `true` if it was a shutdown. + async fn handle_actor_message(&mut self, msg: ActorMessage) { + match msg { + ActorMessage::NetworkChange => { + self.network_monitor.network_change().await.ok(); + } + ActorMessage::RelayMapChange => { + self.handle_relay_map_change(); + } + ActorMessage::ResolveRemote(addr, tx) => { + self.remote_map.resolve_remote(addr, tx).await; + } + ActorMessage::AddConnection(remote, conn, tx) => { + self.remote_map.add_connection(remote, conn, tx).await; + } + ActorMessage::DirectAddrRefresh => { + #[cfg(not(wasm_browser))] + { + let (report, _reason) = self.sock.net_report.get(); + self.update_direct_addresses(report.as_ref()); + } + } + #[cfg(all(test, with_crypto_provider))] + ActorMessage::ForceNetworkChange(is_major) => { + self.handle_network_change(is_major); + } + } + } + + /// Updates the direct addresses of this socket. + /// + /// Updates the [`DiscoveredDirectAddrs`] of this [`Socket`] with the current set of + /// direct addresses from: + /// + /// - The portmapper. + /// - A net_report report. + /// - The local interfaces IP addresses. + /// - User configured addresses. + #[cfg(not(wasm_browser))] + fn update_direct_addresses(&mut self, net_report_report: Option<&net_report::Report>) { + // We only want to have one DirectAddr for each SocketAddr we have. So we store + // this as a map of SocketAddr -> DirectAddrType. At the end we will construct a + // DirectAddr from each entry. + let mut addrs: BTreeMap)> = + BTreeMap::new(); + + // First add PortMapper provided addresses. + let portmap_watcher = self + .direct_addr_update_state + .port_mapper + .watch_external_address(); + let maybe_port_mapped = *portmap_watcher.borrow(); + if let Some(portmap_ext) = maybe_port_mapped.map(SocketAddr::V4) { + addrs + .entry(portmap_ext) + .or_insert((DirectAddrType::Portmapped, None)); + } + + // Next add STUN addresses from the net_report report. + if let Some(net_report_report) = net_report_report { + if let Some(global_v4) = net_report_report.global_v4 { + addrs + .entry(global_v4.into()) + .or_insert((DirectAddrType::Qad, None)); + + // If they're behind a hard NAT and are using a fixed + // port locally, assume they might've added a static + // port mapping on their router to the same explicit + // port that we are running with. Worst case it's an invalid candidate mapping. + let port = self.sock.ip_bind_addrs().iter().find_map(|addr| { + if addr.port() != 0 { + Some(addr.port()) + } else { + None + } + }); + + if let Some(port) = port + && net_report_report + .mapping_varies_by_dest() + .unwrap_or_default() + { + let mut addr = global_v4; + addr.set_port(port); + addrs + .entry(addr.into()) + .or_insert((DirectAddrType::Qad4LocalPort, None)); + } + } + if let Some(global_v6) = net_report_report.global_v6 { + addrs + .entry(global_v6.into()) + .or_insert((DirectAddrType::Qad, None)); + } + } + + self.collect_local_addresses(&mut addrs); + + // Add configured external addresses. + for addr in self.sock.configured_addrs.read().expect("poisoned").iter() { + addrs.entry(*addr).or_insert((DirectAddrType::Config, None)); + } + + // Finally create and store store all these direct addresses + let stored_addrs = addrs + .into_iter() + .filter_map(|(addr, (typ, flags))| { + // Filter out deprecated IPs + let is_deprecated = flags.map(|f| f.deprecated).unwrap_or(false); + if is_deprecated { + return None; + } + Some(DirectAddr { addr, typ }) + }) + .collect(); + self.sock.store_direct_addresses(stored_addrs); + } + + #[cfg(not(wasm_browser))] + fn collect_local_addresses( + &mut self, + addrs: &mut BTreeMap)>, + ) { + let netmon_state = self.local_interfaces_watcher.get(); + + // Matches the addresses that have been bound vs the requested ones. + let local_addrs: Vec<(SocketAddr, SocketAddr)> = self + .sock + .ip_bind_addrs() + .iter() + .copied() + .zip(self.sock.ip_local_addrs()) + .collect(); + + // Do we listen on any IPv4 unspecified address? + let has_ipv4_unspecified = local_addrs.iter().find_map(|(_, a)| { + if a.is_ipv4() && a.ip().is_unspecified() { + Some(a.port()) + } else { + None + } + }); + // Do we listen on any IPv6 unspecified address? + let has_ipv6_unspecified = local_addrs.iter().find_map(|(_, a)| { + if a.is_ipv6() && a.ip().is_unspecified() { + Some(a.port()) + } else { + None + } + }); + + // If a socket is bound to the unspecified address, create SocketAddrs for + // each local IP address by pairing it with the port the socket is bound on. + if local_addrs + .iter() + .any(|(_, local)| local.ip().is_unspecified()) + { + let LocalAddresses { + regular: mut ips, + loopback, + } = self.local_interfaces_watcher.get().local_addresses; + if ips.is_empty() && addrs.is_empty() { + // Include loopback addresses only if there are no other interfaces + // or public addresses, this allows testing offline. + ips = loopback; + } + + for ip in ips { + let port_if_unspecified = match ip { + IpAddr::V4(_) => has_ipv4_unspecified, + IpAddr::V6(_) => has_ipv6_unspecified, + }; + if let Some(port) = port_if_unspecified { + let addr = SocketAddr::new(ip, port); + let flags = find_flags(&netmon_state, ip); + addrs.entry(addr).or_insert((DirectAddrType::Local, flags)); + } + } + } + + // If a socket is bound to a specific address, add it. + for (bound, local) in local_addrs { + if !bound.ip().is_unspecified() { + let flags = find_flags(&netmon_state, local.ip()); + addrs.entry(local).or_insert((DirectAddrType::Local, flags)); + } + } + } + + fn handle_net_report_report(&mut self, mut report: Option) { + if let Some(ref mut r) = report { + self.sock.ipv6_reported.store(r.udp_v6, Ordering::Relaxed); + if r.preferred_relay.is_none() + && let Some(my_relay) = self.sock.my_relay() + { + r.preferred_relay.replace(my_relay); + } + + // Notify all transports + self.transports_network_change.on_network_change(r); + } + + #[cfg(not(wasm_browser))] + self.update_direct_addresses(report.as_ref()); + } +} + +#[cfg(not(wasm_browser))] +fn find_flags(state: &netmon::State, ip: IpAddr) -> Option { + if ip.is_ipv6() { + state + .interfaces + .values() + .flat_map(|i| i.addrs()) + .find_map(|addr| match addr { + IpNet::V4(_) => None, + IpNet::V6 { net, flags, .. } => { + if net.addr() == ip { + Some(flags) + } else { + None + } + } + }) + } else { + None + } +} + +fn new_re_stun_timer(initial_delay: bool) -> time::Interval { + // Pick a random duration between 20 and 26 seconds (just under 30s, + // a common UDP NAT timeout on Linux,etc) + let mut rng = rand::rng(); + let d: Duration = rng.random_range(Duration::from_secs(20)..=Duration::from_secs(26)); + if initial_delay { + debug!("scheduling periodic_stun to run in {}s", d.as_secs()); + time::interval_at(time::Instant::now() + d, d) + } else { + debug!( + "scheduling periodic_stun to run immediately and in {}s", + d.as_secs() + ); + time::interval(d) + } +} + +/// The discovered direct addresses of this [`Socket`]. +/// +/// These are all the [`DirectAddr`]s that this [`Socket`] is aware of for itself. +/// They include all locally bound ones as well as those discovered by other mechanisms like +/// QAD. +#[derive(derive_more::Debug, Clone, Default)] +struct DiscoveredDirectAddrs { + /// The last set of discovered direct addresses. + addrs: Watchable>, + + /// The last time the direct addresses were updated, even if there was no change. + /// + /// This is only ever None at startup. + updated_at: Arc>>, +} + +impl DiscoveredDirectAddrs { + /// Updates the direct addresses, returns `true` if they changed, `false` if not. + fn update(&self, addrs: BTreeSet) -> bool { + *self.updated_at.write().expect("poisoned") = Some(Instant::now()); + let updated = self.addrs.set(addrs).is_ok(); + if updated { + event!( + target: "iroh::_events::direct_addrs", + Level::DEBUG, + addrs = ?self.addrs.get(), + ); + } + updated + } + + fn sockaddrs(&self) -> impl Iterator { + self.addrs.get().into_iter().map(|da| da.addr) + } +} + +/// A *direct address* on which an iroh-endpoint might be contactable. +/// +/// Direct addresses are UDP socket addresses on which an iroh endpoint could potentially be +/// contacted. These can come from various sources depending on the network topology of the +/// iroh endpoint, see [`DirectAddrType`] for the several kinds of sources. +/// +/// This is essentially a combination of our local addresses combined with any reflexive +/// transport addresses we discovered using QAD. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DirectAddr { + /// The address. + pub addr: SocketAddr, + /// The origin of this direct address. + pub typ: DirectAddrType, +} + +/// The type of direct address. +/// +/// These are the various sources or origins from which an iroh endpoint might have found a +/// possible [`DirectAddr`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[non_exhaustive] +pub enum DirectAddrType { + /// Not yet determined.. + Unknown, + /// A locally bound socket address. + Local, + /// Public internet address discovered via QAD. + /// + /// When possible an iroh endpoint will perform QAD to discover which is the address + /// from which it sends data on the public internet. This can be different from locally + /// bound addresses when the endpoint is on a local network which performs NAT or similar. + Qad, + /// An address assigned by the router using port mapping. + /// + /// When possible an iroh endpoint will request a port mapping from the local router to + /// get a publicly routable direct address. + Portmapped, + /// Hard NAT: QAD'ed IPv4 address + local fixed port. + /// + /// It is possible to configure iroh to bound to a specific port and independently + /// configure the router to forward this port to the iroh endpoint. This indicates a + /// situation like this, which still uses QAD to discover the public address. + Qad4LocalPort, + /// An address explicitly provided by the user via configuration. + Config, +} + +impl Display for DirectAddrType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DirectAddrType::Unknown => write!(f, "?"), + DirectAddrType::Local => write!(f, "local"), + DirectAddrType::Qad => write!(f, "qad"), + DirectAddrType::Portmapped => write!(f, "portmap"), + DirectAddrType::Qad4LocalPort => write!(f, "qad4localport"), + DirectAddrType::Config => write!(f, "config"), + } + } +} + +#[cfg(all(test, with_crypto_provider))] +mod tests { + use std::{net::SocketAddrV4, sync::Arc, time::Duration}; + + use data_encoding::HEXLOWER; + use iroh_base::{EndpointAddr, EndpointId, TransportAddr}; + use iroh_relay::tls::{CaTlsConfig, default_provider}; + use n0_error::{Result, StackResultExt, StdResultExt}; + use n0_future::{MergeBounded, StreamExt, time}; + use n0_tracing_test::traced_test; + use n0_watcher::Watcher; + use rand::{CryptoRng, Rng, RngExt, SeedableRng}; + use tokio_util::task::AbortOnDropHandle; + use tracing::{Instrument, error, info, info_span, instrument}; + + use super::Options; + use crate::{ + Endpoint, SecretKey, + address_lookup::memory::MemoryLookup, + dns::DnsResolver, + endpoint::{QuicTransportConfig, presets}, + socket::{ + EndpointInner, StaticConfig, TransportConfig, + biased_rtt_path_selector::BiasedRttPathSelector, + mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, + }, + tls::{self, DEFAULT_MAX_TLS_TICKETS, misc::RustlsTokenKey}, + }; + + const ALPN: &[u8] = b"n0/test/1"; + + fn default_options(rng: &mut impl CryptoRng) -> Options { + let crypto_provider = default_provider(); + let secret_key = SecretKey::from_bytes(&rng.random()); + let tls_config = tls::TlsConfig::new( + secret_key.clone(), + DEFAULT_MAX_TLS_TICKETS, + crypto_provider.clone(), + ); + let static_config = StaticConfig { + server_config: tls_config.make_server_config(false).unwrap(), + client_config: tls_config.make_client_config(false).unwrap(), + tls_config, + token_key: Arc::new(RustlsTokenKey::new(rng, &crypto_provider).unwrap()), + token_store: Arc::new(noq::TokenMemoryCache::default()), + transport_config: QuicTransportConfig::default(), + }; + let server_config = static_config.create_server_config(vec![]); + Options { + transports: vec![ + TransportConfig::default_ipv4(), + TransportConfig::default_ipv6(), + ], + secret_key, + proxy_url: None, + dns_resolver: DnsResolver::new(), + server_config, + tls_config: CaTlsConfig::default() + .client_config(crypto_provider.clone()) + .unwrap(), + #[cfg(any(test, feature = "test-utils"))] + address_lookup_user_data: None, + metrics: Default::default(), + hooks: Default::default(), + path_selector: Arc::new(BiasedRttPathSelector::default()), + portmapper_config: Default::default(), + net_report_config: Default::default(), + static_config, + configured_addrs: Default::default(), + } + } + + #[instrument(skip_all, fields(me = %ep.id().fmt_short()))] + async fn echo_receiver(ep: Endpoint, loss: ExpectedLoss) -> Result { + info!("accepting conn"); + let conn = ep.accept().await.expect("no conn"); + + info!("accepting"); + let conn = conn.await.context("accepting")?; + info!("accepting bi"); + let (mut send_bi, mut recv_bi) = conn.accept_bi().await.std_context("accept bi")?; + + info!("reading"); + let val = recv_bi + .read_to_end(usize::MAX) + .await + .std_context("read to end")?; + + info!("replying"); + for chunk in val.chunks(12) { + send_bi.write_all(chunk).await.std_context("write all")?; + } + + info!("finishing"); + send_bi.finish().std_context("finish")?; + + let stats = conn.stats(); + info!("stats: {:#?}", stats); + if matches!(loss, ExpectedLoss::AlmostNone) { + for info in conn.paths().iter() { + assert!( + info.stats().lost_packets < 10, + "[receiver] path {:?} should not loose many packets", + info.remote_addr() + ); + } + } + + conn.closed().await; + info!("closed"); + ep.inner()?.noq_endpoint().wait_idle().await; + info!("idle"); + + Ok(()) + } + + #[instrument(skip_all, fields(me = %ep.id().fmt_short()))] + async fn echo_sender( + ep: Endpoint, + dest_id: EndpointId, + msg: &[u8], + loss: ExpectedLoss, + ) -> Result { + info!("connecting to {}", dest_id.fmt_short()); + let dest = EndpointAddr::new(dest_id); + let conn = ep.connect(dest, ALPN).await?; + + info!("opening bi"); + let (mut send_bi, mut recv_bi) = conn.open_bi().await.std_context("open bi")?; + + info!("writing message"); + send_bi.write_all(msg).await.std_context("write all")?; + + info!("finishing"); + send_bi.finish().std_context("finish")?; + + info!("reading_to_end"); + let val = recv_bi + .read_to_end(usize::MAX) + .await + .std_context("read to end")?; + assert_eq!( + val, + msg, + "[sender] expected {}, got {}", + HEXLOWER.encode(msg), + HEXLOWER.encode(&val) + ); + + let stats = conn.stats(); + info!("stats: {:#?}", stats); + if matches!(loss, ExpectedLoss::AlmostNone) { + for info in conn.paths().iter() { + assert!( + info.stats().lost_packets < 10, + "[sender] path {:?} should not loose many packets", + info.remote_addr() + ); + } + } + + conn.close(0u32.into(), b"done"); + info!("closed"); + ep.inner()?.noq_endpoint().wait_idle().await; + info!("idle"); + Ok(()) + } + + #[derive(Debug, Copy, Clone)] + enum ExpectedLoss { + AlmostNone, + YeahSure, + } + + /// Runs a roundtrip between the [`echo_sender`] and [`echo_receiver`]. + async fn run_roundtrip( + sender: Endpoint, + receiver: Endpoint, + payload: &[u8], + loss: ExpectedLoss, + ) -> Result<()> { + tokio::time::timeout(Duration::from_secs(20), async move { + let send_endpoint_id = sender.id(); + let recv_endpoint_id = receiver.id(); + info!("\nroundtrip: {send_endpoint_id:#} -> {recv_endpoint_id:#}"); + + let receiver_task = AbortOnDropHandle::new(tokio::spawn(echo_receiver(receiver, loss))); + let sender_res = echo_sender(sender, recv_endpoint_id, payload, loss).await; + let sender_is_err = match sender_res { + Ok(()) => false, + Err(err) => { + error!("[sender] Error:\n{err:#?}"); + true + } + }; + let receiver_is_err = match receiver_task.await { + Ok(Ok(())) => false, + Ok(Err(err)) => { + error!("[receiver] Error:\n{err:#?}"); + true + } + Err(joinerr) => { + if joinerr.is_panic() { + std::panic::resume_unwind(joinerr.into_panic()); + } else { + error!("[receiver] Error:\n{joinerr:#?}"); + } + true + } + }; + if sender_is_err || receiver_is_err { + panic!("Sender or receiver errored"); + } + }) + .await + .std_context("timeout")?; + Ok(()) + } + + /// Returns a pair of endpoints with a shared [`MemoryLookup`]. + /// + /// The endpoints do not use a relay server but can connect to each other via local + /// addresses. Dialing by [`EndpointId`] is possible, and the addresses get updated even if + /// the endpoints rebind. + async fn endpoint_pair() -> (AbortOnDropHandle<()>, Endpoint, Endpoint) { + let address_lookup = MemoryLookup::new(); + let ep1 = Endpoint::builder(presets::Minimal) + .alpns(vec![ALPN.to_vec()]) + .address_lookup(address_lookup.clone()) + .bind() + .await + .unwrap(); + let ep2 = Endpoint::builder(presets::Minimal) + .alpns(vec![ALPN.to_vec()]) + .address_lookup(address_lookup.clone()) + .bind() + .await + .unwrap(); + address_lookup.add_endpoint_info(ep1.addr()); + address_lookup.add_endpoint_info(ep2.addr()); + + let ep1_addr_stream = ep1.watch_addr().stream(); + let ep2_addr_stream = ep2.watch_addr().stream(); + let mut addr_stream = MergeBounded::from_iter([ep1_addr_stream, ep2_addr_stream]); + let task = tokio::spawn(async move { + while let Some(addr) = addr_stream.next().await { + address_lookup.add_endpoint_info(addr); + } + }); + + (AbortOnDropHandle::new(task), ep1, ep2) + } + + #[tokio::test(flavor = "multi_thread")] + #[traced_test] + async fn test_two_devices_roundtrip_noq_small() -> Result { + let (_guard, m1, m2) = endpoint_pair().await; + + run_roundtrip( + m1.clone(), + m2.clone(), + b"hello m1", + ExpectedLoss::AlmostNone, + ) + .await?; + run_roundtrip( + m2.clone(), + m1.clone(), + b"hello m2", + ExpectedLoss::AlmostNone, + ) + .await?; + Ok(()) + } + + #[tokio::test(flavor = "multi_thread")] + #[traced_test] + async fn test_two_devices_roundtrip_noq_large() -> Result { + let (_guard, m1, m2) = endpoint_pair().await; + let mut data = vec![0u8; 10 * 1024]; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + rng.fill_bytes(&mut data); + run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::AlmostNone).await?; + run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::AlmostNone).await?; + + Ok(()) + } + + // EADDRINUSE on the GitHub Android emulator persists past + // `force_network_change()`, so the rebind fails and connect() never + // wakes the connection driver. Passes locally. + #[cfg_attr( + target_os = "android", + ignore = "rebind flakes against the GitHub Android emulator" + )] + #[tokio::test] + #[traced_test] + async fn test_regression_network_change_rebind_wakes_connection_driver() -> Result { + let (_guard, m1, m2) = endpoint_pair().await; + + println!("Net change"); + m1.inner()?.force_network_change(true).await; + tokio::time::sleep(Duration::from_secs(1)).await; // wait for socket rebinding + + let _handle = AbortOnDropHandle::new(tokio::spawn({ + let endpoint = m2.clone(); + async move { + while let Some(incoming) = endpoint.accept().await { + println!("Incoming first conn!"); + let conn = incoming.await.anyerr()?; + conn.closed().await; + } + + n0_error::Ok(()) + } + })); + + println!("first conn!"); + let conn = m1.connect(m2.addr(), ALPN).await?; + println!("Closing first conn"); + conn.close(0u32.into(), b"bye lolz"); + conn.closed().await; + println!("Closed first conn"); + + Ok(()) + } + + fn offset(rng: &mut rand_chacha::ChaCha8Rng) -> Duration { + let delay = rng.random_range(1..=5); + Duration::from_millis(delay * 50) + } + + /// Same structure as `test_two_devices_roundtrip_noq`, but interrupts regularly + /// with (simulated) network changes. + /// Regular network changes to m1 only. + #[tokio::test(flavor = "multi_thread")] + #[traced_test] + async fn test_two_devices_roundtrip_network_change_only_a() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let (_guard, m1, m2) = endpoint_pair().await; + + let _network_change_guard = { + let m1 = m1.clone(); + let mut rng = rng.clone(); + let task = tokio::spawn(async move { + loop { + info!("[m1] network change"); + m1.inner() + .expect("haven't closed the endpoint yet") + .force_network_change(true) + .await; + time::sleep(offset(&mut rng)).await; + } + }); + AbortOnDropHandle::new(task) + }; + + let mut data = vec![0u8; 10 * 1024]; + rng.fill_bytes(&mut data); + run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::YeahSure).await?; + run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::YeahSure).await?; + + Ok(()) + } + + /// Regular network changes to both m1 and m2. + #[tokio::test(flavor = "multi_thread")] + #[traced_test] + async fn test_two_devices_roundtrip_network_change_a_and_b() -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let (_guard, m1, m2) = endpoint_pair().await; + + let _network_change_guard = { + let m1 = m1.clone(); + let m2 = m2.clone(); + let mut rng = rng.clone(); + let task = tokio::spawn(async move { + info!("-- [m1] network change"); + m1.inner() + .expect("haven't closed the endpoint yet") + .force_network_change(true) + .await; + info!("-- [m2] network change"); + m2.inner() + .expect("haven't closed the endpoint yet") + .force_network_change(true) + .await; + time::sleep(offset(&mut rng)).await; + }); + AbortOnDropHandle::new(task) + }; + + let mut data = vec![0u8; 10 * 1024]; + rng.fill_bytes(&mut data); + run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::YeahSure).await?; + run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::YeahSure).await?; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread")] + #[traced_test] + async fn test_two_devices_setup_teardown() -> Result { + for i in 0..10 { + info!("-- round {i}"); + info!("setting up stack"); + let (_guard, m1, m2) = endpoint_pair().await; + + info!("closing endpoints"); + let sock1 = m1.inner()?; + let sock2 = m2.inner()?; + m1.close().await; + m2.close().await; + + assert!(sock1.is_closed()); + assert!(sock2.is_closed()); + } + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_direct_addresses() { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let sock = EndpointInner::bind(default_options(&mut rng)) + .await + .unwrap(); + + // See if we can get endpoints. + let eps0 = sock.ip_addrs().get(); + info!("{eps0:?}"); + assert!(!eps0.is_empty()); + + // Getting the endpoints again immediately should give the same results. + let eps1 = sock.ip_addrs().get(); + info!("{eps1:?}"); + assert_eq!(eps0, eps1); + } + + /// Creates a new [`noq::Endpoint`] hooked up to a [`Socket`]. + /// + /// This is without involving [`crate::endpoint::Endpoint`]. The socket will accept + /// connections using [`ALPN`]. + /// + /// Use [`socket_connect`] to establish connections. + #[instrument(name = "ep", skip_all, fields(me = %secret_key.public().fmt_short()))] + async fn socket_ep(secret_key: SecretKey) -> Result { + let crypto_provider = default_provider(); + let tls_config = tls::TlsConfig::new( + secret_key.clone(), + DEFAULT_MAX_TLS_TICKETS, + crypto_provider.clone(), + ); + let keylog = true; + let static_config = StaticConfig { + server_config: tls_config.make_server_config(keylog).unwrap(), + client_config: tls_config.make_client_config(keylog).unwrap(), + tls_config, + token_key: Arc::new(RustlsTokenKey::new(&mut rand::rng(), &crypto_provider).unwrap()), + token_store: Arc::new(noq::TokenMemoryCache::default()), + transport_config: QuicTransportConfig::default(), + }; + let server_config = static_config.create_server_config(vec![ALPN.to_vec()]); + + let dns_resolver = DnsResolver::new(); + let opts = Options { + transports: vec![ + TransportConfig::default_ipv4(), + TransportConfig::default_ipv6(), + ], + secret_key: secret_key.clone(), + address_lookup_user_data: None, + dns_resolver, + proxy_url: None, + server_config, + tls_config: CaTlsConfig::default() + .client_config(crypto_provider.clone()) + .unwrap(), + metrics: Default::default(), + hooks: Default::default(), + path_selector: Arc::new(BiasedRttPathSelector::default()), + portmapper_config: Default::default(), + net_report_config: Default::default(), + static_config, + configured_addrs: Default::default(), + }; + let sock = EndpointInner::bind(opts).await?; + Ok(sock) + } + + /// Connects from `ep` returned by [`socket_ep`] to the `endpoint_id`. + /// + /// Uses [`ALPN`], `endpoint_id`, must match `addr`. + #[instrument(name = "connect", skip_all, fields(me = %ep_secret_key.public().fmt_short()))] + async fn socket_connect( + ep: noq::Endpoint, + ep_secret_key: SecretKey, + addr: EndpointIdMappedAddr, + endpoint_id: EndpointId, + ) -> Result { + // Endpoint::connect sets this, do the same to have similar behaviour. + let mut transport_config = noq::TransportConfig::default(); + transport_config.server_handshake_migration(true); + transport_config.keep_alive_interval(Some(Duration::from_secs(1))); + + socket_connect_with_transport_config( + ep, + ep_secret_key, + addr, + endpoint_id, + Arc::new(transport_config), + ) + .await + } + + /// Connects from `ep` returned by [`socket_ep`] to the `endpoint_id`. + /// + /// This version allows customising the transport config. + /// + /// Uses [`ALPN`], `endpoint_id`, must match `addr`. + #[instrument(name = "connect", skip_all, fields(me = %ep_secret_key.public().fmt_short()))] + async fn socket_connect_with_transport_config( + ep: noq::Endpoint, + ep_secret_key: SecretKey, + mapped_addr: EndpointIdMappedAddr, + endpoint_id: EndpointId, + transport_config: Arc, + ) -> Result { + let mut quic_client_config = tls::TlsConfig::new( + ep_secret_key.clone(), + DEFAULT_MAX_TLS_TICKETS, + default_provider(), + ) + .make_client_config(true)?; + quic_client_config.set_alpn_protocols(vec![ALPN.to_vec()]); + let mut client_config = noq::ClientConfig::new(Arc::new(quic_client_config)); + client_config.transport_config(transport_config); + let connect = ep + .connect_with( + client_config, + mapped_addr.private_socket_addr(), + &tls::name::encode(endpoint_id), + ) + .std_context("connect")?; + let connection = connect.await.anyerr()?; + Ok(connection) + } + + #[tokio::test] + #[traced_test] + async fn test_try_send_no_send_addr() { + // Regression test: if there is no send_addr we should keep being able to use the + // Endpoint. + + let secret_key_1 = SecretKey::from_bytes(&[1u8; 32]); + let secret_key_2 = SecretKey::from_bytes(&[2u8; 32]); + let endpoint_id_2 = secret_key_2.public(); + let secret_key_missing_endpoint = SecretKey::from_bytes(&[255u8; 32]); + let endpoint_id_missing_endpoint = secret_key_missing_endpoint.public(); + + let sock_1 = socket_ep(secret_key_1.clone()).await.unwrap(); + + // Generate an address not present in the RemoteMap. + let bad_addr = EndpointIdMappedAddr::generate(); + + // 500ms is rather fast here. Running this locally it should always be the correct + // timeout. If this is too slow however the test will not become flaky as we are + // expecting the timeout, we might just get the timeout for the wrong reason. But + // this speeds up the test. + let res = tokio::time::timeout( + Duration::from_millis(500), + socket_connect( + sock_1.noq_endpoint().clone(), + secret_key_1.clone(), + bad_addr, + endpoint_id_missing_endpoint, + ), + ) + .await; + assert!(res.is_err(), "expecting timeout"); + + // Now check we can still create another connection with this endpoint. + let sock_2 = socket_ep(secret_key_2.clone()).await.unwrap(); + + // This needs an accept task + let accept_task = tokio::spawn({ + async fn accept(ep: noq::Endpoint) -> Result<()> { + let incoming = ep.accept().await.std_context("no incoming")?; + let _conn = incoming + .accept() + .std_context("accept")? + .await + .std_context("accepting")?; + + // Keep this connection alive for a while + tokio::time::sleep(Duration::from_secs(10)).await; + info!("accept finished"); + Ok(()) + } + let ep = sock_2.noq_endpoint().clone(); + async move { + if let Err(err) = accept(ep).await { + error!("{err:#}"); + } + } + .instrument(info_span!("ep2.accept, me = endpoint_id_2.fmt_short()")) + }); + let _accept_task = AbortOnDropHandle::new(accept_task); + + let addrs = sock_2 + .ip_addrs() + .get() + .into_iter() + .map(|x| TransportAddr::Ip(x.addr)); + let endpoint_addr_2 = EndpointAddr::from_parts(endpoint_id_2, addrs); + let addr = sock_1 + .resolve_remote(endpoint_addr_2) + .await + .unwrap() + .unwrap(); + let res = tokio::time::timeout( + Duration::from_secs(10), + socket_connect( + sock_1.noq_endpoint().clone(), + secret_key_1.clone(), + addr, + endpoint_id_2, + ), + ) + .await + .expect("timeout while connecting"); + + // aka assert!(res.is_ok()) but with nicer error reporting. + res.unwrap(); + + // TODO: Now check if we can connect to a repaired ep_3, but we can't modify that + // much internal state for now. + } + + #[tokio::test] + #[traced_test] + async fn test_try_send_no_udp_addr_or_relay_url() { + // This specifically tests the `if udp_addr.is_none() && relay_url.is_none()` + // behaviour of Socket::try_send. + + let secret_key_1 = SecretKey::from_bytes(&[1u8; 32]); + let secret_key_2 = SecretKey::from_bytes(&[2u8; 32]); + let endpoint_id_2 = secret_key_2.public(); + + let sock_1 = socket_ep(secret_key_1.clone()).await.unwrap(); + let sock_2 = socket_ep(secret_key_2.clone()).await.unwrap(); + let ep_2 = sock_2.noq_endpoint().clone(); + + // We need a task to accept the connection. + let accept_task = tokio::spawn({ + async fn accept(ep: noq::Endpoint) -> Result<()> { + let incoming = ep.accept().await.std_context("no incoming")?; + let conn = incoming + .accept() + .std_context("accept")? + .await + .std_context("connecting")?; + let mut stream = conn.accept_uni().await.std_context("accept uni")?; + stream + .read_to_end(1 << 16) + .await + .std_context("read to end")?; + info!("accept finished"); + Ok(()) + } + async move { + if let Err(err) = accept(ep_2).await { + error!("{err:#}"); + } + } + .instrument(info_span!("ep2.accept", me = %endpoint_id_2.fmt_short())) + }); + let _accept_task = AbortOnDropHandle::new(accept_task); + + // Add an entry in the RemoteMap of ep_1 with an invalid socket address + let empty_addr_2 = EndpointAddr::from_parts( + endpoint_id_2, + [TransportAddr::Ip( + // Reserved IP range for documentation (unreachable) + SocketAddrV4::new([192, 0, 2, 1].into(), 12345).into(), + )], + ); + let addr_2 = sock_1.resolve_remote(empty_addr_2).await.unwrap().unwrap(); + + // Set a low max_idle_timeout so noq gives up on this quickly and our test does + // not take forever. You need to check the log output to verify this is really + // triggering the correct error. + // In test_try_send_no_send_addr() above you may have noticed we used + // tokio::time::timeout() on the connection attempt instead. Here however we want + // Noq itself to have fully given up on the connection attempt because we will + // later connect to **the same** endpoint. If Noq did not give up on the connection + // we'd close it on drop, and the retransmits of the close packets would interfere + // with the next handshake, closing it during the handshake. This makes the test a + // little slower though. + let mut transport_config = noq::TransportConfig::default(); + transport_config.server_handshake_migration(true); + transport_config.max_idle_timeout(Some(Duration::from_millis(200).try_into().unwrap())); + let res = socket_connect_with_transport_config( + sock_1.noq_endpoint().clone(), + secret_key_1.clone(), + addr_2, + endpoint_id_2, + Arc::new(transport_config), + ) + .await; + assert!(res.is_err(), "expected timeout"); + info!("first connect timed out as expected"); + + // Provide correct addressing information + let correct_addr_2 = EndpointAddr::from_parts( + endpoint_id_2, + sock_2 + .ip_addrs() + .get() + .into_iter() + .map(|x| TransportAddr::Ip(x.addr)), + ); + let addr_2a = sock_1 + .resolve_remote(correct_addr_2) + .await + .unwrap() + .unwrap(); + assert_eq!(addr_2, addr_2a); + + // We can now connect + tokio::time::timeout(Duration::from_secs(10), async move { + info!("establishing new connection"); + let conn = socket_connect( + sock_1.noq_endpoint().clone(), + secret_key_1.clone(), + addr_2, + endpoint_id_2, + ) + .await + .unwrap(); + info!("have connection"); + let mut stream = conn.open_uni().await.unwrap(); + stream.write_all(b"hello").await.unwrap(); + stream.finish().unwrap(); + stream.stopped().await.unwrap(); + info!("finished stream"); + }) + .await + .expect("connection timed out"); + + // TODO: could remove the addresses again, send, add it back and see it recover. + // But we don't have that much private access to the RemoteMap. This will do for now. + } +} diff --git a/vendor/iroh/src/socket/biased_rtt_path_selector.rs b/vendor/iroh/src/socket/biased_rtt_path_selector.rs new file mode 100644 index 0000000..702a209 --- /dev/null +++ b/vendor/iroh/src/socket/biased_rtt_path_selector.rs @@ -0,0 +1,323 @@ +//! Default [`PathSelector`] implementation. +//! +//! [`BiasedRttPathSelector`] preserves iroh's historical "lowest biased RTT wins, with +//! stickiness against flapping" behaviour and is what's installed when no custom +//! selector is provided. + +use std::{sync::Arc, time::Duration}; + +use rustc_hash::FxHashMap; +use tracing::trace; + +use super::{ + remote_map::{PathSelection, PathSelectionContext, PathSelectionData, PathSelector}, + transports::AddrKind, +}; +use crate::socket::transports::FourTuple; + +/// How much do we prefer IPv6 over IPv4 by default. +const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3); + +/// Stickiness threshold for biased RTT comparisons. Switching to a same-tier path only +/// happens when its biased RTT is at least this much better than the current path's. +const RTT_SWITCHING_MIN: Duration = Duration::from_millis(5); + +/// Whether a transport is a primary path or a backup. +/// +/// Primary paths are used preferentially. Backup paths are only used when no primary +/// path is available. This is independent of the QUIC `PathStatus`; today the only +/// transport classified as backup is the relay transport. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum TransportType { + /// A primary path: used whenever available. + Primary, + /// A backup path: only used when no primary path is available. + Backup, +} + +/// Bias configuration for a single transport kind. +/// +/// Used by [`BiasedRttPathSelector`] to bias path selection per address kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct TransportBias { + transport_type: TransportType, + /// RTT bias in nanoseconds. Negative values make this transport more preferred. + rtt_bias: i128, +} + +impl TransportBias { + /// Creates a primary transport bias with no RTT advantage. + pub(crate) fn primary() -> Self { + Self { + transport_type: TransportType::Primary, + rtt_bias: 0, + } + } + + /// Creates a backup transport bias with no RTT advantage. + fn backup() -> Self { + Self { + transport_type: TransportType::Backup, + rtt_bias: 0, + } + } + + /// Adds an RTT advantage to this transport, making it more preferred. + pub(crate) fn with_rtt_advantage(mut self, advantage: Duration) -> Self { + self.rtt_bias -= advantage.as_nanos() as i128; + self + } + + /// Adds an RTT disadvantage to this transport, making it less preferred. + #[cfg(all(test, feature = "unstable-custom-transports"))] + pub(crate) fn with_rtt_disadvantage(mut self, disadvantage: Duration) -> Self { + self.rtt_bias += disadvantage.as_nanos() as i128; + self + } +} + +/// The default [`PathSelector`] used by iroh. +/// +/// Sorts paths by `(transport_type, biased_rtt)` (primary tier wins, then lowest biased +/// RTT). Within the same tier, switching only happens once a candidate's biased RTT is +/// at least 5ms better than the currently-selected path — this avoids flapping under +/// jitter. Across tiers, switching is immediate. +/// +/// The biases are configured per [`AddrKind`]. Defaults: IPv4 and IPv6 are primary +/// (IPv6 has a 3ms RTT advantage), Relay is backup, custom transports are primary with +/// no advantage. +#[derive(Debug, Clone)] +pub(crate) struct BiasedRttPathSelector { + biases: Arc>, +} + +impl Default for BiasedRttPathSelector { + fn default() -> Self { + let mut map = FxHashMap::default(); + map.insert(AddrKind::IpV4, TransportBias::primary()); + map.insert( + AddrKind::IpV6, + TransportBias::primary().with_rtt_advantage(IPV6_RTT_ADVANTAGE), + ); + map.insert(AddrKind::Relay, TransportBias::backup()); + Self { + biases: Arc::new(map), + } + } +} + +impl BiasedRttPathSelector { + /// Returns a new selector with the given bias added or updated for `kind`. + #[cfg(all(test, feature = "unstable-custom-transports"))] + pub(crate) fn with_bias(self, kind: AddrKind, bias: TransportBias) -> Self { + let mut map = (*self.biases).clone(); + map.insert(kind, bias); + Self { + biases: Arc::new(map), + } + } + + /// Looks up the bias for an address. Defaults to primary with no RTT bias. + fn bias_for(&self, addr: &FourTuple) -> TransportBias { + self.biases + .get(&addr.addr_kind()) + .copied() + .unwrap_or_else(TransportBias::primary) + } + + /// Computes the sort key for a path: lower is better. + fn sort_key(&self, addr: &FourTuple, rtt: Duration) -> (TransportType, i128) { + let bias = self.bias_for(addr); + let biased_rtt = (rtt.as_nanos() as i128).saturating_add(bias.rtt_bias); + (bias.transport_type, biased_rtt) + } +} + +impl PathSelector for BiasedRttPathSelector { + fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection { + // Single pass: track the best candidate by sort key, and the best (lowest) + // sort key seen for the currently-selected address. When the same address + // appears multiple times (one path per connection), `min` over `sort_key` + // naturally picks the lowest-RTT instance — no separate aggregation needed. + let current = ctx.current(); + let mut best: Option<(PathSelectionData<'_>, (TransportType, i128))> = None; + let mut current_key: Option<(TransportType, i128)> = None; + + trace!("dumping path RTTs"); + for psd in ctx.paths() { + let network_path = psd.network_path(); + // Skip paths whose stats can't be read (e.g. closed concurrently with select). + let Some(stats) = psd.stats() else { + continue; + }; + let rtt = stats.rtt; + trace!(%network_path, ?rtt); + let key = self.sort_key(network_path, rtt); + + if Some(network_path) == current && current_key.is_none_or(|c| key < c) { + current_key = Some(key); + } + if best.as_ref().is_none_or(|(_, b)| key < *b) { + best = Some((psd, key)); + } + } + + let mut selection = PathSelection::none(); + let Some((best_psd, (best_tier, best_biased))) = best else { + return selection; + }; + + // If we have no current path or no data for it, switch to the best. + let Some((current_tier, current_biased)) = current_key else { + selection.set(&best_psd); + return selection; + }; + + if current_tier != best_tier { + // Always switch across tiers (e.g. relay -> primary). + selection.set(&best_psd); + } else if best_biased + RTT_SWITCHING_MIN.as_nanos() as i128 <= current_biased { + // For the same tier, only switch when biased RTT is meaningfully better. + selection.set(&best_psd); + } + selection + } +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; + + use iroh_base::{EndpointId, RelayUrl}; + use noq::PathStats; + + use super::*; + use crate::socket::{ + remote_map::{PathSelectionContext, PathSelectionData}, + transports::{self, Addr}, + }; + + fn v4(port: u16) -> transports::FourTuple { + transports::FourTuple::from_remote(Addr::Ip(SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::LOCALHOST, + port, + )))) + } + + fn v6(port: u16) -> transports::FourTuple { + transports::FourTuple::from_remote(Addr::Ip(SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + port, + 0, + 0, + )))) + } + + fn relay(port: u16) -> transports::FourTuple { + let url = format!("https://relay{port}.iroh.computer") + .parse::() + .unwrap(); + transports::FourTuple::from_remote(Addr::Relay( + url, + EndpointId::from_bytes(&[0u8; 32]).unwrap(), + )) + } + + fn psd(addr: &transports::FourTuple, rtt_ms: u64) -> PathSelectionData<'_> { + // PathStats is #[non_exhaustive], so build via Default + field assignment. + let mut stats = PathStats::default(); + stats.rtt = Duration::from_millis(rtt_ms); + PathSelectionData::for_test(addr, Some(stats)) + } + + /// Runs [`BiasedRttPathSelector::default`] against the given paths and current + /// selection, returning the selector's primary pick (cloned, for easier asserts). + fn select_with_default( + current: Option<&transports::FourTuple>, + paths: Vec>, + ) -> Option { + let ctx = PathSelectionContext::for_test(current, paths); + BiasedRttPathSelector::default() + .select(&ctx) + .selected() + .cloned() + } + + #[test] + fn ipv6_wins_over_ipv4_within_bias() { + let v4 = v4(1); + let v6 = v6(1); + + // Equal RTTs: IPv6 wins because of the bias advantage. + let chosen = select_with_default(None, vec![psd(&v4, 10), psd(&v6, 10)]); + assert_eq!(chosen.as_ref(), Some(&v6)); + + // IPv6 still wins when 2ms slower (within the 3ms bias). + let chosen = select_with_default(None, vec![psd(&v4, 10), psd(&v6, 12)]); + assert_eq!(chosen.as_ref(), Some(&v6)); + + // IPv4 wins when IPv6 is 10ms slower (exceeds 3ms bias). + let chosen = select_with_default(None, vec![psd(&v4, 10), psd(&v6, 20)]); + assert_eq!(chosen.as_ref(), Some(&v4)); + } + + #[test] + fn primary_wins_over_backup_regardless_of_rtt() { + let v4 = v4(1); + let relay = relay(1); + + // Primary tier beats backup tier even when the backup has a much lower RTT. + let chosen = select_with_default(None, vec![psd(&v4, 100), psd(&relay, 10)]); + assert!(matches!( + chosen.as_ref().map(|t| t.remote()), + Some(Addr::Ip(_)) + )); + + // Even more extreme: 1000ms primary still wins over 1ms backup. + let chosen = select_with_default(None, vec![psd(&v4, 1000), psd(&relay, 1)]); + assert!(matches!( + chosen.as_ref().map(|t| t.remote()), + Some(Addr::Ip(_)) + )); + } + + #[test] + fn same_tier_only_switches_with_significant_rtt_diff() { + let v4_1 = v4(1); + let v4_2 = v4(2); + + // 2ms diff < 5ms threshold → keep current (no switch, primary() == None). + let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 18)]); + assert_eq!(chosen, None); + + // 4ms diff < 5ms → keep current. + let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 16)]); + assert_eq!(chosen, None); + + // 5ms diff hits the threshold (the condition is `<=`) → switch. + let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 15)]); + assert_eq!(chosen.as_ref(), Some(&v4_2)); + + // 6ms diff > 5ms → switch. + let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 14)]); + assert_eq!(chosen.as_ref(), Some(&v4_2)); + } + + #[test] + fn no_current_path_selects_best() { + let v4_1 = v4(1); + let v4_2 = v4(2); + let chosen = select_with_default(None, vec![psd(&v4_1, 20), psd(&v4_2, 10)]); + assert_eq!(chosen.as_ref(), Some(&v4_2)); + } + + #[test] + fn empty_paths_returns_none() { + // No current, no candidates: nothing to pick. + assert_eq!(select_with_default(None, vec![]), None); + + // Current is set but there are no candidates: keep current (primary() == None). + let v4 = v4(1); + assert_eq!(select_with_default(Some(&v4), vec![]), None); + } +} diff --git a/vendor/iroh/src/socket/concurrent_read_map.rs b/vendor/iroh/src/socket/concurrent_read_map.rs new file mode 100644 index 0000000..e640f7e --- /dev/null +++ b/vendor/iroh/src/socket/concurrent_read_map.rs @@ -0,0 +1,70 @@ +//! This module implements a map that can be modified from only one task but read from many others. +//! +//! We ensure this map avoids race conditions from multiple writers by doing these two things: +//! - We only allow writing from one owner of the `&mut self` [`ConcurrentReadMap`]. +//! It cannot be cloned. +//! - The read-only replicas [`ReadOnlyMap`] can only read, not write, but allow reading from +//! concurrent tasks. + +use std::{hash::Hash, sync::Arc}; + +use rustc_hash::FxBuildHasher; + +/// A map that can be modified from only one task but read from many others. +/// +/// Only the single `&mut self`-based "leader" which owns this map can modify it, but cheaply +/// clonable "followers" can be created with [`ConcurrentReadMap::read_only`]. +/// These clones will update with recent writes from the leader. +#[derive(Debug)] +pub(crate) struct ConcurrentReadMap(Arc>); + +impl Default for ConcurrentReadMap { + fn default() -> Self { + Self(Default::default()) + } +} + +impl ConcurrentReadMap { + pub(crate) fn get_or_insert_with(&mut self, key: K, f: F) -> V + where + F: FnOnce() -> V, + V: Clone, + { + self.0.get_or_insert_with(key, f, &self.0.guard()).clone() + } + + pub(crate) fn insert(&mut self, key: K, value: V) { + self.0.insert(key, value, &self.0.guard()); + } + + pub(crate) fn remove(&mut self, key: &K) { + self.0.remove(key, &self.0.guard()); + } + + pub(crate) fn read_only(&self) -> ReadOnlyMap { + ReadOnlyMap(self.0.clone()) + } +} + +/// A read replica for a [`ConcurrentReadMap`]. +/// +/// These can be cloned cheaply and will read recent writes from the original map. +#[derive(Clone, Debug)] +pub(crate) struct ReadOnlyMap(Arc>); + +impl ReadOnlyMap { + pub(crate) fn get(&self, key: &K) -> Option + where + V: Clone, + { + self.0.get(key, &self.0.guard()).cloned() + } + + pub(crate) fn guard(&self) -> papaya::LocalGuard<'_> { + self.0.guard() + } + + pub(crate) fn values<'g, G: papaya::Guard>(&self, guard: &'g G) -> papaya::Values<'g, K, V, G> { + self.0.values(guard) + } +} diff --git a/vendor/iroh/src/socket/mapped_addrs.rs b/vendor/iroh/src/socket/mapped_addrs.rs new file mode 100644 index 0000000..dc1943f --- /dev/null +++ b/vendor/iroh/src/socket/mapped_addrs.rs @@ -0,0 +1,376 @@ +//! The various mapped addresses we use. + +//! We use non-IP transports to carry datagrams. Yet Noq needs to address those +//! transports using IPv6 addresses. These defines mappings of several IPv6 Unique Local +//! Address ranges we use to keep track of the various "fake" address types we use. + +use std::{ + fmt, + hash::Hash, + net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, + sync::Arc, +}; + +use n0_error::{e, stack_error}; +use rand::Rng; +use rustc_hash::FxHashMap; +use tracing::trace; + +/// The Prefix/L of all Unique Local Addresses. +const ADDR_PREFIXL: u8 = 0xfd; + +/// The Global ID used in n0's Unique Local Addresses. +const ADDR_GLOBAL_ID: [u8; 5] = [0x15, 0x07, 0x0a, 0x51, 0x0b]; + +/// The Subnet ID for [`RelayMappedAddr]: fd15:70a:510b:1::/64. +const RELAY_MAPPED_SUBNET: [u8; 2] = [0x00, 0x01]; + +/// The Subnet ID for [`CustomMappedAddr`]: fd15:70a:510b:3::/64. +const CUSTOM_MAPPED_SUBNET: [u8; 2] = [0x00, 0x03]; + +/// The Subnet ID for [`EndpointIdMappedAddr`]: fd15:70a:510b::/64. +const ENDPOINT_ID_SUBNET: [u8; 2] = [0x00, 0x00]; + +/// A default fake addr, using the maximum addr that the internal fake addrs could be using. +pub(crate) const DEFAULT_FAKE_ADDR: SocketAddrV6 = SocketAddrV6::new( + Ipv6Addr::new( + u16::from_be_bytes([ADDR_PREFIXL, 21]), + u16::from_be_bytes([7, 10]), + u16::from_be_bytes([81, 11]), + u16::from_be_bytes([0, 0]), + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + ), + MAPPED_PORT, + 0, + 0, +); + +/// The dummy port used for all mapped addresses. +/// +/// We map each entity, usually an [`crate::EndpointId`], to an IPv6 address. But socket addresses +/// involve ports, so we use a dummy fixed port when creating socket addresses. +const MAPPED_PORT: u16 = 12345; + +/// Generic mapped address. +/// +/// Allows implementing [`AddrMap`]. +pub(crate) trait MappedAddr { + /// Generates a new mapped address in the IPv6 Unique Local Address space. + /// + /// The host bits are random so a remote peer cannot guess one of our live mapped + /// addresses (uniqueness is ensured by [`AddrMap::get`]). + fn generate() -> Self; + + /// Returns a consistent [`SocketAddr`] for the mapped addr. + /// + /// This socket address does not have a routable IP address. It uses a fake but + /// consistent port number, since the port does not play a role in the addressing. This + /// socket address is only to be used to pass into Noq. + fn private_socket_addr(&self) -> SocketAddr; +} + +/// An enum encompassing all the mapped and unmapped addresses. +/// +/// This is essentially a slightly-stronger typed version of the IPv6 mapped addresses that +/// we use on the Noq side. It categorises the addressed in what kind of mapped or +/// unmapped addresses they are. +/// +/// It does not guarantee that a mapped address exists in the mapping. Or that a particular +/// address is even supported on this platform. Hence no wasm exceptions here. +#[derive(Clone, Debug)] +pub(crate) enum MultipathMappedAddr { + /// An address for a [`crate::EndpointId`], via one or more paths. + Mixed(EndpointIdMappedAddr), + /// An address for a particular [`crate::EndpointId`] via a particular relay. + Relay(RelayMappedAddr), + /// An IP based transport address. + Ip(SocketAddr), + /// Custom transport address. + Custom(CustomMappedAddr), +} + +impl From for MultipathMappedAddr { + fn from(value: SocketAddr) -> Self { + match value.ip() { + IpAddr::V4(_) => Self::Ip(value), + IpAddr::V6(addr) => { + if let Ok(addr) = EndpointIdMappedAddr::try_from(addr) { + return Self::Mixed(addr); + } + if let Ok(addr) = RelayMappedAddr::try_from(addr) { + return Self::Relay(addr); + } + if let Ok(addr) = CustomMappedAddr::try_from(addr) { + return Self::Custom(addr); + } + Self::Ip(value) + } + } + } +} + +/// An address used to address a endpoint on any or all paths. +/// +/// This is only used for initially connecting to a remote endpoint. We instruct Noq to +/// send to this address, and duplicate all packets for this address to send on all paths we +/// might want to send the initial on: +/// +/// - If this the first connection to the remote endpoint we don't know which path will work +/// and send to all of them. +/// +/// - If there already is an active connection to this endpoint we now which path to use. +/// +/// It is but a newtype around an IPv6 Unique Local Addr. And in our QUIC-facing socket +/// APIs like [`noq::AsyncUdpSocket`] it comes in as the inner [`Ipv6Addr`], in those +/// interfaces we have to be careful to do the conversion to this type. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct EndpointIdMappedAddr(Ipv6Addr); + +impl MappedAddr for EndpointIdMappedAddr { + /// Generates a globally unique fake UDP address. + /// + /// This generates and IPv6 Unique Local Address according to RFC 4193. + fn generate() -> Self { + let mut addr = [0u8; 16]; + addr[0] = ADDR_PREFIXL; + addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); + addr[6..8].copy_from_slice(&ENDPOINT_ID_SUBNET); + rand::rng().fill_bytes(&mut addr[8..16]); + + Self(Ipv6Addr::from(addr)) + } + + /// Returns a consistent [`SocketAddr`] for the [`EndpointIdMappedAddr`]. + /// + /// This socket address does not have a routable IP address and port. + /// + /// This uses a made-up port number, since the port does not play a role in the + /// addressing. This socket address is only to be used to pass into Noq. + fn private_socket_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) + } +} + +impl std::fmt::Display for EndpointIdMappedAddr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "EndpointIdMappedAddr({})", self.0) + } +} + +impl TryFrom for EndpointIdMappedAddr { + type Error = EndpointIdMappedAddrError; + + fn try_from(value: Ipv6Addr) -> Result { + let octets = value.octets(); + if octets[0] == ADDR_PREFIXL + && octets[1..6] == ADDR_GLOBAL_ID + && octets[6..8] == ENDPOINT_ID_SUBNET + { + return Ok(Self(value)); + } + Err(e!(EndpointIdMappedAddrError)) + } +} + +/// Can occur when converting a [`SocketAddr`] to an [`EndpointIdMappedAddr`] +#[stack_error(derive, add_meta)] +#[error("Failed to convert")] +pub(crate) struct EndpointIdMappedAddrError; + +/// An Ipv6 ULA address, identifying a relay path for a [`crate::EndpointId`]. +/// +/// Since iroh endpoint are reachable via a relay server we have a network path indicated by +/// the `(EndpointId, RelayUrl)`. However Noq can only handle socket addresses, so we use +/// IPv6 addresses in a private IPv6 Unique Local Address range, which map to a unique +/// `(EndointId, RelayUrl)` pair. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub(crate) struct RelayMappedAddr(Ipv6Addr); + +impl MappedAddr for RelayMappedAddr { + /// Generates a globally unique fake UDP address. + /// + /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) + /// which is recognised by iroh as an IP mapped address. + fn generate() -> Self { + let mut addr = [0u8; 16]; + addr[0] = ADDR_PREFIXL; + addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); + addr[6..8].copy_from_slice(&RELAY_MAPPED_SUBNET); + rand::rng().fill_bytes(&mut addr[8..16]); + + Self(Ipv6Addr::from(addr)) + } + + /// Returns a consistent [`SocketAddr`] for the [`RelayMappedAddr`]. + /// + /// This socket address does not have a routable IP address and port. + /// + /// This uses a made-up port number, since the port does not play a role in the + /// addressing. This socket address is only to be used to pass into Noq. + fn private_socket_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) + } +} + +impl TryFrom for RelayMappedAddr { + type Error = RelayMappedAddrError; + + fn try_from(value: Ipv6Addr) -> std::result::Result { + let octets = value.octets(); + if octets[0] == ADDR_PREFIXL + && octets[1..6] == ADDR_GLOBAL_ID + && octets[6..8] == RELAY_MAPPED_SUBNET + { + return Ok(Self(value)); + } + Err(e!(RelayMappedAddrError)) + } +} + +/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] +#[stack_error(derive, add_meta)] +#[error("Failed to convert")] +pub(crate) struct RelayMappedAddrError; + +impl std::fmt::Display for RelayMappedAddr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "RelayMappedAddr({})", self.0) + } +} + +/// An Ipv6 ULA address, identifying a custom transport path. +/// +/// Custom transports allow user-defined transport mechanisms. However Noq can only handle +/// socket addresses, so we use IPv6 addresses in a private IPv6 Unique Local Address range, +/// which map to a unique [`iroh_base::CustomAddr`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub(crate) struct CustomMappedAddr(Ipv6Addr); + +impl MappedAddr for CustomMappedAddr { + /// Generates a globally unique fake UDP address. + /// + /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) + /// which is recognised by iroh as an IP mapped address. + fn generate() -> Self { + let mut addr = [0u8; 16]; + addr[0] = ADDR_PREFIXL; + addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); + addr[6..8].copy_from_slice(&CUSTOM_MAPPED_SUBNET); + rand::rng().fill_bytes(&mut addr[8..16]); + + Self(Ipv6Addr::from(addr)) + } + + /// Returns a consistent [`SocketAddr`] for the [`CustomMappedAddr`]. + /// + /// This socket address does not have a routable IP address and port. + /// + /// This uses a made-up port number, since the port does not play a role in the + /// addressing. This socket address is only to be used to pass into Noq. + fn private_socket_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) + } +} + +impl TryFrom for CustomMappedAddr { + type Error = CustomMappedAddrError; + + fn try_from(value: IpAddr) -> std::result::Result { + match value { + IpAddr::V4(_) => Err(e!(CustomMappedAddrError)), + IpAddr::V6(addr) => addr.try_into(), + } + } +} + +impl TryFrom for CustomMappedAddr { + type Error = CustomMappedAddrError; + + fn try_from(value: Ipv6Addr) -> std::result::Result { + let octets = value.octets(); + if octets[0] == ADDR_PREFIXL + && octets[1..6] == ADDR_GLOBAL_ID + && octets[6..8] == CUSTOM_MAPPED_SUBNET + { + return Ok(Self(value)); + } + Err(e!(CustomMappedAddrError)) + } +} + +/// Can occur when converting a [`SocketAddr`] to a [`CustomMappedAddr`] +#[stack_error(derive, add_meta)] +#[error("Failed to convert")] +pub(crate) struct CustomMappedAddrError; + +impl std::fmt::Display for CustomMappedAddr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "CustomMappedAddr({})", self.0) + } +} + +/// A bi-directional map between a key and a [`MappedAddr`]. +#[derive(Debug, Clone)] +pub(super) struct AddrMap { + inner: Arc>>, +} + +// Manual impl because derive ends up requiring T: Default. +impl Default for AddrMap { + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + +impl AddrMap +where + K: Eq + Hash + Clone + fmt::Debug, + V: MappedAddr + Eq + Hash + Copy + fmt::Debug, +{ + /// Returns the [`MappedAddr`], generating one if needed. + pub(super) fn get(&self, key: &K) -> V { + let mut inner = self.inner.lock().expect("poisoned"); + match inner.addrs.get(key) { + Some(addr) => *addr, + None => { + let addr = loop { + let candidate = V::generate(); + if !inner.lookup.contains_key(&candidate) { + break candidate; + } + }; + inner.addrs.insert(key.clone(), addr); + inner.lookup.insert(addr, key.clone()); + trace!(?addr, ?key, "generated new addr"); + addr + } + } + } + + /// Performs the reverse lookup. + pub(super) fn lookup(&self, addr: &V) -> Option { + let inner = self.inner.lock().expect("poisoned"); + inner.lookup.get(addr).cloned() + } +} + +#[derive(Debug)] +struct AddrMapInner { + addrs: FxHashMap, + lookup: FxHashMap, +} + +// Manual impl because derive ends up requiring T: Default. +impl Default for AddrMapInner { + fn default() -> Self { + Self { + addrs: Default::default(), + lookup: Default::default(), + } + } +} diff --git a/vendor/iroh/src/socket/metrics.rs b/vendor/iroh/src/socket/metrics.rs new file mode 100644 index 0000000..245c267 --- /dev/null +++ b/vendor/iroh/src/socket/metrics.rs @@ -0,0 +1,126 @@ +use iroh_metrics::{Counter, MetricsGroup}; +use serde::{Deserialize, Serialize}; + +/// Metrics collected by the iroh socket. +#[derive(Debug, Serialize, Deserialize, MetricsGroup)] +#[non_exhaustive] +#[metrics(name = "socket", default)] +pub struct Metrics { + /// Intended to count updates to the local direct address set, but currently unused + /// (never incremented). + pub update_direct_addrs: Counter, + + /// Number of bytes sent over IPv4. + pub send_ipv4: Counter, + /// Number of bytes sent over IPv6. + pub send_ipv6: Counter, + /// Number of bytes sent over the relay transport. + pub send_relay: Counter, + + /// Number of data bytes received over the relay transport. + pub recv_data_relay: Counter, + /// Number of data bytes received over any custom transport. + pub recv_data_custom: Counter, + /// Number of data bytes received over IPv4. + pub recv_data_ipv4: Counter, + /// Number of data bytes received over IPv6. + pub recv_data_ipv6: Counter, + /// Number of QUIC datagrams received. + pub recv_datagrams: Counter, + /// Number of receive events that used GRO (coalesced datagram batches). + /// + /// This counts batches, not the individual datagrams within them. See + /// [`Self::recv_datagrams`] for the datagram count. + pub recv_gro_datagrams: Counter, + + /// Number of times the home relay changed to a different relay. + /// + /// This includes the initial assignment from no home relay to a home relay. + pub relay_home_change: Counter, + + /// Number of connections to a relay server that were established. + /// + /// Failed dial attempts are not counted. A relay that is reconnected to is counted + /// once per connection, so this grows on every reconnect. + pub relay_conns_success: Counter, + /// Number of failed attempts to connect to a relay server. + /// + /// Each attempt is counted, so an unreachable relay increments this on every retry. + pub relay_conns_failed: Counter, + /// Number of connections to a relay server that ended. + /// + /// Paired with [`Self::relay_conns_success`], so the number of current connections is + /// `relay_conns_success` - `relay_conns_closed`. + pub relay_conns_closed: Counter, + /// Number of relay connections on which the relay reported rate limiting. + /// + /// This is incremented at most once per connection, so it counts the affected + /// connections and not how often the relay reported the problem. Relate it to + /// [`Self::relay_conns_success`] to see how many connections were affected. + pub relay_conns_ratelimited: Counter, + + /* + * Holepunching metrics + */ + /// The number of times holepunching is initiated on a connection. + /// + /// This can be incremented multiple times for a single connection. Note that only the + /// client-side of a connection will increment this counter. + pub holepunch_attempts: Counter, + /// The number of network paths to peers that are direct. + /// + /// This can be incremented multiple times for a single connection. + pub paths_direct: Counter, + /// The number of network paths to peers that are relayed. + /// + /// This would typically only be incremented once for a single connection. + pub paths_relay: Counter, + /// The number of network paths to peers that are user defined. + /// + /// This would typically only be incremented once for a single connection. + pub paths_custom: Counter, + /// The number of connections that have been direct connections. + /// + /// This is only incremented once for each opened connection. See `num_conns_opened` for + /// the number of opened connections. + pub num_conns_direct: Counter, + + /* + * Connection Metrics + * + * These all only count connections that completed the TLS handshake successfully. This means + * that short lived 0RTT connections are potentially not included in these counts. + */ + /// Number of connections opened (only handshaked connections are counted). + pub num_conns_opened: Counter, + /// Number of connections closed (only handshaked connections are counted). + pub num_conns_closed: Counter, + + /// Number of IP transport paths opened. + pub transport_ip_paths_added: Counter, + /// Number of IP transport paths closed. + pub transport_ip_paths_removed: Counter, + /// Number of relay transport paths opened. + pub transport_relay_paths_added: Counter, + /// Number of relay transport paths closed. + pub transport_relay_paths_removed: Counter, + /// Number of custom transport paths opened. + pub transport_custom_paths_added: Counter, + /// Number of custom transport paths closed. + pub transport_custom_paths_removed: Counter, + + /// Number of iterations of the main socket actor loop. + pub actor_tick_main: Counter, + /// Number of actor messages processed by the socket actor loop. + pub actor_tick_msg: Counter, + /// Number of periodic re-STUN timer ticks handled by the socket actor loop. + pub actor_tick_re_stun: Counter, + /// Number of port-mapping change events handled by the socket actor loop. + pub actor_tick_portmap_changed: Counter, + /// Intended to count direct address heartbeat ticks, but currently unused (never incremented). + pub actor_tick_direct_addr_heartbeat: Counter, + /// Number of local network interface (link) change events handled by the socket actor loop. + pub actor_link_change: Counter, + /// Number of times an input watcher or receiver closed in the socket actor loop. + pub actor_tick_other: Counter, +} diff --git a/vendor/iroh/src/socket/remote_map.rs b/vendor/iroh/src/socket/remote_map.rs new file mode 100644 index 0000000..24618e6 --- /dev/null +++ b/vendor/iroh/src/socket/remote_map.rs @@ -0,0 +1,661 @@ +use std::{ + collections::BTreeSet, + future::poll_fn, + hash::Hash, + net::SocketAddr, + sync::Arc, + task::{Context, Poll, Waker, ready}, +}; + +use iroh_base::{CustomAddr, EndpointAddr, EndpointId, RelayUrl}; +use n0_future::task::JoinSet; +use serde::{Deserialize, Serialize}; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; +use tracing::{Span, error, trace}; + +pub(crate) use self::remote_state::PathStateReceiver; +use self::remote_state::RemoteStateActor; +pub(super) use self::remote_state::RemoteStateMessage; +pub use self::remote_state::{ + Path, PathEvent, PathEventStream, PathList, PathListIter, PathListStream, RemoteInfo, + TransportAddrInfo, TransportAddrUsage, +}; +#[cfg(feature = "unstable-custom-transports")] +pub use self::remote_state::{ + PathSelection, PathSelectionContext, PathSelectionData, PathSelector, +}; +#[cfg(not(feature = "unstable-custom-transports"))] +pub(crate) use self::remote_state::{ + PathSelection, PathSelectionContext, PathSelectionData, PathSelector, +}; +use super::{ + DirectAddr, Metrics as SocketMetrics, + mapped_addrs::{ + AddrMap, CustomMappedAddr, EndpointIdMappedAddr, MappedAddr, MultipathMappedAddr, + RelayMappedAddr, + }, + transports, +}; +use crate::{ + address_lookup::{self, AddressLookupFailed}, + endpoint::LocalTransportAddr, + socket::concurrent_read_map::{ConcurrentReadMap, ReadOnlyMap}, +}; + +mod remote_state; + +// TODO: use this +// /// Number of endpoints that are inactive for which we keep info about. This limit is enforced +// /// periodically via [`NodeMap::prune_inactive`]. +// const MAX_INACTIVE_NODES: usize = 30; + +/// Map containing all the state for endpoints. +/// +/// - Has actors which each manage all the connection state for a remote endpoint. +/// +/// - Has the mapped addresses we use to refer to non-IP transports destinations into IPv6 +/// addressing space that is used by Noq. +#[derive(Debug)] +pub(crate) struct RemoteMap { + /// Maps for converting between mapped and IP/relay addrs. + pub(super) mapped_addrs: MappedAddrs, + + /// The senders for the inbox of each `RemoteStateActor` that runs. + /// + /// This is separated out of `Tasks` to make keeping a mutable borrow of the senders possible + /// while we're spawning a task using another mutable borrow of `Tasks`. + senders: ConcurrentReadMap>, + + /// The state kept for spawning new actors and cleaning them up. + tasks: Tasks, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct MappedAddrs { + /// The mapping between [`EndpointId`]s and [`EndpointIdMappedAddr`]s. + pub(super) endpoint_addrs: AddrMap, + /// The mapping between endpoints via a relay and their [`RelayMappedAddr`]s. + pub(super) relay_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, + /// The mapping between custom transport addresses and their [`CustomMappedAddr`]s. + pub(super) custom_addrs: AddrMap, +} + +impl MappedAddrs { + /// Converts a possibly mapped-IP address into a [`transports::Addr`]. + pub(crate) fn to_transport_addr(&self, addr: SocketAddr) -> Option { + to_transport_addr(addr, &self.relay_addrs, &self.custom_addrs) + } + + /// Converts a possibly mapped-IP 4-tuple into a [`transports::FourTuple`]. + pub(crate) fn to_transport_tuple( + &self, + four_tuple: &noq::FourTuple, + ) -> Option { + let remote = to_transport_addr(four_tuple.remote(), &self.relay_addrs, &self.custom_addrs)?; + let local = LocalTransportAddr::from_noq_local_ip( + four_tuple.local_ip(), + &remote, + &self.custom_addrs, + ); + Some(transports::FourTuple::new(remote, local)) + } + + /// Converts a [`transports::FourTuple`] to a mapped-IP 4-tuple. + pub(crate) fn to_mapped_tuple(&self, four_tuple: &transports::FourTuple) -> noq::FourTuple { + let (remote, local) = match four_tuple { + transports::FourTuple::Ip { remote, local } => (*remote, *local), + transports::FourTuple::Relay { url, endpoint_id } => ( + self.relay_addrs + .get(&(url.clone(), *endpoint_id)) + .private_socket_addr(), + None, + ), + transports::FourTuple::Custom { remote, local } => { + let remote = self.custom_addrs.get(remote).private_socket_addr(); + let local = local.as_ref().map(|custom_addr| { + self.custom_addrs + .get(custom_addr) + .private_socket_addr() + .ip() + }); + (remote, local) + } + }; + noq::FourTuple::new(remote, local) + } +} + +/// Converts a mapped socket address to a transport address. +/// +/// This takes a socket address, converts it into a [`MultipathMappedAddr`] and then tries +/// to convert the mapped address into a [`transports::Addr`]. +/// +/// Returns `Some` with the transport address for IP, relay, or custom mapped addresses +/// if an entry exists in the corresponding map. +/// +/// Returns `None` for [`MultipathMappedAddr::Mixed`] addresses or unknown mapped addresses. +fn to_transport_addr( + addr: impl Into, + relay_addrs: &AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, + custom_addrs: &AddrMap, +) -> Option { + match addr.into() { + MultipathMappedAddr::Mixed(_) => { + error!( + "Failed to convert addr to transport addr: Mixed mapped addr has no transport address" + ); + None + } + MultipathMappedAddr::Relay(relay_mapped_addr) => { + match relay_addrs.lookup(&relay_mapped_addr) { + Some(parts) => Some(transports::Addr::from(parts)), + None => { + error!("Failed to convert addr to transport addr: Unknown relay mapped addr"); + None + } + } + } + MultipathMappedAddr::Custom(custom_mapped_addr) => { + match custom_addrs.lookup(&custom_mapped_addr) { + Some(custom_addr) => Some(transports::Addr::Custom(custom_addr)), + None => { + error!("Failed to convert addr to transport addr: Unknown custom mapped addr"); + None + } + } + } + MultipathMappedAddr::Ip(addr) => Some(transports::Addr::from(addr)), + } +} + +/// Stores the state required for starting and cleaning up the `RemoteStateActor`s. +/// +/// When this is dropped, this will abort all tasks. +#[derive(Debug)] +struct Tasks { + // + // State required for spawning new actors. + // + metrics: Arc, + /// The "direct" addresses known for our local endpoint + local_direct_addrs: n0_watcher::Direct>, + address_lookup: address_lookup::AddressLookupServices, + shutdown_token: CancellationToken, + + // + // State for task-tracking spawned actors. + // + /// All the `RemoteStateActor` tasks, stored inside a `JoinSet`. + /// + /// These tasks return their endpoint ID and the list of messages they didn't get to handle + /// when they shut down. + tasks: JoinSet<(EndpointId, Vec)>, + /// The waker that notifies `poll_cleanup` when the join set is populated with another task. + poll_cleanup_waker: Option, + /// The path selector used by all [`RemoteStateActor`]s spawned by this map. + path_selector: Arc, + /// The tracing span for this endpoint, to be used as parent span for `RemoteStateActor` tasks. + span: Span, +} + +impl RemoteMap { + /// Creates a new [`RemoteMap`]. + pub(super) fn new( + metrics: Arc, + local_direct_addrs: n0_watcher::Direct>, + address_lookup: address_lookup::AddressLookupServices, + shutdown_token: CancellationToken, + path_selector: Arc, + span: Span, + ) -> Self { + Self { + mapped_addrs: Default::default(), + senders: Default::default(), + tasks: Tasks { + metrics, + local_direct_addrs, + address_lookup, + shutdown_token, + tasks: Default::default(), + poll_cleanup_waker: None, + path_selector, + span, + }, + } + } + + /// Cleans up terminated `RemoteStateActor` tasks. + /// + /// This polls for terminated actor tasks, and removes the corresponding actor sender + /// from our sender map, or restarts the actor if it has pending messages. + /// + /// Resolves to the actor's remote endpoint ID whenever a `RemoteStateActor` task joined, + /// independent of whether the task was restarted or not. + /// + /// Returns pending if there was no actor to be cleaned up right now, and registers + /// for moments where this could become the case. + /// + /// This function should be called in a loop to clean up expired tasks. + /// Only one task is allowed to poll this function concurrently. + pub(super) async fn cleanup(&mut self) -> EndpointId { + loop { + let (remote_id, leftover_messages) = poll_fn(|cx| self.poll_join_next(cx)).await; + if self.remove_or_restart_actor(remote_id, leftover_messages) { + return remote_id; + } + } + } + + /// Polls for the next joined actor task. + /// + /// Returns [`Poll::Pending`] if no tasks are running, and is woken when the task set changes. + fn poll_join_next( + &mut self, + cx: &mut Context<'_>, + ) -> Poll<(EndpointId, Vec)> { + while let Some(result) = ready!(self.tasks.tasks.poll_join_next(cx)) { + match result { + Ok((remote_id, leftover_msgs)) => { + return Poll::Ready((remote_id, leftover_msgs)); + } + Err(err) => { + if let Ok(panic) = err.try_into_panic() { + error!("RemoteStateActor panicked."); + std::panic::resume_unwind(panic); + } + } + } + } + // There's nothing to clean up. + // Let's get woken when there's another task. + // If we're called after that, then we'll fall into `poll_join_next` and + // properly wait for a task to finish. + self.tasks.poll_cleanup_waker.replace(cx.waker().clone()); + Poll::Pending + } + + /// Removes an actor sender if `leftover_msgs` is empty, or restarts the actor otherwise. + fn remove_or_restart_actor( + &mut self, + remote_id: iroh_base::PublicKey, + leftover_msgs: Vec, + ) -> bool { + if leftover_msgs.is_empty() { + // the actor shut down cleanly + self.senders.remove(&remote_id); + trace!(%remote_id, "cleaned up RemoteStateActor"); + true + } else { + // The remote actor got messages while it was closing, so we're restarting + trace!(%remote_id, "restarting terminated RemoteStateActor: messages received during shutdown"); + let sender = + self.tasks + .start_remote_state_actor(remote_id, leftover_msgs, &self.mapped_addrs); + // We don't have to be careful about guards - only one thread is modifying this hashmap at a time. + self.senders.insert(remote_id, sender); + false + } + } + + pub(super) fn on_network_change(&mut self, is_major: bool) { + let read = self.senders.read_only(); + let guard = read.guard(); + for sender in read.values(&guard) { + sender + .try_send(RemoteStateMessage::NetworkChange { is_major }) + .ok(); + } + } + + pub(super) async fn resolve_remote( + &mut self, + addr: EndpointAddr, + tx: oneshot::Sender>, + ) { + let EndpointAddr { id, addrs } = addr; + self.send_to_actor(id, RemoteStateMessage::ResolveRemote(addrs, tx)) + .await + } + + pub(super) async fn add_connection( + &mut self, + remote: EndpointId, + conn: noq::Connection, + tx: oneshot::Sender, + ) { + self.send_to_actor(remote, RemoteStateMessage::AddConnection(conn, tx)) + .await + } + + /// Sends a message to a `RemoteStateActor`, starting it if not running already. + /// + /// When sending fails, the actor must be terminating, in which case we wait for its task to + /// join and then restart the sender. + async fn send_to_actor(&mut self, remote_id: EndpointId, message: RemoteStateMessage) { + let sender = self.senders.get_or_insert_with(remote_id, || { + self.tasks + .start_remote_state_actor(remote_id, vec![], &self.mapped_addrs) + }); + + if let Err(mpsc::error::SendError(message)) = sender.send(message).await { + // The send failed, which means the RemoteStateActor is terminating. We call the cleanup + // function so that its task is processed. This ensures that the leftover messages are + // properly enqueued into a new actor, and that a later cleanup does not reap a newly + // created sender again. We can be sure that the task has not been cleaned up yet + // because we take a `&mut self` reference. + loop { + let (id, leftover_messages) = poll_fn(|cx| self.poll_join_next(cx)).await; + if id != remote_id { + self.remove_or_restart_actor(id, leftover_messages); + } else { + let mut messages = leftover_messages; + messages.push(message); + self.remove_or_restart_actor(id, messages); + break; + } + } + } + } + + pub(super) fn senders(&self) -> ReadOnlyMap> { + self.senders.read_only() + } +} + +impl Tasks { + /// Starts a new remote state actor and returns a handle and a sender. + /// + /// The handle is not inserted into the endpoint map, this must be done by the caller of this function. + fn start_remote_state_actor( + &mut self, + eid: EndpointId, + initial_msgs: Vec, + mapped_addrs: &MappedAddrs, + ) -> mpsc::Sender { + // Ensure there is a RemoteMappedAddr for this EndpointId. + mapped_addrs.endpoint_addrs.get(&eid); + let sender = RemoteStateActor::new( + eid, + self.local_direct_addrs.clone(), + mapped_addrs.clone(), + self.metrics.clone(), + self.address_lookup.clone(), + self.path_selector.clone(), + ) + .start( + initial_msgs, + &mut self.tasks, + self.shutdown_token.clone(), + self.span.clone(), + ); + if let Some(waker) = self.poll_cleanup_waker.take() { + // Notify something waiting for changes to tasks when there's a new task. + waker.wake(); + } + sender + } +} + +/// The origin or *source* through which an address associated with a remote endpoint +/// was discovered. +/// +/// An aggregate of the [`Source`]s of all the addresses of an endpoint describe the +/// [`Source`]s of the endpoint itself. +/// +/// A [`Source`] helps track how and where an address was learned. Multiple +/// sources can be associated with a single address, if we have discovered this +/// address through multiple means. +#[derive(Serialize, Deserialize, strum::Display, Debug, Clone, Eq, PartialEq, Hash)] +#[strum(serialize_all = "kebab-case")] +#[allow(private_interfaces)] +#[non_exhaustive] +pub(crate) enum Source { + /// Application layer added the address directly. + App, + /// The address was discovered by an Address Lookup system + #[strum(serialize = "{name}")] + AddressLookup { + /// The name of the Address Lookup that discovered the address. + name: String, + }, + /// The address was added as a path within a connection. + Connection, +} + +#[cfg(test)] +mod tests { + use std::{net::SocketAddr, pin::Pin, time::Duration}; + + use bytes::Bytes; + use futures_util::StreamExt; + use iroh_base::{SecretKey, TransportAddr}; + use n0_future::future::now_or_never; + use n0_tracing_test::traced_test; + use n0_watcher::Watchable; + use tokio::sync::oneshot; + use tracing::Span; + + use super::*; + use crate::socket::{ + biased_rtt_path_selector::BiasedRttPathSelector, + transports::{OwnedTransmit, Transmit}, + }; + + #[tokio::test(start_paused = true)] + async fn pending_initial_send_does_not_block_remote_actor_inbox() { + check_initial_send_inbox(256).await; + } + + #[tokio::test(start_paused = true)] + async fn uncongested_initial_send_keeps_remote_actor_responsive() { + check_initial_send_inbox(0).await; + } + + async fn check_initial_send_inbox(queued: usize) { + let (mut remote_map, _shutdown_token, _guards) = make_remote_map(); + let (endpoint_id, mut receiver) = enqueue_initials(&mut remote_map, queued, 1).await; + let (info_tx, info_rx) = oneshot::channel(); + remote_map + .send_to_actor(endpoint_id, RemoteStateMessage::RemoteInfo(info_tx)) + .await; + // The inbox must respond before the blocked send's three-second deadline. + tokio::time::timeout(Duration::from_secs(1), info_rx) + .await + .expect("pending Initial send blocked the RemoteStateActor inbox") + .expect("remote actor dropped the response"); + assert!( + tokio::time::timeout(Duration::from_secs(1), receiver.next()) + .await + .expect("Initial was not sent on an uncongested queue") + .is_some() + ); + } + + #[tokio::test] + async fn pending_initial_sends_are_cancelled_on_shutdown() { + let (mut remote_map, shutdown_token, _guards) = make_remote_map(); + let (endpoint_id, receiver) = enqueue_initials(&mut remote_map, 256, 1).await; + wait_for_inbox(&mut remote_map, endpoint_id).await; + shutdown_token.cancel(); + tokio::time::timeout(Duration::from_secs(1), remote_map.cleanup()) + .await + .expect("actor shutdown blocked by Initial send"); + let remaining = tokio::time::timeout(Duration::from_secs(1), receiver.count()) + .await + .expect("send task retained the RelaySender after shutdown"); + assert_eq!(remaining, 256); + } + + #[tokio::test(start_paused = true)] + async fn pending_initial_sends_expire_without_sending_late_packets() { + let (mut remote_map, _shutdown_token, _guards) = make_remote_map(); + let (endpoint_id, receiver) = enqueue_initials(&mut remote_map, 256, 1).await; + wait_for_inbox(&mut remote_map, endpoint_id).await; + tokio::time::sleep(Duration::from_secs(4)).await; + let remaining = tokio::time::timeout(Duration::from_secs(1), receiver.count()) + .await + .expect("expired Initial still holds the sender"); + assert_eq!(remaining, 256); + } + + #[tokio::test(start_paused = true)] + async fn congested_initial_sends_have_bounded_pending_work() { + let (mut remote_map, _shutdown_token, _guards) = make_remote_map(); + let (endpoint_id, receiver) = enqueue_initials(&mut remote_map, 256, 32).await; + wait_for_inbox(&mut remote_map, endpoint_id).await; + let remaining = tokio::time::timeout(Duration::from_secs(1), receiver.count()) + .await + .expect("Initial sends did not finish after draining the queue"); + assert_eq!(remaining, 256 + 16); + } + + async fn enqueue_initials( + remote_map: &mut RemoteMap, + queued: usize, + count: usize, + ) -> ( + EndpointId, + impl futures_util::Stream + Unpin + use<>, + ) { + let endpoint_id = SecretKey::from_bytes(&[1u8; 32]).public(); + let relay_url: RelayUrl = "https://relay.example.invalid".parse().unwrap(); + let (resolve_tx, resolve_rx) = oneshot::channel(); + remote_map + .resolve_remote( + EndpointAddr::from_parts(endpoint_id, [TransportAddr::Relay(relay_url.clone())]), + resolve_tx, + ) + .await; + assert!(resolve_rx.await.expect("resolve response").is_ok()); + + // Hold the receiver to apply backpressure without changing poll_send. + let (mut sender, receiver) = transports::TransportsSender::with_bounded_relay( + 256, + #[cfg(not(wasm_browser))] + std::iter::empty(), + ); + let transmit = Transmit { + ecn: None, + contents: b"queued", + segment_size: None, + }; + let path = transports::FourTuple::Relay { + url: relay_url, + endpoint_id, + }; + for _ in 0..queued { + poll_fn(|cx| Pin::new(&mut sender).poll_send(cx, &path, &transmit)) + .await + .expect("fill relay queue"); + } + for _ in 0..count { + remote_map + .send_to_actor( + endpoint_id, + RemoteStateMessage::SendDatagram( + Box::new(sender.clone()), + OwnedTransmit { + ecn: None, + contents: Bytes::from_static(b"initial"), + segment_size: None, + }, + ), + ) + .await; + } + (endpoint_id, receiver) + } + + async fn wait_for_inbox(remote_map: &mut RemoteMap, endpoint_id: EndpointId) { + // Resolve is queued after the sends, so its reply marks them as handled. + let (ack_tx, ack_rx) = oneshot::channel(); + remote_map + .send_to_actor( + endpoint_id, + RemoteStateMessage::ResolveRemote(BTreeSet::new(), ack_tx), + ) + .await; + tokio::time::timeout(Duration::from_secs(1), ack_rx) + .await + .expect("inbox stalled") + .expect("actor dropped acknowledgement") + .expect("resolve acknowledgement"); + } + + fn make_remote_map() -> (RemoteMap, CancellationToken, impl Sized) { + let metrics = Arc::new(SocketMetrics::default()); + let watchable: Watchable> = Watchable::new(BTreeSet::new()); + let local_direct_addrs = watchable.watch(); + let shutdown_token = CancellationToken::new(); + let remote_map = RemoteMap::new( + metrics, + local_direct_addrs, + address_lookup::AddressLookupServices::default(), + shutdown_token.clone(), + Arc::new(BiasedRttPathSelector::default()), + Span::none(), + ); + let guards = (watchable, shutdown_token.clone().drop_guard()); + (remote_map, shutdown_token, guards) + } + + /// Regression test: No new RemoteStateActors may be started before + /// the task of its previous incarnation was processed. + #[tokio::test(flavor = "current_thread", start_paused = true)] + #[traced_test] + async fn poll_cleanup_preserves_restarted_sender() { + let (mut remote_map, _shutdown_token, _guards) = make_remote_map(); + let eid = SecretKey::from_bytes(&[0u8; 32]).public(); + + // Non-empty addrs so each `resolve_remote` resolves its tx + // immediately and does not park in `paths.pending_resolve_requests`; + // the actor would never idle out otherwise. + let addr_with_ip = |port: u16| { + EndpointAddr::from_parts( + eid, + [TransportAddr::Ip(SocketAddr::from(([127, 0, 0, 1], port)))], + ) + }; + + // 1. Spawn A1 and let it process a real `ResolveRemote`. + let (tx1, rx1) = oneshot::channel(); + remote_map.resolve_remote(addr_with_ip(1234), tx1).await; + assert!( + matches!(rx1.await, Ok(Ok(()))), + "First resolve completes Ok" + ); + + // 2. Advance past idle timeout. + tokio::time::sleep(Duration::from_secs(65)).await; + + // 3. Call `resolve_remote` again. The actor A1 has terminated but its task + // has not yet been cleaned up. A1's sender is still in the sender map + // but is closed. This will spawn a new actor A2. + // Before our fixes, `resolve_remote` would spawn a new actor, and when + // `cleanup` was then called, the sender to this new actor would be + // removed again. We fixed this by first processing the joined task for the + // terminated actor, so that it is removed from our task list *before* + // starting a new actor. + // We also resume time here so that we don't immediately idle-out again. + tokio::time::resume(); + let (tx2, rx2) = oneshot::channel(); + remote_map.resolve_remote(addr_with_ip(5678), tx2).await; + + // 4. Drive `cleanup`, like the socket actor does. + // Before our fixes, this would remove the sender to the just-started A2 from the sender map. + now_or_never(remote_map.cleanup()); + + // 5. A third `resolve_remote`, this time with no addrs. + // With our fix, this reaches the actor spawned above (A2); without + // the fix this would start a new actor because A2 was falsely removed from + // the senders map. + let (tx3, rx3) = oneshot::channel(); + remote_map.resolve_remote(EndpointAddr::new(eid), tx3).await; + + let outcome2 = rx2.await.expect("the resolve tx must be sent"); + let outcome3 = rx3.await.expect("the resolve tx must be sent"); + assert!(outcome2.is_ok(), "expected Ok, but got {outcome2:?}"); + assert!(outcome3.is_ok(), "expected Ok, but got {outcome3:?}"); + } +} diff --git a/vendor/iroh/src/socket/remote_map/remote_state.rs b/vendor/iroh/src/socket/remote_map/remote_state.rs new file mode 100644 index 0000000..11b0a63 --- /dev/null +++ b/vendor/iroh/src/socket/remote_map/remote_state.rs @@ -0,0 +1,1635 @@ +use std::{ + collections::{BTreeSet, VecDeque}, + net::SocketAddr, + pin::Pin, + sync::Arc, + task::Poll, +}; + +use iroh_base::{EndpointId, TransportAddr}; +use n0_error::StackResultExt; +use n0_future::{ + FuturesUnordered, FuturesUnorderedBounded, MaybeFuture, MergeUnbounded, Stream, StreamExt, + boxed::BoxStream, + future::{Boxed, now_or_never}, + task::JoinSet, + time::{self, Duration, Instant}, +}; +use n0_watcher::Watcher; +use noq::{Closed, PathStats, PathStatus, WeakConnectionHandle}; +use noq_proto::{PathError, PathEvent as NoqPathEvent, PathId, n0_nat_traversal}; +use rustc_hash::FxHashMap; +use smallvec::{SmallVec, smallvec}; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; +use tracing::{Instrument, Level, Span, debug, error, event, info_span, instrument, trace, warn}; + +use self::path_state::RemotePathState; +pub(crate) use self::path_watcher::PathStateReceiver; +pub use self::{ + path_watcher::{Path, PathEvent, PathEventStream, PathList, PathListIter, PathListStream}, + remote_info::{RemoteInfo, TransportAddrInfo, TransportAddrUsage}, +}; +use super::{MappedAddrs, Source}; +use crate::{ + address_lookup::{AddressLookupFailed, AddressLookupServices, Item as AddressLookupItem}, + endpoint::DirectAddr, + socket::{ + Metrics as SocketMetrics, RELAY_PATH_MAX_IDLE_TIMEOUT, + remote_map::remote_state::path_watcher::PathStateSender, + transports::{self, OwnedTransmit, TransportsSender}, + }, +}; + +mod path_state; +mod path_watcher; +mod remote_info; + +/// How often to attempt holepunching. +/// +/// If there have been no changes to the NAT address candidates, holepunching will not be +/// attempted more frequently than at this interval. +const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); + +/// The latency at or under which we don't try to upgrade to a better path. +const GOOD_ENOUGH_LATENCY: Duration = Duration::from_millis(10); + +// TODO: use this +// /// How long since the last activity we try to keep an established endpoint peering alive. +// /// +// /// It's also the idle time at which we stop doing QAD queries to keep NAT mappings alive. +// pub(super) const SESSION_ACTIVE_TIMEOUT: Duration = Duration::from_secs(45); + +/// How often we try to upgrade to a better path. +/// +/// Even if we have some non-relay route that works. +const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); + +/// The time after which an idle [`RemoteStateActor`] stops. +/// +/// The actor only enters the idle state if no connections are active and no inbox senders exist +/// apart from the one stored in the endpoint map. Stopping and restarting the actor in this state +/// is not an issue; a timeout here serves the purpose of not stopping-and-recreating actors +/// in a high frequency, and to keep data about previous path around for subsequent connections. +const ACTOR_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(60); + +// QUIC retransmits dropped Initials; a blocked transport must not retain them indefinitely. +const DATAGRAM_SEND_TIMEOUT: Duration = Duration::from_secs(3); +const MAX_DATAGRAM_SEND_TASKS: usize = 16; + +/// A stream of events from all paths for all connections. +/// +/// The connection is identified using [`ConnId`]. The event `Err` variant happens when the +/// actor has lagged processing the events, which is rather critical for us. +type PathEvents = MergeUnbounded< + Pin)> + Send + Sync>>, +>; + +/// A stream of events of announced NAT traversal candidate addresses for all connections. +/// +/// The connection is identified using [`ConnId`]. +type AddrEvents = MergeUnbounded< + Pin< + Box< + dyn Stream)> + Send + Sync, + >, + >, +>; + +/// The state we need to know about a single remote endpoint. +/// +/// This actor manages all connections to the remote endpoint. It will trigger holepunching +/// and select the best path etc. +pub(super) struct RemoteStateActor { + /// All connections we have to this remote endpoint. + connections: FxHashMap, + /// State of the actor and hooks into the rest of the remote endpoint. + /// + /// This is on a separate struct so that we can have parallel mutable borrows to + /// `connections` and `state`. + state: State, +} + +/// State of the [`RemoteStateActor`] and hooks into the rest of the remote endpoint. +struct State { + /// The endpoint ID of the remote endpoint. + endpoint_id: EndpointId, + + // Hooks into the rest of the Socket. + // + /// Metrics. + metrics: Arc, + /// Our local addresses. + /// + /// These are our local addresses and any reflexive transport addresses. + local_direct_addrs: n0_watcher::Direct>, + /// The mapped addresses for this endpoint. + mapped_addrs: MappedAddrs, + /// Address lookup service, cloned from the socket. + address_lookup: AddressLookupServices, + + // Internal state - Noq Connections we are managing. + // + /// Notifications when connections are closed. + connections_close: FuturesUnordered, + /// Events emitted by Noq about path changes, for all paths, all connections. + path_events: PathEvents, + /// A stream of events of announced NAT traversal candidate addresses for all connections. + addr_events: AddrEvents, + + // Internal state - Holepunching and path state. + // + /// All possible paths we are aware of. + /// + /// These paths might be entirely impossible to use, since they are added by Address Lookup + /// mechanisms. The are only potentially usable. + paths: RemotePathState, + /// Information about the last holepunching attempt. + last_holepunch: Option, + + /// The path we currently consider the preferred path to the remote endpoint. + /// + /// **We expect this path to work.** If we become aware this path is broken then it is + /// set back to `None`. Having a selected path does not mean we may not be able to get + /// a better path: e.g. when the selected path is a relay path we still need to trigger + /// holepunching regularly. + /// + /// We only select a path once the path is functional in Noq. + selected_path: Option, + /// Time at which we should schedule the next holepunch attempt. + scheduled_holepunch: Option, + /// When to next attempt opening paths in [`Self::pending_open_paths`]. + scheduled_open_path: Option, + /// Paths which we still need to open. + /// + /// They failed to open because we did not have enough CIDs issued by the remote. + pending_open_paths: VecDeque, + + // Internal state - address lookup + // + /// Stream of Address Lookup results, or always pending if Address Lookup is not running. + address_lookup_stream: Option>>, + + /// The path selector used to pick the preferred path among the candidates. + path_selector: Arc, +} + +impl RemoteStateActor { + #[allow(clippy::too_many_arguments)] + pub(super) fn new( + endpoint_id: EndpointId, + local_direct_addrs: n0_watcher::Direct>, + mapped_addrs: MappedAddrs, + metrics: Arc, + address_lookup: AddressLookupServices, + path_selector: Arc, + ) -> Self { + Self { + connections: FxHashMap::default(), + state: State { + endpoint_id, + metrics: metrics.clone(), + local_direct_addrs, + mapped_addrs, + address_lookup, + connections_close: Default::default(), + path_events: Default::default(), + addr_events: Default::default(), + paths: RemotePathState::new(metrics), + last_holepunch: None, + selected_path: Default::default(), + scheduled_holepunch: None, + scheduled_open_path: None, + pending_open_paths: VecDeque::new(), + address_lookup_stream: None, + path_selector, + }, + } + } + + pub(super) fn start( + self, + initial_msgs: Vec, + tasks: &mut JoinSet<(EndpointId, Vec)>, + shutdown_token: CancellationToken, + parent_span: Span, + ) -> mpsc::Sender { + let (tx, rx) = mpsc::channel(16); + let endpoint_id = self.state.endpoint_id; + + // Ideally we'd use the endpoint span as parent. We'd have to plug that span into + // here somehow. Instead we have no parent and explicitly set the me attribute. If + // we don't explicitly set a span we get the spans from whatever call happens to + // first create the actor, which is often very confusing as it then keeps those + // spans for all logging of the actor. + tasks.spawn( + self.run(initial_msgs, rx, shutdown_token) + .instrument(info_span!( + parent: parent_span, + "RemoteStateActor", + remote = %endpoint_id.fmt_short(), + )), + ); + tx + } + + /// Runs the main loop of the actor. + async fn run( + mut self, + initial_msgs: Vec, + mut inbox: mpsc::Receiver, + shutdown_token: CancellationToken, + ) -> (EndpointId, Vec) { + trace!("actor started"); + let mut send_tasks = FuturesUnorderedBounded::new(MAX_DATAGRAM_SEND_TASKS); + for msg in initial_msgs { + self.handle_message(msg, &mut send_tasks); + } + let idle_timeout = time::sleep(ACTOR_MAX_IDLE_TIMEOUT); + n0_future::pin!(idle_timeout); + + let check_connections = time::interval(UPGRADE_INTERVAL); + n0_future::pin!(check_connections); + // Optional custom selection refresh; no interval runs for default selectors. + let refresh_interval = self.state.path_selector.refresh_interval().map(|interval| + interval.clamp(Duration::from_millis(250), Duration::from_secs(60))); + let refresh_paths = time::interval(refresh_interval.unwrap_or(Duration::from_secs(3600))); + n0_future::pin!(refresh_paths); + + loop { + let scheduled_path_open = match self.state.scheduled_open_path { + Some(when) => MaybeFuture::Some(time::sleep_until(when)), + None => MaybeFuture::None, + }; + n0_future::pin!(scheduled_path_open); + let scheduled_hp = match self.state.scheduled_holepunch { + Some(when) => MaybeFuture::Some(time::sleep_until(when)), + None => MaybeFuture::None, + }; + n0_future::pin!(scheduled_hp); + if !self.is_idle(&inbox) { + idle_timeout + .as_mut() + .reset(Instant::now() + ACTOR_MAX_IDLE_TIMEOUT); + } + + tokio::select! { + biased; + + _ = shutdown_token.cancelled() => { + trace!("actor cancelled"); + break; + } + Some(()) = send_tasks.next(), if !send_tasks.is_empty() => {} + msg = inbox.recv() => { + match msg { + Some(msg) => self.handle_message(msg, &mut send_tasks), + None => break, + } + } + Some((id, evt)) = self.state.path_events.next() => { + self.handle_path_event(id, evt); + } + Some((id, evt)) = self.state.addr_events.next() => { + trace!(?id, ?evt, "remote addrs updated, triggering holepunching"); + self.trigger_holepunching(); + } + Some((conn_id, closed)) = self.state.connections_close.next(), if !self.state.connections_close.is_empty() => { + self.handle_connection_close(conn_id, closed); + } + res = self.state.local_direct_addrs.updated() => { + if let Err(n0_watcher::Disconnected) = res { + trace!("direct address watcher disconnected, shutting down"); + break; + } + self.update_local_direct_address(); + trace!("local addrs updated, triggering holepunching"); + self.trigger_holepunching(); + } + _ = &mut scheduled_path_open => { + trace!("triggering scheduled path_open"); + self.state.scheduled_open_path = None; + let mut addrs = std::mem::take(&mut self.state.pending_open_paths); + while let Some(addr) = addrs.pop_front() { + self.open_path_on_all_conns(&addr); + } + } + _ = &mut scheduled_hp => { + trace!("triggering scheduled holepunching"); + self.state.scheduled_holepunch = None; + self.trigger_holepunching(); + } + Some(item) = maybe_next(self.state.address_lookup_stream.as_mut()), if self.state.address_lookup_stream.is_some() => { + self.state.handle_address_lookup_item(item); + } + _ = check_connections.tick() => { + self.check_connections(); + } + _ = refresh_paths.tick(), if refresh_interval.is_some() && !self.connections.is_empty() => { + self.select_path_with_force_apply(false); + } + _ = &mut idle_timeout => { + if self.is_idle(&inbox) { + trace!("idle timeout expired and still idle: terminate actor"); + break; + } else { + // Seems like we weren't really idle, so we reset + idle_timeout.as_mut().reset(Instant::now() + ACTOR_MAX_IDLE_TIMEOUT); + } + } + } + } + + inbox.close(); + // There might be a race between checking `inbox.is_empty()` and `inbox.close()`, + // so we pull out all messages that are left over. + let mut leftover_msgs = Vec::with_capacity(inbox.len()); + inbox.recv_many(&mut leftover_msgs, inbox.len()).await; + + trace!("actor terminating"); + (self.state.endpoint_id, leftover_msgs) + } + + /// Returns `true` if the actor is fully idle. + fn is_idle(&self, inbox: &mpsc::Receiver) -> bool { + self.connections.is_empty() + && inbox.is_empty() + && self.state.paths.resolve_requests_is_empty() + } + + /// Handles an actor message. + #[instrument(skip(self))] + fn handle_message( + &mut self, + msg: RemoteStateMessage, + send_tasks: &mut FuturesUnorderedBounded>, + ) { + match msg { + RemoteStateMessage::SendDatagram(sender, transmit) => { + self.state + .handle_msg_send_datagram(sender, transmit, send_tasks); + } + RemoteStateMessage::AddConnection(handle, tx) => { + self.handle_msg_add_connection(handle, tx); + } + RemoteStateMessage::ResolveRemote(addrs, tx) => { + self.state.handle_msg_resolve_remote(addrs, tx); + } + RemoteStateMessage::RemoteInfo(tx) => { + let addrs = self.state.paths.to_remote_addrs(); + let info = RemoteInfo { + endpoint_id: self.state.endpoint_id, + addrs, + }; + tx.send(info).ok(); + } + RemoteStateMessage::NetworkChange { is_major } => { + self.handle_msg_network_change(is_major); + } + } + } + + /// Handles [`RemoteStateMessage::AddConnection`]. + /// + /// Error returns are fatal and kill the actor. + fn handle_msg_add_connection( + &mut self, + conn: noq::Connection, + tx: oneshot::Sender, + ) { + let (path_state_sender, path_state_receiver) = PathStateSender::new(); + self.state.metrics.num_conns_opened.inc(); + // Remove any conflicting stable_ids from the local state. + let conn_id = ConnId(conn.stable_id()); + self.connections.remove(&conn_id); + + // Hook up paths, NAT addresses and connection closed event streams. + self.state + .path_events + .push(Box::pin(conn.path_events().map(move |evt| (conn_id, evt)))); + self.state.addr_events.push(Box::pin( + conn.nat_traversal_updates().map(move |evt| (conn_id, evt)), + )); + self.state.connections_close.push(OnClosed::new(&conn)); + + // Add local addrs to the connection + let local_addrs = self.state.local_candidates(); + update_qnt_candidates(&conn, &local_addrs); + + // Store the connection + let conn_state = self + .connections + .entry(conn_id) + .insert_entry(ConnectionState { + handle: conn.weak_handle(), + path_state: path_state_sender, + paths: Default::default(), + has_been_direct: false, + }) + .into_mut(); + + // Store PathId(0), set path_status and select best path, check if holepunching + // is needed. + if let Some(path) = conn.path(PathId::ZERO) { + let path_remote = self + .state + .register_and_configure_path(conn_id, conn_state, &path); + + if let Some(path_remote) = path_remote + && !path_remote.is_relay() + && conn.side().is_client() + { + // We may have raced this with a relay address. Try and add any + // relay addresses we have back. + let relays = self + .state + .paths + .addrs() + .filter(|addr| addr.is_relay()) + .map(|addr| transports::FourTuple::from_remote(addr.clone())) + .collect::>(); + for open_addr in relays { + self.state + .open_path_on_conn(conn_id, conn_state, &conn, &open_addr); + } + } + } + self.trigger_holepunching(); + self.select_path(); + tx.send(path_state_receiver).ok(); + } + + /// Handles [`RemoteStateMessage::NetworkChange`]. + fn handle_msg_network_change(&mut self, is_major: bool) { + // Ping all the paths so loss-detection starts ASAP. + for conn in self.connections.values() { + if let Some(noq_conn) = conn.handle.upgrade() { + for (path_id, addr) in &conn.paths { + if let Some(path) = noq_conn.path(*path_id) { + // Ping the current path + if let Err(err) = path.ping() { + warn!(%err, %path_id, ?addr, "failed to ping path"); + } + } + } + } + } + + if is_major { + self.trigger_holepunching(); + } + } + + fn handle_connection_close(&mut self, conn_id: ConnId, closed: Closed) { + event!( + target: "iroh::_events::conn::closed", + Level::DEBUG, + %conn_id, + remote_id = %self.state.endpoint_id.fmt_short(), + reason=?closed.reason, + ); + + if let Some(conn_state) = self.connections.remove(&conn_id) { + self.state.metrics.num_conns_closed.inc(); + conn_state.path_state.close(closed); + } + if self.connections.is_empty() { + trace!("last connection closed - clearing selected_path"); + self.state.selected_path = None; + } + } + + /// Updates the local [`DirectAddr`]s to all connections. + /// + /// Each connection needs to have the local direct addresses to use as QNT address + /// candidates. + fn update_local_direct_address(&mut self) { + let local_addrs = self.state.local_candidates(); + for conn in self.connections.values().filter_map(|s| s.handle.upgrade()) { + update_qnt_candidates(&conn, &local_addrs); + } + // todo: trace + } + + /// Triggers holepunching to the remote endpoint. + /// + /// This will manage the entire process of holepunching with the remote endpoint. + /// + /// - Holepunching happens on the Connection with the lowest [`ConnId`] which is a + /// client. + /// - Both endpoints may initiate holepunching if both have a client connection. + /// - Any opened paths are opened on all other connections without holepunching. + /// - If there are no changes in local or remote candidate addresses since the + /// last attempt **and** there was a recent attempt, a trigger_holepunching call + /// will be scheduled instead. + fn trigger_holepunching(&mut self) { + if self.connections.is_empty() { + trace!("not holepunching: no connections"); + return; + } + + let Some(conn) = self + .connections + .iter() + .filter_map(|(id, state)| state.handle.upgrade().map(|conn| (*id, conn))) + .filter(|(_, conn)| conn.side().is_client()) + .min_by_key(|(id, _)| *id) + .map(|(_, conn)| conn) + else { + trace!("not holepunching: no client connection"); + return; + }; + let remote_candidates = match conn.get_remote_nat_traversal_addresses() { + Ok(addrs) => BTreeSet::from_iter(addrs), + Err(err) => { + warn!("failed to get nat candidate addresses: {err:#}"); + return; + } + }; + let local_candidates = self.state.local_candidates(); + let new_candidates = self + .state + .last_holepunch + .as_ref() + .map(|last_hp| { + // Addrs are allowed to disappear, but if there are new ones we need to + // holepunch again. + trace!( + ?last_hp, + ?local_candidates, + ?remote_candidates, + "candidates to holepunch?" + ); + !remote_candidates.is_subset(&last_hp.remote_candidates) + || !local_candidates.is_subset(&last_hp.local_candidates) + }) + .unwrap_or(true); + if !new_candidates && let Some(ref last_hp) = self.state.last_holepunch { + let next_hp = last_hp.when + HOLEPUNCH_ATTEMPTS_INTERVAL; + let now = Instant::now(); + if next_hp > now { + trace!(scheduled_in = ?(next_hp - now), "not holepunching: no new addresses"); + self.state.scheduled_holepunch = Some(next_hp); + return; + } + } + + self.state.do_holepunching(conn); + } + + #[instrument(skip(self))] + fn handle_path_event(&mut self, conn_id: ConnId, event: Result) { + let Ok(event) = event else { + warn!("missed a PathEvent, RemoteStateActor lagging"); + // TODO: Is it possible to recover using the sync APIs to figure out what the + // state of the connection and it's paths are? + return; + }; + let Some(conn_state) = self.connections.get_mut(&conn_id) else { + trace!("event for removed connection"); + return; + }; + let Some(conn) = conn_state.handle.upgrade() else { + trace!("event for closed connection"); + return; + }; + trace!("path event"); + match event { + NoqPathEvent::Established { id: path_id, .. } => { + let Some(path) = conn.path(path_id) else { + trace!("path open event for unknown path"); + return; + }; + + self.state + .register_and_configure_path(conn_id, conn_state, &path); + self.select_path(); + } + NoqPathEvent::Abandoned { id, reason, .. } => { + // Remove abandoned path from the conn state. + let Some(network_path) = conn_state.remove_path(&id, &conn) else { + debug!(%id, "path not in path_id_map"); + return; + }; + + // We track all known remote addresses for the peer in `State::paths`. The + // paths are tracked by remote address only (we ignore the local + // IP). Therefore, we mark a remote addr as abandoned in the remote-global + // state only once no connections have any path to that remote addr. + if !conn_state + .paths + .values() + .any(|tuple| tuple.remote() == network_path.remote()) + { + self.state.paths.abandoned_path(&network_path.remote()); + } + + event!( + target: "iroh::_events::path::abandoned", + Level::DEBUG, + remote = %self.state.endpoint_id.fmt_short(), + %conn_id, + path_id = %id, + %network_path, + ?reason + ); + + // If the remote closed our selected path, select a new one. + self.select_path(); + } + NoqPathEvent::Discarded { id, path_stats, .. } => { + trace!(%id, ?path_stats, "path discarded"); + } + NoqPathEvent::RemoteStatus { .. } | NoqPathEvent::ObservedAddr { .. } => { + // Nothing to do for these events. + } + _ => { + // We expect to keep noq and iroh in sync in all test setups, but in production it's totally possible + // that iroh itself is linked against a newer version of noq with additional events we don't yet + // know how to handle. + #[cfg(test)] + panic!("Unhandled path event: {event:?}"); + } + } + } + + /// Selects the preferred path by invoking the configured [`PathSelector`]. + /// + /// The selected path is added to any connections which do not yet have it. Any unused + /// direct paths are closed for all connections. + #[instrument(skip_all)] + fn select_path(&mut self) { + self.select_path_with_force_apply(true); + } + + fn select_path_with_force_apply(&mut self, force_apply: bool) { + let current_path = self.state.selected_path.as_ref(); + let selected_addr = { + let ctx = PathSelectionContext::new(current_path, &self.connections); + self.state.path_selector.select(&ctx).selected().cloned() + }; + + let changed = selected_addr.as_ref().is_some_and(|addr| self.state.selected_path.as_ref() != Some(addr)); + if let Some(addr) = selected_addr + && changed + { + let prev_remote = self.state.selected_path.replace(addr.clone()); + event!( + target: "iroh::_events::path::selected", + Level::DEBUG, + remote = %self.state.endpoint_id.fmt_short(), + network_path = %addr, + prev_network_path = %prev_remote.map(|p| format!("{p}")).unwrap_or("None".to_string()), + ); + } else { + trace!(?current_path, "keeping current path"); + } + + if force_apply || changed { + self.apply_selected_path(); + } + } + + /// Propagates a change of [`State::selected_path`] to noq. + /// + /// Iterates over all connections and applies the selected path as follows: + /// - Closes non-selected IP paths (but keeps one IP path open still) + /// - Sets all non-selected paths to [`PathStatus::Backup`] + /// - Opens the selected path if it does not exist on the connection + /// - Sets the selected path to [`PathStatus::Available`] + fn apply_selected_path(&mut self) { + let Some(selected) = self.state.selected_path.clone() else { + // We can't open the selected path on all paths if we don't have one yet. And + // we can't close all "unselected" paths either, because we don't know which one + // is selected. + return; + }; + + for (conn_id, conn_state) in self.connections.iter() { + let Some(conn) = conn_state.handle.upgrade() else { + continue; + }; + + // Open path if it doesn't exist yet. + self.state + .open_path_on_conn(*conn_id, conn_state, &conn, &selected); + + for (path_id, path_fourtuple) in conn_state.paths.iter() { + let Some(path) = conn.path(*path_id) else { + continue; + }; + + // Closes redundant IP paths so that at most one remains per connection. + // + // Relay and custom paths are kept open. Only the client closes paths, + // to avoid the client and server independently closing different paths + // and racing to abandon the last one. + if conn.side().is_client() + && path_fourtuple.is_ip() + && path_fourtuple != &selected + && conn_state.paths.values().filter(|a| a.is_ip()).count() > 1 + { + trace!(?path_fourtuple, %conn_id, %path_id, "closing direct path"); + match path.close() { + Err(noq_proto::ClosePathError::MultipathNotNegotiated) => { + error!("multipath not negotiated"); + } + Err(noq_proto::ClosePathError::LastOpenPath) => { + error!("could not close last open path"); + } + Err(noq_proto::ClosePathError::ClosedPath) => { + // We already closed this. + } + Ok(()) => {} + } + continue; + } + + // Set path status: The selected path becomes Available, all other paths + // become Backup. + self.state.set_path_status(*conn_id, &path, path_fourtuple); + } + + // Record the new selected path in the path watcher. + conn_state.path_state.record_selected(&selected); + } + } + + fn open_path_on_all_conns(&mut self, open_addr: &transports::FourTuple) { + for (conn_id, conn_state) in self.connections.iter() { + let Some(conn) = conn_state.handle.upgrade() else { + continue; + }; + self.state + .open_path_on_conn(*conn_id, conn_state, &conn, open_addr); + } + } + + /// Handles regularly checking if any paths need hole punching currently + /// + /// Currently we need to have 1 IP path, with a good enough latency. + fn check_connections(&mut self) { + let mut is_goodenough = true; + for conn_state in self.connections.values() { + let mut is_conn_goodenough = false; + if let Some(conn) = conn_state.handle.upgrade() { + let min_ip_rtt = conn_state + .paths + .iter() + .filter_map(|(path_id, addr)| { + if addr.is_ip() { + conn.path_stats(*path_id).map(|stats| stats.rtt) + } else { + None + } + }) + .min(); + + if let Some(min_ip_rtt) = min_ip_rtt { + let is_latency_goodenough = min_ip_rtt <= GOOD_ENOUGH_LATENCY; + is_conn_goodenough = is_latency_goodenough; + } else { + // No IP transport found + is_conn_goodenough = false; + } + } + is_goodenough &= is_conn_goodenough; + } + + if !is_goodenough { + debug!("connections are not good enough, triggering holepunching"); + self.trigger_holepunching(); + } + } +} + +impl State { + /// Handles [`RemoteStateMessage::SendDatagram`]. + fn handle_msg_send_datagram( + &mut self, + sender: Box, + transmit: OwnedTransmit, + send_tasks: &mut FuturesUnorderedBounded>, + ) { + // Sending datagrams might fail, e.g. because we don't have the right transports set + // up to handle sending this owned transmit to. + // After all, we try every single path that we know (relay URL, IP address), even + // though we might not have a relay transport or ip-capable transport set up. + // So these errors must not be fatal for this actor (or even this operation). + + let targets = if let Some(addr) = self.selected_path.as_ref() { + trace!(?addr, "sending datagram to selected path"); + + // TODO(Frando): We might want to include a local IP here in the future, if we confidently + // know that it is the correct one. + // See https://github.com/n0-computer/iroh/issues/4280. + smallvec![transports::FourTuple::from_remote(addr.remote())] + } else { + trace!( + paths = ?self.paths.addrs().collect::>(), + "sending datagram to all known paths", + ); + if self.paths.is_empty() { + warn!("Cannot send datagrams: No paths to remote endpoint known"); + } + + let mut targets = SmallVec::new(); + for addr in self.paths.addrs() { + // We never want to send to our local addresses. + // The local address set is updated in the main loop so we can use `peek` here. + if let transports::Addr::Ip(sockaddr) = addr + && self + .local_direct_addrs + .peek() + .iter() + .any(|a| a.addr == *sockaddr) + { + trace!(%sockaddr, "not sending datagram to our own address"); + + // TODO(Frando): We might want to include a local IP here in the future, if we confidently + // know that it is the correct one. + // See https://github.com/n0-computer/iroh/issues/4280. + } else { + targets.push(transports::FourTuple::from_remote(addr.clone())); + } + } + // This message is received *before* a connection is added. So we do + // not yet have a connection to holepunch. Instead we trigger + // holepunching when AddConnection is received. + targets + }; + + if targets.is_empty() { + return; + } + let send = Box::pin( + async move { + if time::timeout( + DATAGRAM_SEND_TIMEOUT, + send_datagram_to_targets(sender, transmit, targets), + ) + .await + .is_err() + { + debug!("Datagram send timed out"); + } + } + .instrument(Span::current()), + ); + if send_tasks.try_push(send).is_err() { + debug!("dropping datagram: send task limit reached"); + } + } + + /// Handles [`RemoteStateMessage::ResolveRemote`]. + fn handle_msg_resolve_remote( + &mut self, + addrs: BTreeSet, + tx: oneshot::Sender>, + ) { + let addrs = to_transports_addr(self.endpoint_id, addrs); + self.paths.insert_multiple(addrs, Source::App); + self.paths.resolve_remote(tx); + // Start Address Lookup if we have no selected path. + self.trigger_address_lookup(); + } + + /// Triggers Address Lookup for the remote endpoint, if needed. + /// + /// Does not start Address Lookup if we have a selected path or if Address Lookup is + /// currently running. + fn trigger_address_lookup(&mut self) { + if self.selected_path.is_some() || self.address_lookup_stream.is_some() { + return; + } + let stream = self.address_lookup.resolve(self.endpoint_id); + let stream = stream.filter_map(|item| match item { + // We don't care about errors from individual services, we just continue. + // Individual errors are buffered into the final error by `AddressLookupServices::resolve`, + // and if the lookup fails we return them upstream with the final `AddressLookupFailed` error. + Ok(Err(_err)) => None, + Ok(Ok(item)) => Some(Ok(item)), + Err(err) => Some(Err(err)), + }); + self.address_lookup_stream = Some(Box::pin(stream)); + } + + /// Handles an address lookup result. + /// + /// All address lookup results end up being sent here. It takes care of updating the + /// [`RemotePathState`] with the results. + fn handle_address_lookup_item( + &mut self, + item: Option>, + ) { + match item { + None => { + self.paths.address_lookup_finished(Ok(())); + self.address_lookup_stream = None; + } + Some(Err(err)) => { + if let AddressLookupFailed::NoServiceConfigured { .. } = err { + trace!("Address Lookup not configured"); + } else { + debug!("Address Lookup failed: {err:#}"); + } + self.paths.address_lookup_finished(Err(err)); + self.address_lookup_stream = None; + } + Some(Ok(item)) => { + if item.endpoint_id() != self.endpoint_id { + warn!( + ?item, + "Address Lookup emitted item for wrong remote endpoint" + ); + } else { + let source = Source::AddressLookup { + name: item.provenance().to_string(), + }; + let addrs = + to_transports_addr(self.endpoint_id, item.into_endpoint_addr().addrs); + self.paths.insert_multiple(addrs, source); + } + } + } + } + + /// Unconditionally perform holepunching. + #[instrument(skip_all)] + fn do_holepunching(&mut self, conn: noq::Connection) { + self.metrics.holepunch_attempts.inc(); + let local_candidates = self.local_candidates(); + match conn.initiate_nat_traversal_round() { + Ok(remote_candidates) => { + let remote_candidates = remote_candidates + .iter() + .map(|addr| SocketAddr::new(addr.ip().to_canonical(), addr.port())) + .collect(); + event!( + target: "iroh::_events::qnt::init", + Level::DEBUG, + remote = %self.endpoint_id.fmt_short(), + ?local_candidates, + ?remote_candidates, + ); + self.last_holepunch = Some(HolepunchAttempt { + when: Instant::now(), + local_candidates, + remote_candidates, + }); + } + Err(err) => { + debug!("failed to initiate NAT traversal: {err:#}"); + use noq_proto::n0_nat_traversal::Error; + match err { + Error::Closed + | Error::TooManyAddresses + | Error::WrongConnectionSide + | Error::ExtensionNotNegotiated => { + // Fatal, no need to retry for now + } + Error::Multipath(_) | Error::NotEnoughAddresses => { + // Retry in a bit + let now = Instant::now(); + let next_hp = now + Duration::from_millis(100); + trace!(scheduled_in = ?(next_hp - now), "holepunching retry"); + self.scheduled_holepunch = Some(next_hp); + } + } + } + } + } + + /// Register a path with our state and configure path-specific settings. + /// + /// This inserts the path in the [`ConnectionState`] and [`Self::paths`]. + /// + /// It configures the path with the correct path status (see [`Self::set_path_status`]), + /// and applies path-type-specific settings: + /// Relay paths get a longer idle timeout to accommodate transparent reconnection + /// by the relay actor (see [`RELAY_PATH_MAX_IDLE_TIMEOUT`]). + fn register_and_configure_path( + &mut self, + conn_id: ConnId, + conn_state: &mut ConnectionState, + path: &noq::Path, + ) -> Option { + let network_path = self + .mapped_addrs + .to_transport_tuple(&path.network_path().ok()?)?; + event!( + target: "iroh::_events::path::open", + Level::DEBUG, + remote = %self.endpoint_id.fmt_short(), + %conn_id, + path_id=%path.id(), + %network_path, + ); + conn_state.add_open_path(network_path.clone(), path.id(), &self.metrics); + if network_path.is_relay() + && let Err(e) = path.set_max_idle_timeout(Some(RELAY_PATH_MAX_IDLE_TIMEOUT)) + { + debug!(?e, "failed to set relay path idle timeout"); + } + + self.set_path_status(conn_id, path, &network_path); + self.paths + .insert_open_path(network_path.remote(), Source::Connection); + Some(network_path) + } + + fn set_path_status( + &mut self, + conn_id: ConnId, + path: &noq::Path, + network_path: &transports::FourTuple, + ) { + let status = self.path_status_for_addr(network_path); + match path.set_status(status) { + Err(error) => warn!(?error, ?network_path, ?status, "set_status failed"), + Ok(prev_status) if prev_status != status => { + event!( + target: "iroh::_events::path::set_status", + Level::DEBUG, + remote = %self.endpoint_id.fmt_short(), + %conn_id, + path_id=%path.id(), + %network_path, + ?status, + ?prev_status, + ); + } + Ok(_) => {} + } + } + + fn open_path_on_conn( + &mut self, + conn_id: ConnId, + conn_state: &ConnectionState, + conn: &noq::Connection, + open_4tuple: &transports::FourTuple, + ) { + // Only the client opens paths; the server receives them via + // QUIC frames and reacts to PathOpened events. + if conn.side().is_server() { + return; + } + // Already open on this connection; nothing to do. + if conn_state.paths.values().any(|a| a == open_4tuple) { + return; + } + + let mapped_4tuple = self.mapped_addrs.to_mapped_tuple(open_4tuple); + let path_status = self.path_status_for_addr(open_4tuple); + + let fut = conn.open_path_ensure(mapped_4tuple, path_status); + match fut.path_id() { + Some(path_id) => { + trace!(%conn_id, %path_id, ?path_status, "opening new path"); + } + None => { + let ret = now_or_never(fut); + match ret { + Some(Err(PathError::RemoteCidsExhausted)) + | Some(Err(PathError::MaxPathIdReached)) => { + self.scheduled_open_path = + Some(Instant::now() + Duration::from_millis(333)); + self.pending_open_paths.push_back(open_4tuple.clone()); + trace!(?open_4tuple, ?ret, "scheduling open_path"); + } + _ => warn!(?ret, "Opening path failed"), + } + } + } + } + + /// Returns the [`PathStatus`] for `addr`. + /// + /// Returns [`PathStatus::Available`] if `addr` is the currently-selected path, + /// or [`PathStatus::Backup`] otherwise. + fn path_status_for_addr(&self, addr: &transports::FourTuple) -> PathStatus { + if Some(addr) == self.selected_path.as_ref() { + PathStatus::Available + } else { + PathStatus::Backup + } + } + + /// Returns the current set of local direct addresses. + fn local_candidates(&mut self) -> BTreeSet { + self.local_direct_addrs + .get() + .iter() + .map(|d| d.addr) + .collect() + } +} + +/// Updates QNT's candidate addresses to be the current set of direct addresses. +/// +/// `direct_addrs` must be a set of addresses extracted from the endpoint's current +/// [`DirectAddr`]s. +fn update_qnt_candidates(conn: &noq::Connection, direct_addrs: &BTreeSet) { + let noq_candidates = match conn.get_local_nat_traversal_addresses() { + Ok(addrs) => BTreeSet::from_iter(addrs), + Err(err) => { + warn!("failed to get local nat candidates: {err:#}"); + return; + } + }; + for addr in direct_addrs.difference(&noq_candidates) { + if let Err(err) = conn.add_nat_traversal_address(*addr) { + warn!("failed adding local addr: {err:#}",); + } + } + for addr in noq_candidates.difference(direct_addrs) { + if let Err(err) = conn.remove_nat_traversal_address(*addr) { + warn!("failed removing local addr: {err:#}"); + } + } + trace!(?direct_addrs, "updated local QNT addresses"); +} + +fn send_datagram<'a>( + sender: &'a mut TransportsSender, + addr: transports::FourTuple, + owned_transmit: OwnedTransmit, +) -> impl Future> + 'a { + std::future::poll_fn(move |cx| { + let transmit = transports::Transmit { + ecn: owned_transmit.ecn, + contents: owned_transmit.contents.as_ref(), + segment_size: owned_transmit.segment_size, + }; + + Pin::new(&mut *sender) + .poll_send(cx, &addr, &transmit) + .map(|res| res.with_context(|_| format!("failed to send datagram to {:?}", addr))) + }) +} + +async fn send_datagram_to_targets( + sender: Box, + transmit: OwnedTransmit, + targets: SmallVec<[transports::FourTuple; 8]>, +) { + let mut sends = targets + .into_iter() + .map(|target| { + let mut sender = sender.clone(); + let transmit = transmit.clone(); + async move { + if let Err(err) = send_datagram(&mut sender, target.clone(), transmit).await { + debug!(?target, "failed to send datagram: {err:#}"); + } + } + }) + .collect::>(); + while sends.next().await.is_some() {} +} + +/// Messages to send to the [`RemoteStateActor`]. +#[derive(derive_more::Debug)] +pub(crate) enum RemoteStateMessage { + /// Sends a datagram to all known paths. + /// + /// Used to send QUIC Initial packets. If there is no working direct path this will + /// trigger holepunching. + /// + /// This is not acceptable to use on the normal send path, as it is an async send + /// operation with a bunch more copying. So it should only be used for sending QUIC + /// Initial packets. + #[debug("SendDatagram(..)")] + SendDatagram(Box, OwnedTransmit), + /// Adds an active connection to this remote endpoint. + /// + /// The actor will downgrade the connection to a [`noq::WeakConnectionHandle`] as soon + /// as it processes the message. It will keep hold of the weak handle until it closes, + /// but only update to a strong [`noq::Connection`] for brief moments. + /// + /// The actor will actively manage paths on the connection and start holepunching as needed. + #[debug("AddConnection({})", _0.stable_id())] + AddConnection(noq::Connection, oneshot::Sender), + /// Asks if there is any possible path that could be used. + /// + /// This adds the provided transport addresses to the list of potential paths for this + /// remote and starts Address Lookup if needed. + /// + /// Sends back `Ok` immediately if the provided address list is non-empy or we have are + /// other known paths. Otherwise sends back `Ok` once Address Lookup produces a result, + /// or the Address Lookup error if Address Lookup fails or produces no results, + #[debug("ResolveRemote(..)")] + ResolveRemote( + BTreeSet, + oneshot::Sender>, + ), + /// Returns information about the remote. + /// + /// This currently only includes a list of all known transport addresses for the remote. + RemoteInfo(oneshot::Sender), + /// The network status has changed in some way + NetworkChange { is_major: bool }, +} + +/// Information about a holepunch attempt. +/// +/// Addresses are always stored in canonical form. +#[derive(Debug)] +struct HolepunchAttempt { + when: Instant, + /// The set of local addresses which could take part in holepunching. + /// + /// This does not mean every address here participated in the holepunching. E.g. we + /// could have tried only a sub-set of the addresses because a previous attempt already + /// covered part of the range. + /// + /// We do not store this as a [`DirectAddr`] because this is checked for equality and we + /// do not want to compare the sources of these addresses. + local_candidates: BTreeSet, + /// The set of remote addresses which could take part in holepunching. + /// + /// Like [`Self::local_candidates`] we may not have used them. + remote_candidates: BTreeSet, +} + +/// Newtype to track Connections. +/// +/// The wrapped value is the [`noq::Connection::stable_id`] value, and is thus only valid +/// for active connections. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Display)] +#[display("{_0}")] +struct ConnId(usize); + +/// A connection managed by the [`RemoteStateActor`]. +/// +/// - A handle to the connection. +/// - The paths we know about. +/// - Some stuff to make observers happy. +#[derive(Debug)] +struct ConnectionState { + /// Weak handle to the connection. + handle: WeakConnectionHandle, + /// Writer-side handle for the connection's path observation state. + /// + /// The matching [`PathStateReceiver`] is held by the [`Connection`]. + /// + /// [`Connection`]: crate::endpoint::Connection + path_state: PathStateSender, + /// The open paths that exist on this connection. + /// + /// This might be lagging from the connection state inside noq itself as this can only + /// be updated once we received the event from noq. + /// + /// IP paths *should* have the local IP address filled in by the time we receive the + /// event. The noq established event is only emitted once at least one datagram is + /// received from the peer on the path. + paths: FxHashMap, + /// Whether this connection has ever had a direct path. + /// + /// Used for recording metrics. + has_been_direct: bool, +} + +impl ConnectionState { + /// Tracks an open path for the connection. + fn add_open_path( + &mut self, + network_path: transports::FourTuple, + path_id: PathId, + metrics: &Arc, + ) { + match network_path { + transports::FourTuple::Ip { .. } => metrics.paths_direct.inc(), + transports::FourTuple::Relay { .. } => metrics.paths_relay.inc(), + transports::FourTuple::Custom { .. } => metrics.paths_custom.inc(), + }; + if !self.has_been_direct && network_path.is_ip() { + self.has_been_direct = true; + metrics.num_conns_direct.inc(); + } + + self.paths.insert(path_id, network_path.clone()); + if let Some(conn) = self.handle.upgrade() + && let Some(path) = conn.path(path_id) + { + let handle = path.weak_handle(); + self.path_state.record_opened(handle, network_path); + } + } + + /// Removes a path from this connection. + fn remove_path( + &mut self, + path_id: &PathId, + conn: &noq::Connection, + ) -> Option { + let addr = self.paths.remove(path_id)?; + self.path_state.record_abandoned(*path_id, conn); + Some(addr) + } +} + +/// State of the endpoint relevant for path selection. +/// +/// Constructed by the endpoint and passed to [`PathSelector::select`]. Borrows from +/// the endpoint's internal data. +#[derive(Debug)] +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +pub struct PathSelectionContext<'a> { + current: Option<&'a transports::FourTuple>, + source: PathsSource<'a>, +} + +/// Either a reference to live connection state, or a synthesized list of paths +/// (for unit-testing selectors). +#[derive(Debug)] +enum PathsSource<'a> { + Live(&'a FxHashMap), + #[cfg(test)] + Test(Vec>), +} + +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +impl<'a> PathSelectionContext<'a> { + fn new( + current: Option<&'a transports::FourTuple>, + connections: &'a FxHashMap, + ) -> Self { + Self { + current, + source: PathsSource::Live(connections), + } + } + + /// Constructs a context with synthetic path data for testing. + #[cfg(test)] + pub(crate) fn for_test( + current: Option<&'a transports::FourTuple>, + paths: Vec>, + ) -> Self { + Self { + current, + source: PathsSource::Test(paths), + } + } + + /// The path currently considered the preferred path to the remote endpoint, if any. + pub fn current(&self) -> Option<&transports::FourTuple> { + self.current + } + + /// Iterator over candidate paths. + /// + /// The same address may appear more than once when it is a path on multiple + /// connections to the remote. Selectors that care should aggregate as appropriate. + pub fn paths(&self) -> Box> + '_> { + match &self.source { + PathsSource::Live(connections) => Box::new( + connections + .values() + .filter_map(|state| state.handle.upgrade().map(|conn| (state, conn))) + .flat_map(|(state, conn)| { + state.paths.iter().map(move |(path_id, addr)| { + PathSelectionData::live(addr, *path_id, conn.clone()) + }) + }), + ), + #[cfg(test)] + PathsSource::Test(paths) => Box::new(paths.iter().cloned()), + } + } +} + +/// Data the selector sees about one candidate path. +// +// In production this borrows from a live connection and looks up stats from noq on +// demand. In `#[cfg(test)]` builds it can also wrap synthesized stats so selectors +// can be unit-tested without standing up real connections. +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +#[derive(derive_more::Debug, Clone)] +pub struct PathSelectionData<'a> { + network_path: &'a transports::FourTuple, + #[debug(skip)] + source: StatsSource, +} + +#[derive(Clone)] +enum StatsSource { + Live { + path_id: PathId, + conn: noq::Connection, + }, + /// Boxed so `PathStats` (100+ bytes, 14 fields) doesn't inflate the enum's + /// size in production where only the `Live` variant is ever constructed. + #[cfg(test)] + Test(Option>), +} + +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +impl<'a> PathSelectionData<'a> { + fn live( + network_path: &'a transports::FourTuple, + path_id: PathId, + conn: noq::Connection, + ) -> Self { + Self { + network_path, + source: StatsSource::Live { path_id, conn }, + } + } + + /// Constructs a [`PathSelectionData`] with synthetic stats for testing. + /// + /// `PathStats` is `#[non_exhaustive]` so callers build it via + /// `let mut s = PathStats::default(); s.rtt = ...;`. + #[cfg(test)] + pub(crate) fn for_test( + network_path: &'a transports::FourTuple, + stats: Option, + ) -> Self { + Self { + network_path, + source: StatsSource::Test(stats.map(Box::new)), + } + } + + /// The network path of the candidate path. + pub fn network_path(&self) -> &transports::FourTuple { + self.network_path + } + + /// Returns path statistics if available. + pub fn stats(&self) -> Option { + match &self.source { + StatsSource::Live { path_id, conn } => conn.path_stats(*path_id), + #[cfg(test)] + StatsSource::Test(stats) => stats.as_deref().copied(), + } + } +} + +/// Trait to configure path selection. +/// +/// Most users do not need to provide their own selector. +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +pub trait PathSelector: Send + Sync + std::fmt::Debug + 'static { + /// Optional RTT-driven reselection interval. Defaults to topology events only. + /// Intervals are clamped to 250ms..60s; refresh does not reapply unchanged selection. + fn refresh_interval(&self) -> Option { + None + } + + /// Pick the selected path to carry application data among the currently + /// open network paths to the remote endpoint. + /// + /// Build the result by starting from [`PathSelection::none`] and calling + /// [`PathSelection::set`] for the path the selector wants active. + /// + /// Returning an empty [`PathSelection`] keeps the current selection unchanged. + fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection; +} + +/// The set of paths a [`PathSelector`] has chosen. +/// +/// Today this holds at most one path. Build via [`PathSelection::none`] + +/// [`PathSelection::set`]. +#[derive(Debug, Clone)] +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +pub struct PathSelection { + selection: Option, +} + +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +impl PathSelection { + /// An empty selection. + pub fn none() -> Self { + Self { selection: None } + } + + /// Sets the path as the selected path. + /// + /// This discards any previously selected path and sets this one as a single selected + /// path. + pub fn set(&mut self, path: &PathSelectionData<'_>) { + if self.selection.is_some() { + tracing::warn!( + path = %path.network_path(), + "PathSelection already contains a path; ignoring additional path" + ); + return; + } + self.selection = Some(path.network_path.clone()); + } + + /// The selected path: the one data should be sent on. This is not public so + /// we can later allow for selecting multiple paths without changing the + /// public API of `PathSelection`. + /// + /// Returns `None` when nothing has been selected. + pub(crate) fn selected(&self) -> Option<&transports::FourTuple> { + self.selection.as_ref() + } +} + +/// Future that resolves to the `conn_id` once a connection is closed. +/// +/// This uses [`noq::Connection::on_closed`], which does not keep the connection alive +/// while awaiting the future. +struct OnClosed { + conn_id: ConnId, + inner: noq::OnClosed, +} + +impl OnClosed { + fn new(conn: &noq::Connection) -> Self { + Self { + conn_id: ConnId(conn.stable_id()), + inner: conn.on_closed(), + } + } +} + +impl Future for OnClosed { + type Output = (ConnId, Closed); + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let closed = std::task::ready!(Pin::new(&mut self.inner).poll(cx)); + Poll::Ready((self.conn_id, closed)) + } +} + +/// Converts an iterator of [`TransportAddr'] into an iterator of [`transports::Addr`]. +fn to_transports_addr( + endpoint_id: EndpointId, + addrs: impl IntoIterator, +) -> impl Iterator { + addrs.into_iter().filter_map(move |addr| match addr { + TransportAddr::Relay(relay_url) => Some(transports::Addr::from((relay_url, endpoint_id))), + TransportAddr::Ip(sockaddr) => Some(transports::Addr::from(sockaddr)), + TransportAddr::Custom(custom_addr) => Some(transports::Addr::from(custom_addr)), + _ => { + warn!(?addr, "Unsupported TransportAddr"); + None + } + }) +} + +/// Returns the next item if `maybe_stream` is `Some`, or `None` otherwise. +async fn maybe_next(maybe_stream: Option<&mut S>) -> Option> { + match maybe_stream { + None => None, + Some(s) => Some(s.next().await), + } +} + +#[cfg(all(test, not(wasm_browser)))] +mod tests { + use super::*; + + #[tokio::test] + async fn blocked_relay_does_not_delay_direct_initial() { + let receiver = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let (mut sender, _relay_receiver) = TransportsSender::with_bounded_relay( + 1, + [transports::IpConfig::V4 { + ip_net: "127.0.0.1/8".parse().unwrap(), + port: 0, + is_required: true, + is_default: true, + }] + .into_iter(), + ); + let relay = transports::FourTuple::Relay { + url: "https://relay.example.invalid".parse().unwrap(), + endpoint_id: iroh_base::SecretKey::from_bytes(&[1; 32]).public(), + }; + let transmit = OwnedTransmit { + ecn: None, + contents: bytes::Bytes::from_static(b"initial"), + segment_size: None, + }; + send_datagram(&mut sender, relay.clone(), transmit.clone()) + .await + .unwrap(); + let sends = send_datagram_to_targets( + Box::new(sender), + transmit, + smallvec![ + relay, + transports::FourTuple::from_remote(transports::Addr::Ip( + receiver.local_addr().unwrap(), + )), + ], + ); + let mut buf = [0; 64]; + // The Relay queue stays full while the later Direct target receives its Initial. + tokio::select! { + _ = sends => panic!("blocked Relay send unexpectedly completed"), + received = time::timeout(Duration::from_secs(1), receiver.recv(&mut buf)) => { + let len = received.expect("Direct send blocked behind Relay").unwrap(); + assert_eq!(&buf[..len], b"initial"); + } + } + } +} diff --git a/vendor/iroh/src/socket/remote_map/remote_state/path_state.rs b/vendor/iroh/src/socket/remote_map/remote_state/path_state.rs new file mode 100644 index 0000000..ca81c31 --- /dev/null +++ b/vendor/iroh/src/socket/remote_map/remote_state/path_state.rs @@ -0,0 +1,691 @@ +//! The state kept for each network path to a remote endpoint. + +use std::{ + collections::{HashMap, HashSet, VecDeque}, + sync::Arc, +}; + +use n0_error::e; +use n0_future::time::Instant; +use rustc_hash::FxHashMap; +use tokio::sync::oneshot; +use tracing::trace; + +use super::{Source, TransportAddrInfo, TransportAddrUsage}; +use crate::{address_lookup::AddressLookupFailed, metrics::SocketMetrics, socket::transports}; + +/// Maximum number of non-relay paths we keep around per endpoint. +pub(super) const MAX_NON_RELAY_PATHS: usize = 30; + +/// Maximum number of inactive non-relay paths we keep around per endpoint. +/// +/// These are paths that at one point been opened and are now closed. +pub(super) const MAX_INACTIVE_NON_RELAY_PATHS: usize = 10; + +/// Map of all paths that we are aware of for a remote endpoint. +/// +/// Also stores a list of resolve requests which are triggered once at least one path is known, +/// or once this struct is notified of a failed Address Lookup run. +#[derive(Debug)] +pub(super) struct RemotePathState { + /// All possible paths we are aware of. + /// + /// These paths might be entirely impossible to use, since they are added by Address Lookup + /// mechanisms. The are only potentially usable. + paths: FxHashMap, + /// Pending resolve requests from [`Self::resolve_remote`]. + pending_resolve_requests: VecDeque>>, + metrics: Arc, +} + +/// Describes the usability of this path, i.e. whether it has ever been opened, +/// when it was closed, or if it has never been usable. +#[derive(Debug, Default)] +pub(super) enum PathStatus { + /// This path is open and active. + Open, + /// This path was once opened, but was abandoned at the given [`Instant`]. + Inactive(Instant), + /// This path was never usable (we attempted holepunching and it didn't work). + Unusable, + /// We have not yet attempted holepunching, or holepunching is currently in + /// progress, so we do not know the usability of this path. + #[default] + Unknown, +} + +impl RemotePathState { + pub(super) fn new(metrics: Arc) -> Self { + Self { + paths: Default::default(), + pending_resolve_requests: Default::default(), + metrics, + } + } + + pub(super) fn to_remote_addrs(&self) -> Vec { + self.paths + .iter() + .flat_map(|(addr, state)| { + let usage = match state.status { + PathStatus::Open => TransportAddrUsage::Active, + PathStatus::Inactive(_) | PathStatus::Unusable | PathStatus::Unknown => { + TransportAddrUsage::Inactive + } + }; + Some(TransportAddrInfo { + addr: addr.clone().into(), + usage, + }) + }) + .collect() + } + + /// Insert a new address of an open path into our list of paths. + /// + /// This will emit pending resolve requests and trigger pruning paths. + pub(super) fn insert_open_path(&mut self, addr: transports::Addr, source: Source) { + match addr { + transports::Addr::Ip(_) => self.metrics.transport_ip_paths_added.inc(), + transports::Addr::Relay(_, _) => self.metrics.transport_relay_paths_added.inc(), + transports::Addr::Custom(_) => self.metrics.transport_custom_paths_added.inc(), + }; + let state = self.paths.entry(addr).or_default(); + state.status = PathStatus::Open; + state.sources.insert(source.clone(), Instant::now()); + self.emit_pending_resolve_requests(None); + self.prune_paths(); + } + + /// Mark a path as abandoned. + /// + /// If this path does not exist, it does nothing to the + /// `RemotePathState` + pub(super) fn abandoned_path(&mut self, addr: &transports::Addr) { + if let Some(state) = self.paths.get_mut(addr) { + if matches!(state.status, PathStatus::Open) { + match addr { + transports::Addr::Ip(_) => self.metrics.transport_ip_paths_removed.inc(), + transports::Addr::Relay(_, _) => { + self.metrics.transport_relay_paths_removed.inc() + } + transports::Addr::Custom(_) => { + self.metrics.transport_custom_paths_removed.inc() + } + }; + } + match state.status { + PathStatus::Open | PathStatus::Inactive(_) => { + state.status = PathStatus::Inactive(Instant::now()); + } + PathStatus::Unusable | PathStatus::Unknown => { + state.status = PathStatus::Unusable; + } + } + } + } + + /// Inserts multiple addresses of unknown status into our list of potential paths. + /// + /// If this caused the path set to transition from empty to non-empty, any + /// pending resolve requests are woken with `Ok(())`. Inserts that add no + /// new paths (empty iterator, or only duplicates) are a no-op: waking + /// pending requests while the path set is still empty would send a bogus + /// `AddressLookupFailed::NoResults` while an address lookup is in flight. + pub(super) fn insert_multiple( + &mut self, + addrs: impl Iterator, + source: Source, + ) { + let now = Instant::now(); + let was_empty = self.paths.is_empty(); + for addr in addrs { + self.paths + .entry(addr) + .or_default() + .sources + .insert(source.clone(), now); + } + trace!("added addressing information"); + if was_empty && !self.paths.is_empty() { + self.emit_pending_resolve_requests(None); + } + self.prune_paths(); + } + + /// Sends back on `tx` once a possible path to the remote is known. + /// + /// If there already is a known path, `Ok(())` is returned immediately. Otherwise an + /// address lookup is performed and the result is sent back once that + /// completes. [`AddressLookupFailed`] is sent if there are no known paths. + pub(super) fn resolve_remote(&mut self, tx: oneshot::Sender>) { + if !self.paths.is_empty() { + tx.send(Ok(())).ok(); + } else { + self.pending_resolve_requests.push_back(tx); + } + } + + /// Returns `true` if there are any queued resolve requests from [`Self::resolve_remote`]. + pub(super) fn resolve_requests_is_empty(&self) -> bool { + self.pending_resolve_requests.is_empty() + } + + /// Notifies that a Address Lookup run has finished. + /// + /// This will emit pending resolve requests. + pub(super) fn address_lookup_finished(&mut self, result: Result<(), AddressLookupFailed>) { + self.emit_pending_resolve_requests(result.err()); + } + + /// Returns an iterator over the addresses of all paths. + pub(super) fn addrs(&self) -> impl Iterator { + self.paths.keys() + } + + /// Returns whether this stores any addresses. + pub(super) fn is_empty(&self) -> bool { + self.paths.is_empty() + } + + /// Replies to all pending resolve requests. + /// + /// This is a no-op if no requests are queued. Replies `Ok` if we have any known paths, + /// otherwise with the provided `address_lookup_error` or with [`AddressLookupFailed::NoResults`]. + fn emit_pending_resolve_requests(&mut self, address_lookup_error: Option) { + if self.pending_resolve_requests.is_empty() { + return; + } + let result = match (self.paths.is_empty(), address_lookup_error) { + (false, _) => Ok(()), + (true, Some(err)) => Err(err), + (true, None) => Err(e!(AddressLookupFailed::NoResults { errors: Vec::new() })), + }; + for tx in self.pending_resolve_requests.drain(..) { + tx.send(result.clone()).ok(); + } + } + + /// Prune paths. + /// + /// Should be invoked any time we insert a new path. + /// + /// We currently only prune non-relay paths. For more information on the + /// criteria for when and which paths we prune, look at the [`prune_non_relay_paths`] function. + pub(super) fn prune_paths(&mut self) { + prune_non_relay_paths(&mut self.paths); + } +} + +/// The state of a single path to the remote endpoint. +/// +/// Each path is identified by the destination [`transports::Addr`] and they are stored in +/// the [`RemotePathState`] map in [`RemoteStateActor`]. +/// +/// [`RemoteStateActor`]: super::RemoteStateActor +#[derive(Debug, Default)] +pub(super) struct PathState { + /// How we learned about this path, and when. + /// + /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size + /// of the map of sources down to one entry per type of source. + pub(super) sources: HashMap, + /// The usability status of this path. + pub(super) status: PathStatus, +} + +/// Prunes the non-relay paths in the paths HashMap. +/// +/// Only prunes if the number of non-relay paths is above [`MAX_NON_RELAY_PATHS`]. +/// +/// Keeps paths that are open or of unknown status. +/// +/// Always prunes paths that have unsuccessfully holepunched. +/// +/// Keeps [`MAX_INACTIVE_NON_RELAY_PATHS`] of the most recently closed paths +/// that are not currently being used but have successfully been +/// holepunched previously. +/// +/// This all ensures that: +/// +/// - We do not have unbounded growth of paths. +/// - If we have many paths for this remote, we prune the paths that cannot hole punch. +/// - We do not prune holepunched paths that are currently not in use too quickly. For +/// example, if a large number of untested paths are added at once, we will not +/// immediately prune all of the unused, but valid, paths at once. +fn prune_non_relay_paths(paths: &mut FxHashMap) { + // if the total number of paths is less than the max, bail early + if paths.len() < MAX_NON_RELAY_PATHS { + return; + } + + let primary_paths: Vec<_> = paths.iter().filter(|(addr, _)| !addr.is_relay()).collect(); + + // if the total number of non-relay paths is less than the max, bail early + if primary_paths.len() < MAX_NON_RELAY_PATHS { + return; + } + + // paths that were opened at one point but have previously been closed + let mut inactive = Vec::with_capacity(primary_paths.len()); + // paths where we attempted hole punching but it not successful + let mut failed = Vec::with_capacity(primary_paths.len()); + + for (addr, state) in primary_paths { + match state.status { + PathStatus::Inactive(t) => { + // paths where holepunching succeeded at one point, but the path was closed. + inactive.push((addr.clone(), t)); + } + PathStatus::Unusable => { + // paths where holepunching has been attempted and failed. + failed.push(addr.clone()); + } + _ => { + // ignore paths that are open or the status is unknown + } + } + } + + // All paths are bad, don't prune all of them. + // + // This implies that `inactive` is empty. + if failed.len() == paths.len() { + // leave the max number of non-relay paths + failed.truncate(paths.len().saturating_sub(MAX_NON_RELAY_PATHS)); + } + + // sort the potentially prunable from most recently closed to least recently closed + inactive.sort_by_key(|b| std::cmp::Reverse(b.1)); + + // Prune the "oldest" closed paths. + let old_inactive = + inactive.split_off(inactive.len().saturating_sub(MAX_INACTIVE_NON_RELAY_PATHS)); + + // collect all the paths that should be pruned + let must_prune: HashSet<_> = failed + .into_iter() + .chain(old_inactive.into_iter().map(|(addr, _)| addr)) + .collect(); + + paths.retain(|addr, _| !must_prune.contains(addr)); +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::Duration, + }; + + use iroh_base::{RelayUrl, SecretKey}; + use rand::{RngExt, SeedableRng}; + + use super::*; + + fn ip_addr(port: u16) -> transports::Addr { + transports::Addr::Ip(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port).into()) + } + + fn path_state_inactive(closed: Instant) -> PathState { + PathState { + sources: HashMap::new(), + status: PathStatus::Inactive(closed), + } + } + + fn path_state_unusable() -> PathState { + PathState { + sources: HashMap::new(), + status: PathStatus::Unusable, + } + } + + #[test] + fn test_prune_under_max_paths() { + let mut paths = FxHashMap::default(); + for i in 0..20 { + paths.insert(ip_addr(i), PathState::default()); + } + + prune_non_relay_paths(&mut paths); + assert_eq!( + 20, + paths.len(), + "should not prune when under MAX_NON_RELAY_PATHS" + ); + } + + #[test] + fn test_prune_at_max_paths_no_prunable() { + let mut paths = FxHashMap::default(); + // All paths are active (never abandoned), so none should be pruned + for i in 0..MAX_NON_RELAY_PATHS { + paths.insert(ip_addr(i as u16), PathState::default()); + } + + prune_non_relay_paths(&mut paths); + assert_eq!( + MAX_NON_RELAY_PATHS, + paths.len(), + "should not prune active paths" + ); + } + + #[test] + fn test_prune_failed_holepunch() { + let mut paths = FxHashMap::default(); + + // Add 20 active paths + for i in 0..20 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 15 failed holepunch paths (must_prune) + for i in 20..35 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + prune_non_relay_paths(&mut paths); + + // All failed holepunch paths should be pruned + assert_eq!(20, paths.len()); + for i in 0..20 { + assert!(paths.contains_key(&ip_addr(i))); + } + for i in 20..35 { + assert!(!paths.contains_key(&ip_addr(i))); + } + } + + #[test] + fn test_prune_keeps_most_recent_inactive() { + let mut paths = FxHashMap::default(); + let now = Instant::now(); + + // Add 15 active paths + for i in 0..15 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 20 inactive paths with different abandon times + // Ports 15-34, with port 34 being most recently abandoned + for i in 0..20 { + let abandoned_time = now - Duration::from_secs((20 - i) as u64); + paths.insert(ip_addr(15 + i as u16), path_state_inactive(abandoned_time)); + } + + assert_eq!(35, paths.len()); + prune_non_relay_paths(&mut paths); + + // Should keep 15 active + 10 most recently abandoned + assert_eq!(25, paths.len()); + + // Active paths should remain + for i in 0..15 { + assert!(paths.contains_key(&ip_addr(i))); + } + + // Most recently abandoned (ports 25-34) should remain + for i in 25..35 { + assert!(paths.contains_key(&ip_addr(i)), "port {} should be kept", i); + } + + // Oldest abandoned (ports 15-24) should be pruned + for i in 15..25 { + assert!( + !paths.contains_key(&ip_addr(i)), + "port {} should be pruned", + i + ); + } + } + + #[test] + fn test_prune_mixed_must_and_can_prune() { + let mut paths = FxHashMap::default(); + let now = Instant::now(); + + // Add 15 active paths + for i in 0..15 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 5 failed holepunch paths + for i in 15..20 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + // Add 15 usable but abandoned paths + for i in 0..15 { + let abandoned_time = now - Duration::from_secs((15 - i) as u64); + paths.insert(ip_addr(20 + i as u16), path_state_inactive(abandoned_time)); + } + + assert_eq!(35, paths.len()); + prune_non_relay_paths(&mut paths); + + // Remove all failed paths -> down to 30 + // Keep MAX_INACTIVE_NON_RELAY_PATHS, eg remove 5 usable but abandoned paths -> down to 20 + assert_eq!(20, paths.len()); + + // Active paths should remain + for i in 0..15 { + assert!(paths.contains_key(&ip_addr(i))); + } + + // Failed holepunch should be pruned + for i in 15..20 { + assert!(!paths.contains_key(&ip_addr(i))); + } + + // Most recently abandoned (ports 30-34) should remain + for i in 30..35 { + assert!(paths.contains_key(&ip_addr(i)), "port {} should be kept", i); + } + } + + #[test] + fn test_prune_relay_paths_not_counted() { + let mut paths = FxHashMap::default(); + + // Add 25 IP paths (under MAX_NON_RELAY_PATHS) + for i in 0..25 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let relay_url: RelayUrl = url::Url::parse("https://localhost") + .expect("should be valid url") + .into(); + // Add 10 relay addresses + for _ in 0..10 { + let id = SecretKey::from_bytes(&rng.random()).public(); + let relay_addr = transports::Addr::Relay(relay_url.clone(), id); + paths.insert(relay_addr, PathState::default()); + } + + assert_eq!(35, paths.len()); // 25 IP + 10 relay + prune_non_relay_paths(&mut paths); + + // Should not prune since non-relay paths < MAX_NON_RELAY_PATHS + assert_eq!(35, paths.len()); + } + + #[test] + fn test_prune_preserves_never_dialed() { + let mut paths = FxHashMap::default(); + + // Add 20 never-dialed paths (PathStatus::Unknown) + for i in 0..20 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 15 failed paths to trigger pruning + for i in 20..35 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + prune_non_relay_paths(&mut paths); + + // Never-dialed paths should be preserved + for i in 0..20 { + assert!(paths.contains_key(&ip_addr(i))); + } + } + + #[test] + fn test_prune_all_paths_failed() { + let mut paths = FxHashMap::default(); + + // Add 40 failed holepunch paths (all paths have failed) + for i in 0..40 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + assert_eq!(40, paths.len()); + prune_non_relay_paths(&mut paths); + + // Should keep MAX_NON_RELAY_PATHS instead of pruning everything + // This prevents catastrophic loss of all path information + assert_eq!( + MAX_NON_RELAY_PATHS, + paths.len(), + "should keep MAX_NON_RELAY_PATHS when all paths failed" + ); + } + + #[test] + fn test_insert_open_path() { + let mut state = RemotePathState::new(Default::default()); + let addr = ip_addr(1000); + let source = Source::Connection; + + assert!(state.is_empty()); + + state.insert_open_path(addr.clone(), source.clone()); + + assert!(!state.is_empty()); + assert!(state.paths.contains_key(&addr)); + let path = &state.paths[&addr]; + assert!(matches!(path.status, PathStatus::Open)); + assert_eq!(path.sources.len(), 1); + assert!(path.sources.contains_key(&source)); + } + + #[test] + fn test_abandoned_path() { + let metrics = Arc::new(SocketMetrics::default()); + let mut state = RemotePathState::new(metrics.clone()); + + // Test: Open goes to Inactive + let addr_open = ip_addr(1000); + state.insert_open_path(addr_open.clone(), Source::Connection); + assert!(matches!(state.paths[&addr_open].status, PathStatus::Open)); + assert_eq!(metrics.transport_ip_paths_added.get(), 1); + + state.abandoned_path(&addr_open); + assert!(matches!( + state.paths[&addr_open].status, + PathStatus::Inactive(_) + )); + assert_eq!(metrics.transport_ip_paths_added.get(), 1); + assert_eq!(metrics.transport_ip_paths_removed.get(), 1); + + // Test: Inactive stays Inactive + state.abandoned_path(&addr_open); + assert!(matches!( + state.paths[&addr_open].status, + PathStatus::Inactive(_) + )); + assert_eq!(metrics.transport_ip_paths_added.get(), 1); + assert_eq!(metrics.transport_ip_paths_removed.get(), 1); + + // Test: Unknown goes to Unusable + let addr_unknown = ip_addr(2000); + state.insert_multiple([addr_unknown.clone()].into_iter(), Source::Connection); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Unknown + )); + assert_eq!(metrics.transport_ip_paths_added.get(), 1); + assert_eq!(metrics.transport_ip_paths_removed.get(), 1); + + state.abandoned_path(&addr_unknown); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Unusable + )); + assert_eq!(metrics.transport_ip_paths_added.get(), 1); + assert_eq!(metrics.transport_ip_paths_removed.get(), 1); + + // Test: Unusable stays Unusable + state.abandoned_path(&addr_unknown); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Unusable + )); + assert_eq!(metrics.transport_ip_paths_added.get(), 1); + assert_eq!(metrics.transport_ip_paths_removed.get(), 1); + + // Test: Unusable can go to open + state.insert_open_path(addr_unknown.clone(), Source::Connection); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Open + )); + assert_eq!(metrics.transport_ip_paths_added.get(), 2); + assert_eq!(metrics.transport_ip_paths_removed.get(), 1); + } + + /// An empty `insert_multiple` must not drain pending resolve requests. + /// + /// This reproduces the race where multiple concurrent `connect_with_opts` + /// calls send `ResolveRemote` messages with empty addrs. The first pushes + /// a tx, then the second's `insert_multiple([])` used to drain that tx + /// with `NoResults { errors: [] }`, even though an address lookup was + /// still in flight and would shortly have resolved it. + #[test] + fn empty_insert_does_not_drain_pending() { + let metrics = Arc::new(SocketMetrics::default()); + let mut state = RemotePathState::new(metrics); + + let (tx, mut rx) = oneshot::channel(); + state.resolve_remote(tx); + + // Second concurrent resolve arrives with empty addrs (no app-provided + // addresses) while address lookup is still running. + state.insert_multiple(std::iter::empty(), Source::App); + + assert!( + rx.try_recv().is_err(), + "pending tx must stay pending while paths are empty and lookup is in flight" + ); + + // When real addresses arrive, the tx resolves Ok. + state.insert_multiple([ip_addr(4242)].into_iter(), Source::App); + let resolved = rx.try_recv().expect("tx should have been woken"); + assert!(resolved.is_ok(), "expected Ok once a path was added"); + } + + /// `address_lookup_finished(Ok(()))` drains pending requests with `NoResults` when no paths are known. + /// + /// This is the "lookup done but nothing was found" signal and it must + /// still reach callers. + #[test] + fn address_lookup_finished_empty_emits_no_results() { + let metrics = Arc::new(SocketMetrics::default()); + let mut state = RemotePathState::new(metrics); + + let (tx, mut rx) = oneshot::channel(); + state.resolve_remote(tx); + + state.address_lookup_finished(Ok(())); + + let resolved = rx.try_recv().expect("tx should have been woken"); + assert!(matches!( + resolved, + Err(AddressLookupFailed::NoResults { .. }) + )); + } +} diff --git a/vendor/iroh/src/socket/remote_map/remote_state/path_watcher.rs b/vendor/iroh/src/socket/remote_map/remote_state/path_watcher.rs new file mode 100644 index 0000000..0dc321f --- /dev/null +++ b/vendor/iroh/src/socket/remote_map/remote_state/path_watcher.rs @@ -0,0 +1,556 @@ +//! Path observation for a [`Connection`]. +//! +//! [`Connection::paths`] returns a borrowed [`PathList`] with live +//! statistics. [`Connection::path_events`] returns a `'static` stream +//! of [`PathEvent`]s. Subscribing to the event stream before reading +//! the snapshot ensures any change that happens between the read and +//! the next poll is observed by the subscriber. +//! +//! Closed paths are not retained in [`PathList`]; their final +//! statistics arrive inline on [`PathEvent::Closed`]. +//! +//! # Internal structure +//! +//! [`PathStateSender`] (owned by the [`RemoteStateActor`]) and +//! [`PathStateReceiver`] (held by the [`Connection`]) share a +//! [`Mutex`]`<`[`State`]`>`, a [`Notify`], and a [`broadcast`] channel. +//! The receiver holds a [`WeakSender`]; when the actor drops the +//! sender, outstanding event streams end. +//! +//! [`Connection`]: crate::endpoint::Connection +//! [`Connection::paths`]: crate::endpoint::Connection::paths +//! [`Connection::path_events`]: crate::endpoint::Connection::path_events +//! [`RemoteStateActor`]: super::RemoteStateActor +//! [`WeakSender`]: broadcast::WeakSender + +use std::{ + pin::Pin, + sync::{Arc, Mutex}, + task::{Context, Poll}, +}; + +use iroh_base::TransportAddr; +use n0_future::{StreamExt, time::Duration}; +use noq::WeakPathHandle; +use noq_proto::PathId; +use smallvec::SmallVec; +use tokio::sync::{Notify, broadcast, futures::Notified}; +use tokio_stream::{ + Stream, + wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}, +}; +use tracing::warn; + +use crate::{ + endpoint::PathStats, + socket::transports::{self, LocalTransportAddr}, +}; + +/// Per-connection broadcast channel capacity for path events. +const BROADCAST_CAPACITY: usize = 8; + +/// Lifecycle notifications for a transmission paths in a connection. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum PathEvent { + /// A new network path was opened. + #[non_exhaustive] + Opened { + /// Path identifier. + id: PathId, + /// Remote transport address. + remote_addr: TransportAddr, + /// Local address of the path, if known. + local_addr: LocalTransportAddr, + }, + /// A network path was closed. + #[non_exhaustive] + Closed { + /// Path identifier. + id: PathId, + /// Remote transport address. + remote_addr: TransportAddr, + /// Local address of the path, if known. + local_addr: LocalTransportAddr, + /// Path statistics captured at close time. + last_stats: Box, + }, + /// This path was selected for transmission of application data. + #[non_exhaustive] + Selected { + /// Path identifier of the newly selected path. + id: PathId, + /// Remote transport address of the newly selected path. + remote_addr: TransportAddr, + /// The local address of the newly selected path, if known. + local_addr: LocalTransportAddr, + }, + /// Events were dropped before the subscriber received them. + /// + /// Yielded when the subscriber does not poll the stream fast + /// enough to keep up with the writer. The current set of open + /// paths and the selected path remain accessible via + /// [`Connection::paths`]. + /// + /// [`Connection::paths`]: crate::endpoint::Connection::paths + #[non_exhaustive] + Lagged { + /// Number of events dropped since the last delivered event. + missed: u64, + }, +} + +#[derive(Clone, derive_more::Debug)] +#[debug("PathData({}, {})", self.handle.id(), self.remote_addr)] +struct PathData { + handle: WeakPathHandle, + remote_addr: TransportAddr, + local_addr: LocalTransportAddr, +} + +impl PathData { + /// Returns a strong [`noq::Path`]. + /// + /// # Panics + /// + /// This may panic if the passed `noq::Connection` is not the one to which this path belongs. + fn upgrade(&self, _conn: &noq::Connection) -> noq::Path { + self.handle + .upgrade() + .expect("Wrong Connection reference passed to PathData::upgrade") + } +} + +#[derive(Default, Debug, Clone)] +struct State { + list: SmallVec<[PathData; 4]>, + selected: Option, + closed: bool, +} + +#[derive(Debug)] +struct Shared { + state: Mutex, + notify: Notify, +} + +/// The writer-side handle for a connection's path state. +/// +/// Owned by the [`RemoteStateActor`]; the only handle that mutates +/// state and emits events. When dropped, every outstanding +/// [`PathEventStream`] ends. +/// +/// [`RemoteStateActor`]: super::RemoteStateActor +#[derive(Debug)] +pub(super) struct PathStateSender { + shared: Arc, + events: broadcast::Sender, +} + +impl PathStateSender { + /// Creates a sender/receiver pair sharing empty state. + /// + /// Gets passed a clone of the custom address map, so that we can convert local addresses + /// to the public [`LocalTransportAddr`] exposed to users. + pub(super) fn new() -> (Self, PathStateReceiver) { + let (events, _) = broadcast::channel(BROADCAST_CAPACITY); + let shared = Arc::new(Shared { + state: Default::default(), + notify: Notify::new(), + }); + let receiver = PathStateReceiver { + shared: shared.clone(), + events: events.downgrade(), + }; + let sender = PathStateSender { shared, events }; + (sender, receiver) + } + + /// Records a newly-opened path and emits [`PathEvent::Opened`]. + pub(super) fn record_opened( + &self, + handle: WeakPathHandle, + network_path: transports::FourTuple, + ) { + let id = handle.id(); + let remote_addr: TransportAddr = network_path.remote().into(); + let local_addr = network_path.local(); + { + let mut state = self.shared.state.lock().expect("poisoned"); + let entry = PathData { + handle, + remote_addr: remote_addr.clone(), + local_addr: local_addr.clone(), + }; + match state.list.iter().position(|e| e.handle.id() == id) { + Some(idx) => state.list[idx] = entry, + None => state.list.push(entry), + } + } + self.shared.notify.notify_waiters(); + let _ = self.events.send(PathEvent::Opened { + id, + remote_addr, + local_addr, + }); + } + + /// Records that a path was abandoned by `noq`. + pub(super) fn record_abandoned(&self, id: PathId, conn: &noq::Connection) { + let removed = { + let mut state = self.shared.state.lock().expect("poisoned"); + if state.selected == Some(id) { + state.selected = None; + } + state + .list + .iter() + .position(|e| e.handle.id() == id) + .map(|pos| state.list.remove(pos)) + }; + if let Some(data) = removed { + let stats = data.upgrade(conn).stats(); + self.shared.notify.notify_waiters(); + let _ = self.events.send(PathEvent::Closed { + id, + remote_addr: data.remote_addr.clone(), + local_addr: data.local_addr.clone(), + last_stats: Box::new(stats), + }); + } + } + + /// Updates the selected transmission path. + pub(super) fn record_selected(&self, network_path: &transports::FourTuple) { + let remote_addr: TransportAddr = network_path.remote().into(); + let local_addr = network_path.local(); + let event = { + let mut state = self.shared.state.lock().expect("poisoned"); + let selected_path_id = state + .list + .iter() + .find(|p| p.remote_addr == remote_addr && p.local_addr == local_addr) + .map(|p| p.handle.id()); + if selected_path_id != state.selected { + state.selected = selected_path_id; + selected_path_id.map(|path_id| PathEvent::Selected { + id: path_id, + remote_addr: remote_addr.clone(), + local_addr: local_addr.clone(), + }) + } else { + None + } + }; + if let Some(event) = event { + let _ = self.events.send(event); + self.shared.notify.notify_waiters(); + } + } + + /// Closes the writer side of the path observation. + /// + /// Emits a final [`PathEvent::Closed`] for every remaining open + /// path with its statistics taken from `closed.path_stats`, marks + /// the state closed, and drops the sender. No-op if already closed. + /// + /// [`WeakPathHandle`]: noq::WeakPathHandle + pub(super) fn close(self, closed: noq::Closed) { + let mut state = self.shared.state.lock().expect("poisoned"); + if !state.closed { + for path in state.list.iter() { + if let Some(stats) = closed + .path_stats + .iter() + .find(|(id, _stats)| *id == path.handle.id()) + .map(|(_id, stats)| stats) + { + let _ = self.events.send(PathEvent::Closed { + id: path.handle.id(), + remote_addr: path.remote_addr.clone(), + local_addr: path.local_addr.clone(), + last_stats: Box::new(*stats), + }); + } else { + warn!( + "Connection close event is missing path stats for path {}", + path.handle.id() + ); + } + } + state.closed = true; + self.shared.notify.notify_waiters(); + } + } +} + +impl Drop for PathStateSender { + fn drop(&mut self) { + let mut state = self.shared.state.lock().expect("poisoned"); + if !state.closed { + state.closed = true; + self.shared.notify.notify_waiters(); + } + } +} + +/// The reader-side handle for a connection's path state. +/// +/// Held by a [`Connection`]. Cheap to clone. +/// +/// [`Connection`]: crate::endpoint::Connection +#[derive(Clone, Debug)] +pub(crate) struct PathStateReceiver { + shared: Arc, + events: broadcast::WeakSender, +} + +impl PathStateReceiver { + /// Returns a snapshot of the currently-open paths, tied to `conn`. + pub(crate) fn get<'a>(&self, conn: &'a noq::Connection) -> PathList<'a> { + PathList { + snapshot: self.shared.state.lock().expect("poisoned").clone(), + conn, + } + } + + /// Returns a stream of [`PathEvent`]s. + /// + /// Already closed if the sender has been dropped. + pub(crate) fn events(&self) -> PathEventStream { + let receiver = if let Some(sender) = self.events.upgrade() { + sender.subscribe() + } else { + let (_tx, rx) = broadcast::channel(1); + rx + }; + PathEventStream { + inner: BroadcastStream::new(receiver), + } + } + + /// Returns a stream of [`PathList`] snapshots tied to `conn`. + /// + /// Yields the current snapshot on the first poll, then a fresh + /// snapshot on every state change. Ends when the state is marked + /// closed. + pub(crate) fn stream<'a>(&'a self, conn: &'a noq::Connection) -> PathListStream<'a> { + PathListStream { + shared: &self.shared, + conn, + notified: Box::pin(self.shared.notify.notified()), + first_poll: true, + } + } +} + +/// A borrowed snapshot of a connection's currently-open paths. +/// +/// Returned by [`Connection::paths`]. The list is captured at call +/// time and does not reflect later changes. Closed paths are not +/// retained; to track per-path totals over the connection's lifetime, +/// accumulate from [`PathEvent::Closed`]. +/// +/// [`Connection::paths`]: crate::endpoint::Connection::paths +#[derive(Clone, derive_more::Debug)] +pub struct PathList<'conn> { + snapshot: State, + #[debug(skip)] + conn: &'conn noq::Connection, +} + +impl<'conn> PathList<'conn> { + /// Returns the number of open paths. + pub fn len(&self) -> usize { + self.snapshot.list.len() + } + + /// Returns `true` if no paths are open. + pub fn is_empty(&self) -> bool { + self.snapshot.list.is_empty() + } + + /// Returns an iterator over the open paths. + pub fn iter(&self) -> PathListIter<'_> { + PathListIter { + inner: self.snapshot.list.iter(), + selected: self.snapshot.selected, + conn: self.conn, + } + } + + /// Returns the path with the given [`PathId`]. + /// + /// Returns `None` if no open path with that id is present in + /// this snapshot. + pub fn get(&self, id: PathId) -> Option> { + self.iter().find(|p| p.id() == id) + } +} + +impl<'a> IntoIterator for &'a PathList<'a> { + type IntoIter = PathListIter<'a>; + type Item = Path<'a>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// An iterator over the open paths in a [`PathList`] snapshot. +#[derive(Debug)] +pub struct PathListIter<'a> { + inner: std::slice::Iter<'a, PathData>, + selected: Option, + conn: &'a noq::Connection, +} + +impl<'a> PathListIter<'a> { + fn item(&self, data: &'a PathData) -> Path<'a> { + Path { + data, + is_selected: self.selected == Some(data.handle.id()), + conn: self.conn, + } + } +} + +impl<'a> Iterator for PathListIter<'a> { + type Item = Path<'a>; + + fn next(&mut self) -> Option { + self.inner.next().map(|d| self.item(d)) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl<'a> DoubleEndedIterator for PathListIter<'a> { + fn next_back(&mut self) -> Option { + self.inner.next_back().map(|d| self.item(d)) + } +} + +impl ExactSizeIterator for PathListIter<'_> {} + +/// A single path within a [`PathList`] snapshot. +/// +/// Borrows from the enclosing [`PathList`] and from the [`Connection`] +/// that produced it, so a [`Path`] cannot cross a task boundary. If you need +/// to send path data to other tasks, you can clone [`Self::remote_addr`] or +/// [`Self::stats`] into an owned value first. +/// +/// [`Connection`]: crate::endpoint::Connection +#[derive(Clone, Debug)] +pub struct Path<'a> { + data: &'a PathData, + is_selected: bool, + /// Reference to a `noq::Connection` for safe upgrading via [`PathData::upgrade`] + conn: &'a noq::Connection, +} + +impl<'conn> Path<'conn> { + /// Returns the path's [`PathId`]. + pub fn id(&self) -> PathId { + self.data.handle.id() + } + + /// Returns the path's remote transport address. + pub fn remote_addr(&self) -> &TransportAddr { + &self.data.remote_addr + } + + /// Returns the path's local address, if known. + pub fn local_addr(&self) -> &LocalTransportAddr { + &self.data.local_addr + } + + /// Returns `true` if this path is currently selected for application data transmission. + pub fn is_selected(&self) -> bool { + self.is_selected + } + + /// Returns `true` if this is a direct IP path. + pub fn is_ip(&self) -> bool { + self.data.remote_addr.is_ip() + } + + /// Returns `true` if this is a relay path. + pub fn is_relay(&self) -> bool { + self.data.remote_addr.is_relay() + } + + /// Returns the path's statistics. + /// + /// Returns live statistics from the QUIC state for an open path, or + /// the final statistics retained by `noq` for a path that closed + /// after this snapshot was taken. + pub fn stats(&self) -> PathStats { + self.data.upgrade(self.conn).stats() + } + + /// Returns the path's round-trip time estimate. + pub fn rtt(&self) -> Duration { + self.stats().rtt + } +} + +/// A stream of [`PathList`] snapshots for a connection. +/// +/// Returned by [`Connection::paths_stream`]. Yields the current +/// snapshot on the first poll and a fresh snapshot whenever the open +/// paths or the selected path change. Ends when the connection closes. +/// +/// [`Connection::paths_stream`]: crate::endpoint::Connection::paths_stream +#[derive(Debug)] +pub struct PathListStream<'conn> { + shared: &'conn Shared, + conn: &'conn noq::Connection, + notified: Pin>>, + first_poll: bool, +} + +impl<'conn> Stream for PathListStream<'conn> { + type Item = PathList<'conn>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + if this.first_poll { + this.first_poll = false; + } else { + std::task::ready!(this.notified.as_mut().poll(cx)); + this.notified.set(this.shared.notify.notified()); + } + this.notified.as_mut().enable(); + let snapshot = this.shared.state.lock().expect("poisoned").clone(); + if snapshot.closed { + Poll::Ready(None) + } else { + Poll::Ready(Some(PathList { + snapshot, + conn: this.conn, + })) + } + } +} + +/// A `'static` stream of [`PathEvent`]s. +/// +/// Returned by [`Connection::path_events`]. +/// +/// [`Connection::path_events`]: crate::endpoint::Connection::path_events +#[derive(Debug)] +pub struct PathEventStream { + inner: BroadcastStream, +} + +impl Stream for PathEventStream { + type Item = PathEvent; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_next(cx).map(|event| match event? { + Ok(event) => Some(event), + Err(BroadcastStreamRecvError::Lagged(missed)) => Some(PathEvent::Lagged { missed }), + }) + } +} diff --git a/vendor/iroh/src/socket/remote_map/remote_state/remote_info.rs b/vendor/iroh/src/socket/remote_map/remote_state/remote_info.rs new file mode 100644 index 0000000..48713fa --- /dev/null +++ b/vendor/iroh/src/socket/remote_map/remote_state/remote_info.rs @@ -0,0 +1,94 @@ +use iroh_base::{EndpointId, TransportAddr}; + +/// Information about a remote endpoint. +/// +/// This information is a snapshot in time, i.e. it is not updating and may +/// already be outdated by the time you are reading this. Updated information +/// can only be retrieved by calling [`Endpoint::remote_info`] again. +/// +/// [`Endpoint::remote_info`]: crate::Endpoint::remote_info +#[derive(Debug, Clone)] +pub struct RemoteInfo { + pub(super) endpoint_id: EndpointId, + pub(super) addrs: Vec, +} + +impl RemoteInfo { + /// Returns the remote's endpoint id. + pub fn id(&self) -> EndpointId { + self.endpoint_id + } + + /// Returns an iterator over known all addresses for this remote. + /// + /// Note that this may include outdated or unusable addresses. + pub fn addrs(&self) -> impl Iterator { + self.addrs.iter() + } + + /// Converts into an iterator over known all addresses for this remote. + /// + /// Note that this may include outdated or unusable addresses. You can use [`TransportAddrInfo::usage`] + /// to filter for addresses that are actively used. + /// + /// You can use this to construct an [`EndpointAddr`] for this remote: + /// + /// ```no_run + /// # #[cfg(with_crypto_provider)] + /// # { + /// # use iroh::{Endpoint, EndpointId, EndpointAddr, endpoint::presets}; + /// # #[tokio::main] + /// # async fn main() { + /// # let endpoint = Endpoint::bind(presets::N0).await.unwrap(); + /// # let remote_id = EndpointId::from_bytes(&[0u8; 32]).unwrap(); + /// let info = endpoint.remote_info(remote_id).await.unwrap(); + /// let addr = EndpointAddr::from_parts(info.id(), info.into_addrs().map(|addr| addr.into_addr())); + /// # } + /// # } + /// ``` + /// + /// [`EndpointAddr`]: crate::EndpointAddr + pub fn into_addrs(self) -> impl Iterator { + self.addrs.into_iter() + } +} + +/// Address of a remote with some metadata +#[derive(Debug, Clone)] +pub struct TransportAddrInfo { + pub(super) addr: TransportAddr, + pub(super) usage: TransportAddrUsage, +} + +impl TransportAddrInfo { + /// Returns the [`TransportAddr`]. + pub fn addr(&self) -> &TransportAddr { + &self.addr + } + + /// Converts into [`TransportAddr`]. + pub fn into_addr(self) -> TransportAddr { + self.addr + } + + /// Returns information how this address is used. + pub fn usage(&self) -> TransportAddrUsage { + self.usage + } +} + +impl From for TransportAddr { + fn from(value: TransportAddrInfo) -> Self { + value.addr + } +} + +/// Information how a transport address is used. +#[derive(Debug, Copy, Clone)] +#[non_exhaustive] +pub enum TransportAddrUsage { + /// The address is in active use. + Active, + /// The address is not currently used. + Inactive, +} diff --git a/vendor/iroh/src/socket/transports.rs b/vendor/iroh/src/socket/transports.rs new file mode 100644 index 0000000..4c72a57 --- /dev/null +++ b/vendor/iroh/src/socket/transports.rs @@ -0,0 +1,1486 @@ +use std::{ + fmt, + io::{self, IoSliceMut}, + net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, + num::NonZeroUsize, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use iroh_base::{CustomAddr, EndpointId, RelayUrl, TransportAddr}; +use iroh_relay::RelayMap; +use n0_watcher::Watcher; +use relay::{RelayNetworkChangeSender, RelaySender}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, instrument, trace, warn}; + +use super::{Socket, mapped_addrs::MultipathMappedAddr}; +use crate::{ + endpoint::RelayStatus, + metrics::EndpointMetrics, + net_report::Report, + socket::mapped_addrs::{AddrMap, CustomMappedAddr}, +}; + +pub(crate) mod custom; +#[cfg(not(wasm_browser))] +mod ip; +mod relay; + +use custom::{CustomEndpoint, CustomSender, CustomTransport}; + +#[cfg(not(wasm_browser))] +pub(crate) use self::ip::Config as IpConfig; +#[cfg(not(wasm_browser))] +use self::ip::{IpNetworkChangeSender, IpTransports, IpTransportsSender}; +pub(crate) use self::relay::{ + HomeRelayWatch, RelayActorConfig, RelayConnectionFailure, RelayConnectionState, RelayTransport, +}; + +/// How many times all transports may error on `poll_recv` before we give up. +/// +/// Once all transports errored for this many times in a row, we give up and forward +/// the error to noq, which will kill the endpoint driver then. +const MAX_CONSECUTIVE_RECV_ERRORS: usize = 8; + +/// Manages the different underlying data transports that the socket can support. +#[derive(Debug)] +pub(crate) struct Transports { + #[cfg(not(wasm_browser))] + ip: IpTransports, + relay: Vec, + custom: Vec>, + + poll_recv_counter: usize, + /// Cache for per-packet recv info, to speed up access + recv_infos: [RecvInfo; noq_udp::BATCH_SIZE], + consecutive_total_recv_failures: usize, +} + +/// Combined watcher type for all ip transports +type IpTransportsWatcher = n0_watcher::Join>; +/// Combined watcher type for all custom transports +type CustomTransportsWatcher = + n0_watcher::Join, n0_watcher::Direct>>; +/// Combined watcher type for all relay transports +type RelayTransportsWatcher = n0_watcher::Join< + Option<(RelayUrl, EndpointId)>, + n0_watcher::Map>, Option<(RelayUrl, EndpointId)>>, +>; + +pub(super) type HomeRelayWatcher = n0_watcher::Map< + n0_watcher::Join, n0_watcher::Direct>>, + Vec, +>; + +#[cfg(not(wasm_browser))] +/// Combined watcher type for all transports, custom, relay and ip +pub(crate) type LocalAddrsWatch = n0_watcher::Map< + n0_watcher::Tuple< + n0_watcher::Tuple, + RelayTransportsWatcher, + >, + Vec, +>; + +/// Type for watching relay and custom transports only, no ip +#[cfg(wasm_browser)] +pub(crate) type LocalAddrsWatch = + n0_watcher::Map, Vec>; + +/// Available transport configurations. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub(crate) enum TransportConfig { + /// IP based transport + #[cfg(not(wasm_browser))] + Ip { + /// The actual IP Config + config: ip::Config, + /// Was this added explicitly by the user. + is_user_defined: bool, + }, + /// Relay transport + Relay { + /// The [`RelayMap`] used for this relay. + relay_map: RelayMap, + /// Was this added explicitly by the user. + is_user_defined: bool, + }, + /// Custom transport factory. + #[cfg_attr(not(feature = "unstable-custom-transports"), allow(dead_code))] + Custom(Arc), +} + +impl TransportConfig { + /// Configures a default IPv4 transport, listening on `0.0.0.0:0`. + #[cfg(not(wasm_browser))] + pub(crate) fn default_ipv4() -> Self { + use std::net::Ipv4Addr; + + use ipnet::Ipv4Net; + + Self::Ip { + config: ip::Config::V4 { + ip_net: Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).expect("checked"), + port: 0, + is_required: true, + is_default: false, + }, + is_user_defined: false, + } + } + + /// Configures a default IPv6 transport, listening on `[::]:0`. + #[cfg(not(wasm_browser))] + pub(crate) fn default_ipv6() -> Self { + use ipnet::Ipv6Net; + + Self::Ip { + config: ip::Config::V6 { + ip_net: Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).expect("checked"), + scope_id: 0, + port: 0, + is_required: false, + is_default: false, + }, + is_user_defined: false, + } + } + + /// Is this a default IPv4 configuration + #[cfg(not(wasm_browser))] + pub(crate) fn is_ipv4_default(&self) -> bool { + match self { + Self::Ip { config, .. } => config.is_default() && config.is_ipv4(), + _ => false, + } + } + + /// Is this a default IPv6 configuration + #[cfg(not(wasm_browser))] + pub(crate) fn is_ipv6_default(&self) -> bool { + match self { + Self::Ip { config, .. } => config.is_default() && config.is_ipv6(), + _ => false, + } + } + + /// Is this configuration set by the user. + pub(crate) fn is_user_defined(&self) -> bool { + match self { + #[cfg(not(wasm_browser))] + Self::Ip { + is_user_defined, .. + } => *is_user_defined, + Self::Relay { + is_user_defined, .. + } => *is_user_defined, + Self::Custom(_) => true, + } + } +} + +impl Transports { + /// Binds the transports. + pub(crate) fn bind( + configs: &[TransportConfig], + relay_actor_config: RelayActorConfig, + metrics: &EndpointMetrics, + shutdown_token: CancellationToken, + ) -> io::Result { + #[cfg(not(wasm_browser))] + let ip_configs = { + let mut ip_configs = Vec::new(); + + // user defined overrides defaults + let has_ipv4_default = configs + .iter() + .any(|t| t.is_ipv4_default() && t.is_user_defined()); + let has_ipv6_default = configs + .iter() + .any(|t| t.is_ipv6_default() && t.is_user_defined()); + for config in configs { + if let TransportConfig::Ip { + config, + is_user_defined, + } = config + { + if !is_user_defined + && (config.is_ipv4() && has_ipv4_default + || config.is_ipv6() && has_ipv6_default) + { + continue; + } + ip_configs.push(*config); + } + } + ip_configs + }; + #[cfg(not(wasm_browser))] + let ip = IpTransports::bind(ip_configs.into_iter(), metrics)?; + + let relay = configs + .iter() + .filter(|t| matches!(t, TransportConfig::Relay { .. })) + .map(|_c| RelayTransport::new(relay_actor_config.clone(), shutdown_token.child_token())) + .collect(); + + let mut custom = Vec::new(); + for config in configs.iter().filter_map(|t| { + if let TransportConfig::Custom(config) = t { + Some(config) + } else { + None + } + }) { + let transport = config.bind()?; + custom.push(transport); + } + + Ok(Self { + #[cfg(not(wasm_browser))] + ip, + relay, + custom, + poll_recv_counter: Default::default(), + recv_infos: Default::default(), + consecutive_total_recv_failures: 0, + }) + } + + pub(crate) fn poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + sock: &Socket, + ) -> Poll> { + assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + assert!(bufs.len() <= noq_udp::BATCH_SIZE, "too many buffers"); + if sock.is_closed() { + return Poll::Pending; + } + + match self.inner_poll_recv(cx, bufs, metas)? { + Poll::Pending => Poll::Pending, + Poll::Ready(0) => Poll::Ready(Ok(0)), + Poll::Ready(n) => { + sock.process_datagrams(&mut bufs[..n], &mut metas[..n], &self.recv_infos[..n]); + Poll::Ready(Ok(n)) + } + } + } + + /// Tries to recv data, on all available transports. + fn inner_poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + ) -> Poll> { + assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + + let mut total_polled = 0; + let mut total_errors = 0; + let mut return_ready = false; + + macro_rules! poll_transport { + ($transport:expr) => { + total_polled += 1; + match $transport.poll_recv(cx, bufs, metas, &mut self.recv_infos) { + Poll::Pending => {} + Poll::Ready(Ok(0)) => { + return_ready = true; + } + Poll::Ready(Ok(n)) => { + // Once a transport has data ready, we return directly. + self.consecutive_total_recv_failures = 0; + return Poll::Ready(Ok(n)); + } + Poll::Ready(Err(err)) => { + // We don't set `has_poll_ready` to `true` here, because if we did, + // a single always-failing transport would put us into a hot loop + // where `poll_recv` would be called right away again and again even if + // the non-failing transports are all pending. + total_errors += 1; + debug!(transport = %$transport, "recv error: {err:#}"); + } + } + }; + } + + // To improve fairness, every other call reverses the ordering of polling. + self.poll_recv_counter = self.poll_recv_counter.wrapping_add(1); + let counter = self.poll_recv_counter; + if counter.is_multiple_of(2) { + #[cfg(not(wasm_browser))] + for transport in self.ip.iter_mut() { + poll_transport!(transport); + } + for transport in self.relay.iter_mut() { + poll_transport!(transport); + } + for transport in self.custom.iter_mut() { + poll_transport!(transport); + } + } else { + for transport in self.custom.iter_mut().rev() { + poll_transport!(transport); + } + for transport in self.relay.iter_mut().rev() { + poll_transport!(transport); + } + #[cfg(not(wasm_browser))] + for transport in self.ip.iter_mut() { + poll_transport!(transport); + } + } + + if total_polled == total_errors { + // All transports errored. + self.consecutive_total_recv_failures += 1; + debug!( + "All transports failed to receive ({} remaining)", + MAX_CONSECUTIVE_RECV_ERRORS.wrapping_sub(self.consecutive_total_recv_failures) + ); + if self.consecutive_total_recv_failures >= MAX_CONSECUTIVE_RECV_ERRORS { + warn!("All transports failed to receive. QUIC endpoint will be shutdown."); + Poll::Ready(Err(io::Error::new( + io::ErrorKind::NetworkDown, + "All transports failed to receive", + ))) + } else { + Poll::Ready(Ok(0)) + } + } else { + // At least one transport is pending or returned Ok(0). + self.consecutive_total_recv_failures = 0; + if return_ready { + Poll::Ready(Ok(0)) + } else { + Poll::Pending + } + } + } + + /// Returns a list of all currently known local addresses. + /// + /// For IP based transports this is the [`SocketAddr`] of the socket, + /// for relay transports, this is the home relay. + pub(crate) fn local_addrs(&self) -> Vec { + self.local_addrs_watch().get() + } + + pub(super) fn home_relay_watch(&self) -> HomeRelayWatcher { + n0_watcher::Join::new(self.relay.iter().map(|t| t.my_relay_status())) + .map(|v| v.into_iter().flatten().collect()) + } + + #[cfg(not(wasm_browser))] + /// Watch for all currently known local addresses, including IP based transports. + pub(crate) fn local_addrs_watch(&self) -> LocalAddrsWatch { + let ips = n0_watcher::Join::new(self.ip.iter().map(|t| t.local_addr_watch())); + let relays = n0_watcher::Join::new(self.relay.iter().map(|t| t.local_addr_watch())); + let custom = n0_watcher::Join::new(self.custom.iter().map(|t| t.watch_local_addrs())); + + ips.or(custom).or(relays).map(|((ips, custom), relays)| { + let ips = ips.into_iter().map(Addr::from); + let custom = custom.into_iter().flatten().map(Addr::from); + let relays = relays + .into_iter() + .flatten() + .map(|(relay_url, endpoint_id)| Addr::Relay(relay_url, endpoint_id)); + ips.chain(custom).chain(relays).collect() + }) + } + + #[cfg(wasm_browser)] + /// Watch for all currently known local addresses, excluding IP based transports. + pub(crate) fn local_addrs_watch(&self) -> LocalAddrsWatch { + let relays = n0_watcher::Join::new(self.relay.iter().map(|t| t.local_addr_watch())); + let custom = n0_watcher::Join::new(self.custom.iter().map(|t| t.watch_local_addrs())); + custom.or(relays).map(|(custom, relays)| { + let custom = custom.into_iter().flatten().map(Addr::from); + let relays = relays + .into_iter() + .flatten() + .map(|(relay_url, endpoint_id)| Addr::Relay(relay_url, endpoint_id)); + custom.chain(relays).collect() + }) + } + + /// Returns the bound addresses for IP based transports + #[cfg(not(wasm_browser))] + pub(crate) fn ip_bind_addrs(&self) -> Vec { + self.ip.iter().map(|t| t.bind_addr()).collect() + } + + #[cfg(not(wasm_browser))] + pub(crate) fn max_transmit_segments(&self) -> NonZeroUsize { + let ip = self.ip.iter().map(|t| t.max_transmit_segments()); + let custom = self.custom.iter().map(|t| t.max_transmit_segments()); + ip.chain(custom).min().unwrap_or(NonZeroUsize::MIN) + } + + #[cfg(wasm_browser)] + pub(crate) fn max_transmit_segments(&self) -> NonZeroUsize { + self.custom + .iter() + .map(|t| t.max_transmit_segments()) + .min() + .unwrap_or(NonZeroUsize::MIN) + } + + #[cfg(not(wasm_browser))] + pub(crate) fn max_receive_segments(&self) -> NonZeroUsize { + // `max_receive_segments` controls the size of the `RecvMeta` buffer + // that noq creates. Having buffers slightly bigger than necessary + // isn't terrible, and makes sure a single socket can read the maximum + // amount with a single poll. We considered adding these numbers instead, + // but we never get data from both sockets at the same time in `poll_recv` + // and it's impossible and unnecessary to be refactored that way. + + let res = self.ip.iter().map(|t| t.max_receive_segments()).max(); + res.unwrap_or(NonZeroUsize::MIN) + } + + #[cfg(wasm_browser)] + pub(crate) fn max_receive_segments(&self) -> NonZeroUsize { + NonZeroUsize::MIN + } + + #[cfg(not(wasm_browser))] + pub(crate) fn may_fragment(&self) -> bool { + self.ip.iter().any(|t| t.may_fragment()) + } + + #[cfg(wasm_browser)] + pub(crate) fn may_fragment(&self) -> bool { + false + } + + pub(crate) fn create_sender(&self) -> TransportsSender { + #[cfg(not(wasm_browser))] + let ip = self.ip.create_sender(); + + let relay = self.relay.iter().map(|t| t.create_sender()).collect(); + let custom = self.custom.iter().map(|t| t.create_sender()).collect(); + let max_transmit_segments = self.max_transmit_segments(); + + TransportsSender { + #[cfg(not(wasm_browser))] + ip, + relay, + custom, + max_transmit_segments, + } + } + + /// Handles potential changes to the underlying network conditions. + pub(crate) fn create_network_change_sender(&self) -> NetworkChangeSender { + NetworkChangeSender { + #[cfg(not(wasm_browser))] + ip: self + .ip + .iter() + .map(|t| t.create_network_change_sender()) + .collect(), + relay: self + .relay + .iter() + .map(|t| t.create_network_change_sender()) + .collect(), + } + } +} + +#[derive(Debug)] +pub(crate) struct NetworkChangeSender { + #[cfg(not(wasm_browser))] + ip: Vec, + relay: Vec, +} + +impl NetworkChangeSender { + pub(crate) fn on_network_change(&self, report: &Report) { + #[cfg(not(wasm_browser))] + for ip in &self.ip { + ip.on_network_change(report); + } + + for relay in &self.relay { + relay.on_network_change(report); + } + } + + /// Triggers an immediate relay connection health check after a network change. + /// + /// Uses RTT-based timeout for faster detection of broken connections. + pub(crate) fn check_relay_connection(&self) { + for relay in &self.relay { + relay.check_connection_after_network_change(); + } + } + + /// Rebinds underlying connections, if necessary. + pub(crate) fn rebind(&self) -> std::io::Result<()> { + let mut res = Ok(()); + + #[cfg(not(wasm_browser))] + for transport in &self.ip { + if let Err(err) = transport.rebind() { + warn!("failed to rebind {:?}", err); + res = Err(err); + } + } + + for transport in &self.relay { + if let Err(err) = transport.rebind() { + warn!("failed to rebind {:?}", err); + res = Err(err); + } + } + res + } +} + +/// An outgoing packet +#[derive(Debug, Clone)] +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +pub struct Transmit<'a> { + pub(crate) ecn: Option, + /// Packet contents + pub contents: &'a [u8], + /// Optional segment size for GSO + pub segment_size: Option, +} + +impl<'a> Transmit<'a> { + fn datagram_count(&self) -> usize { + match self.segment_size { + None => 1, + Some(size) => self.contents.len().div_ceil(size), + } + } +} + +/// An outgoing packet that can be sent across channels. +#[derive(Debug, Clone)] +pub(crate) struct OwnedTransmit { + pub(crate) ecn: Option, + pub(crate) contents: Bytes, + pub(crate) segment_size: Option, +} + +impl From<&noq_udp::Transmit<'_>> for OwnedTransmit { + fn from(source: &noq_udp::Transmit<'_>) -> Self { + Self { + ecn: source.ecn, + contents: Bytes::copy_from_slice(source.contents), + segment_size: source.segment_size, + } + } +} + +/// Transports address. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Addr { + /// An IP address, should always be stored in its canonical form. + Ip(SocketAddr), + /// A relay address. + Relay(RelayUrl, EndpointId), + /// A custom transport address. + Custom(CustomAddr), +} + +impl fmt::Debug for Addr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Addr::Ip(addr) => write!(f, "Ip({addr})"), + Addr::Relay(url, node_id) => write!(f, "Relay({url}, {})", node_id.fmt_short()), + Addr::Custom(custom_addr) => write!(f, "Custom({custom_addr:?})"), + } + } +} + +/// Per-packet recv data filled in by transports during [`poll_recv`][CustomEndpoint::poll_recv]. +/// +/// This carries the bits of [`noq_udp::RecvMeta`] that custom transports +/// can't express through `RecvMeta` itself: the remote address as a +/// [`CustomAddr`] and, optionally, the local custom address that received +/// the packet. For IP transports the kernel populates `RecvMeta` directly; +/// for relays the local URL is the remote's relay URL — so for them this +/// struct is filled in only with the remote variant. +/// +/// Custom transport authors construct values via [`RecvInfo::new`], which +/// only accepts [`CustomAddr`]. +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +#[derive(Clone, Debug, Default)] +pub struct RecvInfo { + remote: Addr, + local: Option, +} + +impl RecvInfo { + /// Creates a [`RecvInfo`] from an internal [`Addr`], with no local custom + /// address. Used by IP and relay recv paths. + pub(crate) fn from_addr(remote: Addr) -> Self { + Self { + remote, + local: None, + } + } + + pub(crate) fn remote(&self) -> &Addr { + &self.remote + } + + pub(crate) fn local(&self) -> Option<&CustomAddr> { + self.local.as_ref() + } +} + +#[cfg(feature = "unstable-custom-transports")] +impl RecvInfo { + /// Creates a new [`RecvInfo`] for an incoming packet on a custom transport. + /// + /// `remote` is the remote custom address. `local` is the local custom + /// address that received this packet, if the transport can identify it; + /// pass `None` otherwise. + pub fn new(remote: CustomAddr, local: Option) -> Self { + Self { + remote: Addr::Custom(remote), + local, + } + } +} + +impl Default for Addr { + fn default() -> Self { + Self::Ip(SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::UNSPECIFIED, + 0, + 0, + 0, + ))) + } +} + +impl From for Addr { + fn from(value: SocketAddr) -> Self { + match value { + SocketAddr::V4(_) => Self::Ip(value), + SocketAddr::V6(addr) => { + Self::Ip(SocketAddr::new(addr.ip().to_canonical(), addr.port())) + } + } + } +} + +impl From<&SocketAddr> for Addr { + fn from(value: &SocketAddr) -> Self { + match value { + SocketAddr::V4(_) => Self::Ip(*value), + SocketAddr::V6(addr) => { + Self::Ip(SocketAddr::new(addr.ip().to_canonical(), addr.port())) + } + } + } +} + +impl From for Addr { + fn from(value: CustomAddr) -> Self { + Self::Custom(value) + } +} + +impl From<(RelayUrl, EndpointId)> for Addr { + fn from(value: (RelayUrl, EndpointId)) -> Self { + Self::Relay(value.0, value.1) + } +} + +impl From for TransportAddr { + fn from(value: Addr) -> Self { + match value { + Addr::Ip(addr) => TransportAddr::Ip(addr), + Addr::Relay(url, _) => TransportAddr::Relay(url), + Addr::Custom(addr) => TransportAddr::Custom(addr), + } + } +} + +impl Addr { + pub(crate) fn is_relay(&self) -> bool { + matches!(self, Self::Relay(..)) + } + + /// Returns `None` if not an `Ip`. + pub(crate) fn into_socket_addr(self) -> Option { + match self { + Self::Ip(ip) => Some(ip), + Self::Relay(..) => None, + Self::Custom(_) => None, + } + } +} + +/// The local address of a network path. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum LocalTransportAddr { + /// The local IP. + /// + /// This is almost always present, only some very old operating systems do not support + /// it. + Ip(Option), + /// The relay over which this network path is connected. + Relay(RelayUrl), + /// The local custom address, if the transport reports one. + Custom(Option), +} + +impl LocalTransportAddr { + /// Converts a local address from noq into a [`LocalTransportAddr`]. + /// + /// This also needs the `remote_addr`, because currently the meaning of the local_ip as returned + /// from noq depends on the kind of network path, which we can gather from the remote address. + /// + /// The meaning of the local IP is a bit particular: + /// + /// * For IP transports, it is the address of the local socket. + /// * For relay transports, we never set the local_ip in the recv meta. + /// We take the relay URL of the remote address here instead. + /// * For custom transports, the custom transport implementation can set a [`CustomAddr`] + /// through [`RecvInfo`], which is passed as a mapped address to noq. So we convert it + /// back into the [`CustomAddr`] here. + pub(super) fn from_noq_local_ip( + noq_local_ip: Option, + remote_addr: &Addr, + custom_mapped_addrs: &AddrMap, + ) -> Self { + match &remote_addr { + // If the remote is a relay, noq_local_ip will be unset because we never set it for relay transports. + // We return a [`LocalTransportAddr`] with the relay URL. + Addr::Relay(url, _endpoint_id) => LocalTransportAddr::Relay(url.clone()), + // For IP transports, the local_ip as reported from noq is the interface IP (umapped), if known. + Addr::Ip(_) => LocalTransportAddr::Ip(noq_local_ip), + // For custom transports, the custom transport implementation can set a `CustomAddr` in `RecvInfo`. + // The custom addr is converted to a mapped address in `super::Socket::process_datagrams`. + // We convert back to a `CustomAddr` here. + Addr::Custom(_) => { + let addr = noq_local_ip + .and_then(|ip_addr| CustomMappedAddr::try_from(ip_addr).ok()) + .and_then(|custom_mapped_addr| custom_mapped_addrs.lookup(&custom_mapped_addr)); + LocalTransportAddr::Custom(addr) + } + } + } +} + +/// The kind of a transport address, used for configuring bias. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +pub enum AddrKind { + /// An IPv4 address. + IpV4, + /// An IPv6 address. + IpV6, + /// A relay address. + Relay, + /// A custom transport address with the given id. + Custom(u64), +} + +impl PartialEq for Addr { + fn eq(&self, other: &TransportAddr) -> bool { + match self { + Addr::Ip(socket_addr) => { + matches!(other, TransportAddr::Ip(a) if a == socket_addr) + } + Addr::Relay(relay_url, _) => { + matches!(other, TransportAddr::Relay(a) if a == relay_url) + } + Addr::Custom(custom_addr) => { + matches!(other, TransportAddr::Custom(a) if a == custom_addr) + } + } + } +} + +/// Identifies a network path by the combination of remote and local addresses. +/// +/// The meaning of the local address is a bit particular: +/// * For IP transports it is the interface IP, if known. +/// * For custom transports it is a custom transport address, if the transport implementation reports one. +/// * For relay transports there is no separate local address. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +pub enum FourTuple { + /// A path over an IP transport. + Ip { + /// The remote socket address. + remote: SocketAddr, + /// The local interface IP, if the OS reported one. + local: Option, + }, + /// A path over a relay transport. + Relay { + /// The URL of the relay server carrying this path. + url: RelayUrl, + /// The remote endpoint reached through the relay. + endpoint_id: EndpointId, + }, + /// A path over a custom transport. + Custom { + /// The remote custom transport address. + remote: CustomAddr, + /// The local custom transport address, if the transport reports one. + local: Option, + }, +} + +#[cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] +impl FourTuple { + /// Creates a four-tuple from a remote address, with no known local address. + pub fn from_remote(remote: Addr) -> Self { + match remote { + Addr::Ip(remote) => Self::Ip { + remote, + local: None, + }, + Addr::Relay(url, endpoint_id) => Self::Relay { url, endpoint_id }, + Addr::Custom(remote) => Self::Custom { + remote, + local: None, + }, + } + } + + /// Creates a four-tuple from a remote and a local transport address. + /// + /// The variant is determined by `remote`. The `local` address is retained only when + /// it matches that variant. A mismatched `local` is dropped, which cannot happen for + /// values derived together from the same network path. + pub fn new(remote: Addr, local: LocalTransportAddr) -> Self { + match remote { + Addr::Ip(remote) => Self::Ip { + remote, + local: match local { + LocalTransportAddr::Ip(local) => local, + _ => None, + }, + }, + Addr::Relay(url, endpoint_id) => Self::Relay { url, endpoint_id }, + Addr::Custom(remote) => Self::Custom { + remote, + local: match local { + LocalTransportAddr::Custom(local) => local, + _ => None, + }, + }, + } + } + + /// Returns the remote transport address. + pub fn remote(&self) -> Addr { + match self { + Self::Ip { remote, .. } => Addr::Ip(*remote), + Self::Relay { url, endpoint_id } => Addr::Relay(url.clone(), *endpoint_id), + Self::Custom { remote, .. } => Addr::Custom(remote.clone()), + } + } + + /// Returns the local transport address. + pub fn local(&self) -> LocalTransportAddr { + match self { + Self::Ip { local, .. } => LocalTransportAddr::Ip(*local), + Self::Relay { url, .. } => LocalTransportAddr::Relay(url.clone()), + Self::Custom { local, .. } => LocalTransportAddr::Custom(local.clone()), + } + } + + /// Returns `true` if the remote is an IP address. + pub fn is_ip(&self) -> bool { + matches!(self, Self::Ip { .. }) + } + + /// Returns `true` if the remote is a relay address. + pub fn is_relay(&self) -> bool { + matches!(self, Self::Relay { .. }) + } + + /// Returns the kind of address, for configuring bias. + pub fn addr_kind(&self) -> AddrKind { + match self { + Self::Ip { remote, .. } => match remote { + SocketAddr::V4(_) => AddrKind::IpV4, + SocketAddr::V6(_) => AddrKind::IpV6, + }, + Self::Relay { .. } => AddrKind::Relay, + Self::Custom { remote, .. } => AddrKind::Custom(remote.id()), + } + } +} + +impl fmt::Display for FourTuple { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FourTuple::Ip { remote, local } => { + if let Some(local) = local { + write!(f, "Ip({local}->{remote})") + } else { + write!(f, "Ip({remote})") + } + } + FourTuple::Relay { url, endpoint_id } => { + write!(f, "Relay({url}, {})", endpoint_id.fmt_short()) + } + FourTuple::Custom { remote, local } => { + if let Some(local) = local { + write!(f, "Custom({local}->{remote})") + } else { + write!(f, "Custom({remote})") + } + } + } + } +} + +/// A sender that sends to all our transports. +#[derive(Debug, Clone)] +pub(crate) struct TransportsSender { + #[cfg(not(wasm_browser))] + ip: IpTransportsSender, + relay: Vec, + custom: Vec>, + max_transmit_segments: NonZeroUsize, +} + +impl TransportsSender { + #[instrument(name = "poll_send", skip(self, cx, transmit), fields(len = transmit.contents.len()))] + pub(crate) fn poll_send( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context, + network_path: &FourTuple, + transmit: &Transmit<'_>, + ) -> Poll> { + match network_path { + #[cfg(wasm_browser)] + FourTuple::Ip { .. } => { + return Poll::Ready(Err(io::Error::other("IP is unsupported in browser"))); + } + #[cfg(not(wasm_browser))] + FourTuple::Ip { + remote: dst_addr, + local: src, + } => match dst_addr { + SocketAddr::V4(_) => { + if let Some(sender) = self + .ip + .v4_iter_mut() + .find(|s| s.is_valid_send_addr(*src, dst_addr)) + { + return Pin::new(sender).poll_send(cx, *dst_addr, *src, transmit); + } + if let Some(sender) = self.ip.v4_default_mut() + && sender.is_valid_default_addr(*src, dst_addr) + { + return Pin::new(sender).poll_send(cx, *dst_addr, *src, transmit); + } + } + SocketAddr::V6(_) => { + if let Some(sender) = self + .ip + .v6_iter_mut() + .find(|s| s.is_valid_send_addr(*src, dst_addr)) + { + return Pin::new(sender).poll_send(cx, *dst_addr, *src, transmit); + } + if let Some(sender) = self.ip.v6_default_mut() + && sender.is_valid_default_addr(*src, dst_addr) + { + return Pin::new(sender).poll_send(cx, *dst_addr, *src, transmit); + } + } + }, + FourTuple::Relay { url, endpoint_id } => { + let mut has_valid_sender = false; + for sender in self + .relay + .iter_mut() + .filter(|s| s.is_valid_send_addr(url, endpoint_id)) + { + has_valid_sender = true; + match sender.poll_send(cx, url.clone(), *endpoint_id, transmit) { + Poll::Pending => {} + Poll::Ready(res) => return Poll::Ready(res), + } + } + if has_valid_sender { + return Poll::Pending; + } + } + FourTuple::Custom { remote, local } => { + for sender in &mut self.custom { + if sender.is_valid_send_addr(remote) { + match sender.poll_send(cx, remote, local.as_ref(), transmit) { + Poll::Pending => {} + Poll::Ready(res) => return Poll::Ready(res), + } + } + } + } + } + + // We "blackhole" data that we have not found any usable transport for on + // to make sure the QUIC stack picks up that currently this data does not arrive. + trace!(%network_path, "no valid transport available"); + Poll::Ready(Ok(())) + } +} + +/// A [`Transports`] that works with [`MultipathMappedAddr`]s and their IPv6 representation. +/// +/// The [`MultipathMappedAddr`]s have an IPv6 representation that Noq uses. This struct +/// knows about these and maps them back to the transport [`Addr`]s used by the wrapped +/// [`Transports`]. +#[derive(Debug)] +pub(crate) struct Transport { + sock: Arc, + transports: Transports, +} + +impl Transport { + pub(crate) fn new(sock: Arc, transports: Transports) -> Self { + Self { sock, transports } + } +} + +impl noq::AsyncUdpSocket for Transport { + fn create_sender(&self) -> Pin> { + Box::pin(Sender { + sock: self.sock.clone(), + sender: self.transports.create_sender(), + }) + } + + fn poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [noq_udp::RecvMeta], + ) -> Poll> { + self.transports.poll_recv(cx, bufs, meta, &self.sock) + } + + #[cfg(not(wasm_browser))] + fn local_addr(&self) -> io::Result { + let local_addrs = self.transports.local_addrs(); + let addrs: Vec<_> = local_addrs + .into_iter() + .map(|addr| { + use crate::socket::mapped_addrs::DEFAULT_FAKE_ADDR; + + match addr { + Addr::Ip(addr) => addr, + Addr::Relay(..) => DEFAULT_FAKE_ADDR.into(), + Addr::Custom(_) => DEFAULT_FAKE_ADDR.into(), + } + }) + .collect(); + + if let Some(addr) = addrs.iter().find(|addr| addr.is_ipv6()) { + return Ok(*addr); + } + if let Some(SocketAddr::V4(addr)) = addrs.first() { + // Pretend to be IPv6, because our `MappedAddr`s need to be IPv6. + let ip = addr.ip().to_ipv6_mapped().into(); + return Ok(SocketAddr::new(ip, addr.port())); + } + + if !self.transports.relay.is_empty() { + // pretend we have an address to make sure things are not too sad during startup + use crate::socket::mapped_addrs::DEFAULT_FAKE_ADDR; + + return Ok(DEFAULT_FAKE_ADDR.into()); + } + if !self.transports.custom.is_empty() { + // pretend we have an address to make sure things are not too sad during startup + use crate::socket::mapped_addrs::DEFAULT_FAKE_ADDR; + + return Ok(DEFAULT_FAKE_ADDR.into()); + } + Err(io::Error::other("no valid address available")) + } + + #[cfg(wasm_browser)] + fn local_addr(&self) -> io::Result { + // Again, we need to pretend we're IPv6, because of our `MappedAddr`s. + Ok(SocketAddr::new(std::net::Ipv6Addr::LOCALHOST.into(), 0)) + } + + fn max_receive_segments(&self) -> NonZeroUsize { + self.transports.max_receive_segments() + } + + fn may_fragment(&self) -> bool { + self.transports.may_fragment() + } +} + +/// A sender for [`Transport`]. +/// +/// This is special in that it handles [`MultipathMappedAddr::Mixed`] by delegating to the +/// [`Socket`] which expands it back to one or more [`Addr`]s and sends it +/// using the underlying [`Transports`]. +#[derive(Debug)] +#[pin_project::pin_project] +pub(crate) struct Sender { + sock: Arc, + #[pin] + sender: TransportsSender, +} + +impl Sender { + /// Extracts the right [`Addr`] from the [`noq_udp::Transmit`]. + /// + /// Because Noq does only know about IP transports we map other transports to private + /// IPv6 Unique Local Address ranges. This extracts the transport addresses out of the + /// transmit's destination. + fn mapped_addr(&self, transmit: &noq_udp::Transmit) -> io::Result { + if self.sock.is_closed() { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "connection closed", + )); + } + + Ok(MultipathMappedAddr::from(transmit.destination)) + } +} + +impl noq::UdpSender for Sender { + fn poll_send( + self: Pin<&mut Self>, + noq_transmit: &noq_udp::Transmit, + cx: &mut Context, + ) -> Poll> { + // On errors this methods prefers returning Ok(()) to Noq. Returning an error + // should only happen if the error is permanent and fatal and it will never be + // possible to send anything again. Doing so kills the Noq EndpointDriver. Most + // send errors are intermittent errors, returning Ok(()) in those cases will mean + // Noq eventually considers the packets that had send errors as lost and will try + // and re-send them. + let mapped_addr = self.mapped_addr(noq_transmit)?; + + let network_path = match mapped_addr { + MultipathMappedAddr::Mixed(mapped_addr) => { + let Some(endpoint_id) = self.sock.mapped_addrs.endpoint_addrs.lookup(&mapped_addr) + else { + error!(dst = ?mapped_addr, "unknown NodeIdMappedAddr, dropped transmit"); + return Poll::Ready(Ok(())); + }; + + // Note we drop the src_ip set in the Noq Transmit. This is only the + // Initial packet we are sending, so we do not yet have an src address we + // need to respond from. + if let Some(src_ip) = noq_transmit.src_ip { + warn!(dst = ?mapped_addr, ?src_ip, dst_endpoint = %endpoint_id.fmt_short(), + "oops, flub didn't think this would happen"); + } + + match self.sock.try_send_remote_state_msg( + endpoint_id, + super::RemoteStateMessage::SendDatagram( + Box::new(self.sender.clone()), + OwnedTransmit::from(noq_transmit), + ), + ) { + Ok(()) => { + trace!(dst = ?mapped_addr, dst_endpoint = %endpoint_id.fmt_short(), "sent transmit"); + return Poll::Ready(Ok(())); + } + Err(msg) => { + // We do not want to block the next send which might be on a + // different transport. Instead we let Noq handle this as + // a lost datagram. + // TODO: Revisit this: we might want to do something better. + debug!( + dst = ?mapped_addr, + dst_endpoint = %endpoint_id.fmt_short(), + ?msg, + "RemoteStateActor inbox dropped message" + ); + return Poll::Ready(Ok(())); + } + }; + } + MultipathMappedAddr::Relay(relay_mapped_addr) => { + match self + .sock + .mapped_addrs + .relay_addrs + .lookup(&relay_mapped_addr) + { + Some((url, endpoint_id)) => FourTuple::Relay { url, endpoint_id }, + None => { + error!("unknown RelayMappedAddr, dropped transmit"); + return Poll::Ready(Ok(())); + } + } + } + MultipathMappedAddr::Custom(custom_mapped_addr) => { + match self + .sock + .mapped_addrs + .custom_addrs + .lookup(&custom_mapped_addr) + { + Some(addr) => { + let local = noq_transmit + .src_ip + .and_then(|ip_addr| CustomMappedAddr::try_from(ip_addr).ok()) + .and_then(|addr| self.sock.mapped_addrs.custom_addrs.lookup(&addr)); + FourTuple::Custom { + remote: addr, + local, + } + } + None => { + error!("unknown CustomMappedAddr, dropped transmit"); + return Poll::Ready(Ok(())); + } + } + } + MultipathMappedAddr::Ip(socket_addr) => { + // Ensure IPv6 mapped addresses are converted back + let socket_addr = + SocketAddr::new(socket_addr.ip().to_canonical(), socket_addr.port()); + FourTuple::Ip { + remote: socket_addr, + local: noq_transmit.src_ip, + } + } + }; + + let transmit = Transmit { + ecn: noq_transmit.ecn, + contents: noq_transmit.contents, + segment_size: noq_transmit.segment_size, + }; + let this = self.project(); + + match this.sender.poll_send(cx, &network_path, &transmit) { + Poll::Ready(Ok(())) => { + trace!( + dst = %network_path, + len = transmit.contents.len(), + datagram_count = transmit.datagram_count(), + "sent transmit" + ); + Poll::Ready(Ok(())) + } + Poll::Ready(Err(ref err)) => { + debug!(dst=%network_path, "dropped transmit: {err:#}"); + Poll::Ready(Ok(())) + } + Poll::Pending => { + // We do not want to block the next send which might be on a + // different transport. Instead we let Noq handle this as a lost + // datagram. + // TODO: Revisit this: we might want to do something better. + trace!(dst=%network_path, "transport pending, dropped transmit"); + Poll::Ready(Ok(())) + } + } + } + + fn max_transmit_segments(&self) -> NonZeroUsize { + self.sender.max_transmit_segments + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + use n0_watcher::Watchable; + + use super::*; + + impl TransportsSender { + pub(in crate::socket) fn with_bounded_relay( + capacity: usize, + #[cfg(not(wasm_browser))] ip_configs: impl Iterator, + ) -> ( + TransportsSender, + impl futures_util::Stream + Unpin, + ) { + use futures_util::StreamExt; + let (sender, receiver) = RelaySender::bounded_for_test(capacity); + let sender = TransportsSender { + #[cfg(not(wasm_browser))] + ip: IpTransports::bind(ip_configs, &EndpointMetrics::default()) + .expect("test transports must bind") + .create_sender(), + relay: vec![sender], + custom: Vec::new(), + max_transmit_segments: NonZeroUsize::new(1).unwrap(), + }; + ( + sender, + tokio_stream::wrappers::ReceiverStream::new(receiver).map(|_| ()), + ) + } + } + + const FAIRNESS_SAMPLE_POLLS: usize = 10_000; + + #[test] + fn ready_custom_transports_are_polled_fairly() { + // GIVEN: two custom transport lanes that are always ready. + let first_polls = Arc::new(AtomicUsize::new(0)); + let second_polls = Arc::new(AtomicUsize::new(0)); + let mut transports = custom_only_transports(vec![ + ready_custom_endpoint(1, first_polls.clone()), + ready_custom_endpoint(2, second_polls.clone()), + ]); + + // WHEN: receive polling runs long enough to exercise both polling orders. + let selected = + sample_ready_custom_transport_selection(&mut transports, FAIRNESS_SAMPLE_POLLS); + + let first_selected = selected.iter().filter(|&&id| id == 1).count(); + let second_selected = selected.iter().filter(|&&id| id == 2).count(); + + // THEN: selection and actual polling are split evenly between lanes. + assert_eq!(first_selected, FAIRNESS_SAMPLE_POLLS / 2); + assert_eq!(second_selected, FAIRNESS_SAMPLE_POLLS / 2); + assert_eq!( + first_polls.load(Ordering::Relaxed), + FAIRNESS_SAMPLE_POLLS / 2 + ); + assert_eq!( + second_polls.load(Ordering::Relaxed), + FAIRNESS_SAMPLE_POLLS / 2 + ); + + // And the lane order alternates from the first poll. + assert_eq!( + selected.iter().take(4).copied().collect::>(), + vec![2, 1, 2, 1], + ); + } + + fn custom_only_transports(custom: Vec>) -> Transports { + let metrics = EndpointMetrics::default(); + Transports { + #[cfg(not(wasm_browser))] + ip: ip::IpTransports::bind(std::iter::empty(), &metrics).unwrap(), + relay: Vec::new(), + custom, + poll_recv_counter: 0, + recv_infos: Default::default(), + consecutive_total_recv_failures: 0, + } + } + + fn ready_custom_endpoint(id: u8, polls: Arc) -> Box { + Box::new(ReadyCustomEndpoint { + id, + polls, + local_addr: CustomAddr::from_parts(1, &[id]), + remote_addr: CustomAddr::from_parts(2, &[id]), + local_addr_watch: Watchable::new(vec![CustomAddr::from_parts(1, &[id])]), + }) + } + + fn sample_ready_custom_transport_selection( + transports: &mut Transports, + polls: usize, + ) -> Vec { + let mut selected = Vec::with_capacity(polls); + let waker = futures_util::task::noop_waker(); + let mut cx = Context::from_waker(&waker); + let mut storage = [0u8; 1]; + let mut metas = [noq_udp::RecvMeta::default()]; + + for _ in 0..polls { + let mut bufs = [IoSliceMut::new(&mut storage)]; + let n = match transports.inner_poll_recv(&mut cx, &mut bufs, &mut metas) { + Poll::Ready(Ok(n)) => n, + Poll::Ready(Err(err)) => panic!("custom transport poll failed: {err}"), + Poll::Pending => panic!("ready custom transport returned pending"), + }; + assert_eq!(n, 1); + selected.push(storage[0]); + storage[0] = 0; + metas[0] = noq_udp::RecvMeta::default(); + } + + selected + } + + #[derive(Debug)] + struct ReadyCustomEndpoint { + id: u8, + polls: Arc, + local_addr: CustomAddr, + remote_addr: CustomAddr, + local_addr_watch: Watchable>, + } + + impl CustomEndpoint for ReadyCustomEndpoint { + fn watch_local_addrs(&self) -> n0_watcher::Direct> { + self.local_addr_watch.watch() + } + + fn create_sender(&self) -> Arc { + Arc::new(NoopCustomSender) + } + + fn poll_recv( + &mut self, + _cx: &mut Context, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + recv_infos: &mut [RecvInfo], + ) -> Poll> { + self.polls.fetch_add(1, Ordering::Relaxed); + bufs[0][0] = self.id; + metas[0].len = 1; + metas[0].stride = 1; + recv_infos[0] = RecvInfo { + remote: Addr::Custom(self.remote_addr.clone()), + local: Some(self.local_addr.clone()), + }; + Poll::Ready(Ok(1)) + } + } + + #[derive(Debug)] + struct NoopCustomSender; + + impl CustomSender for NoopCustomSender { + fn is_valid_send_addr(&self, _addr: &CustomAddr) -> bool { + false + } + + fn poll_send( + &self, + _cx: &mut Context, + _dst: &CustomAddr, + _src: Option<&CustomAddr>, + _transmit: &Transmit<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + } +} diff --git a/vendor/iroh/src/socket/transports/custom.rs b/vendor/iroh/src/socket/transports/custom.rs new file mode 100644 index 0000000..4cfadf9 --- /dev/null +++ b/vendor/iroh/src/socket/transports/custom.rs @@ -0,0 +1,105 @@ +// The items in this module are exported from [`crate::endpoint::transports`] only if +// the "unstable-custom-transports" feature is enabled +#![cfg_attr(not(feature = "unstable-custom-transports"), allow(unreachable_pub))] + +use std::{ + io, + num::NonZeroUsize, + sync::Arc, + task::{Context, Poll}, +}; + +use iroh_base::CustomAddr; + +use super::{RecvInfo, Transmit}; + +/// Custom transport. +/// +/// Usually a transport will only deal with a single custom address type, but +/// the signature allows for dealing with multiple custom address types. +/// +/// A transport is a factory for custom endpoints. Whenever an iroh endpoint is +/// created using [crate::endpoint::Builder::bind], a new custom endpoint will +/// be created using [CustomTransport::bind]. +pub trait CustomTransport: std::fmt::Debug + Send + Sync + 'static { + /// Create a custom endpoint + /// + /// Analogously to [std::net::UdpSocket::bind], this is where the actual + /// underlying hardware resource is created. + fn bind(&self) -> io::Result>; +} + +/// Custom endpoint created by a [CustomTransport]. +/// +/// An endpoint has a local address (or multiple local addresses), can receive +/// packets, and can create senders to send packets. +pub trait CustomEndpoint: std::fmt::Debug + Send + Sync + 'static { + /// A watcher for local addresses for this custom endpoint. + fn watch_local_addrs(&self) -> n0_watcher::Direct>; + /// Create a custom sender for this custom endpoint. + fn create_sender(&self) -> Arc; + /// poll receiving a packet on this custom endpoint. + /// + /// This will be called with `bufs`, `metas` and `recv_infos` of the same length. + /// It is acceptable to panic if this is not the case. + /// + /// The maximum length of the slices is [`noq_udp::BATCH_SIZE`]. + /// It is acceptable to panic if this is exceeded. + /// + /// On success, all three slices must be filled up to the returned length, + /// and the returned length must be less than or equal to the length of the slices. + /// + /// It does not make much sense to return addresses unrelated to this transport. + /// + /// For each filled slot, write a [`RecvInfo::new`] carrying the remote + /// custom address and, if the transport can identify it, the local custom + /// address that received the packet. The latter surfaces via + /// [`crate::endpoint::Incoming::local_addr`]. + fn poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + recv_infos: &mut [RecvInfo], + ) -> Poll>; + + /// Maximum number of segments to transmit (GSO). + /// + /// This controls how many datagrams Noq will batch into a single transmit. + /// The default is 1 (no batching). Custom transports that support batching + /// can override this to allow more efficient transmission. + fn max_transmit_segments(&self) -> NonZeroUsize { + NonZeroUsize::MIN + } +} + +impl std::fmt::Display for Box { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CustomTransport") + } +} + +/// Custom sender +/// +/// A sender provides a poll based interface to send packets to custom addresses. +/// It can decide whether it wants to send to a given custom address type. +/// +/// This is not enforced at type level, but [CustomSender::poll_send] should +/// only be called with addresses for which [CustomSender::is_valid_send_addr] +/// returns true. +pub trait CustomSender: std::fmt::Debug + Send + Sync + 'static { + /// True if this sender can send to the given address. + fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool; + /// poll sending a packet on this sender. + /// + /// This will only be called from iroh with addresses for which [CustomSender::is_valid_send_addr] returns true. + /// + /// You should handle invalid addresses by returning an error. + fn poll_send( + &self, + cx: &mut std::task::Context, + dst: &CustomAddr, + src: Option<&CustomAddr>, + transmit: &Transmit<'_>, + ) -> Poll>; +} diff --git a/vendor/iroh/src/socket/transports/ip.rs b/vendor/iroh/src/socket/transports/ip.rs new file mode 100644 index 0000000..4d78953 --- /dev/null +++ b/vendor/iroh/src/socket/transports/ip.rs @@ -0,0 +1,548 @@ +use std::{ + io, + net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}, + num::NonZeroUsize, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use ipnet::{Ipv4Net, Ipv6Net}; +use n0_watcher::Watchable; +use netwatch::{UdpSender, UdpSocket}; +use pin_project::pin_project; +use tracing::{debug, info, trace}; + +use super::{RecvInfo, Transmit}; +use crate::metrics::{EndpointMetrics, SocketMetrics}; + +#[derive(Debug)] +pub(crate) struct IpTransport { + config: Config, + socket: Arc, + local_addr: Watchable, + metrics: Arc, +} + +impl std::fmt::Display for IpTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let version = if self.config.is_ipv4() { "v4" } else { "v6" }; + write!(f, "IpTransport({version})") + } +} + +/// IP transport configuration +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum Config { + /// General IPv4 binding + V4 { + /// The IP address to bind on + ip_net: Ipv4Net, + /// The port to bind on + port: u16, + /// Is binding mandatory? + is_required: bool, + /// Is this a default route? + is_default: bool, + }, + /// General IPv6 binding + V6 { + /// The IP address to bind on + ip_net: Ipv6Net, + /// The scope id. + scope_id: u32, + /// The port to bind on + port: u16, + /// Is binding mandatory? + is_required: bool, + /// Is this a default route? + is_default: bool, + }, +} + +impl Config { + /// Is this a v4 config. + pub(crate) fn is_ipv4(&self) -> bool { + matches!(self, | Self::V4 { .. }) + } + + /// Is this a v6 config. + pub(crate) fn is_ipv6(&self) -> bool { + matches!(self, | Self::V6 { .. }) + } + + /// Returns the prefix len for the address. + pub(crate) fn prefix_len(&self) -> u8 { + match self { + Self::V4 { ip_net, .. } => ip_net.prefix_len(), + Self::V6 { ip_net, .. } => ip_net.prefix_len(), + } + } + + /// Is this a default config? + pub(crate) fn is_default(&self) -> bool { + match self { + Self::V4 { is_default, .. } => *is_default, + Self::V6 { is_default, .. } => *is_default, + } + } + + /// Is this required to bind. + pub(crate) fn is_required(&self) -> bool { + match self { + Self::V4 { is_required, .. } => *is_required, + Self::V6 { is_required, .. } => *is_required, + } + } + + pub(crate) fn is_valid_default_addr(&self, src: Option, dst: SocketAddr) -> bool { + match src { + Some(src) => match (self, src) { + (Self::V4 { is_default, .. }, IpAddr::V4(_)) => *is_default, + (Self::V6 { is_default, .. }, IpAddr::V6(_)) => *is_default, + _ => false, + }, + None => match (self, dst) { + (Self::V4 { is_default, .. }, SocketAddr::V4(_)) => *is_default, + (Self::V6 { is_default, .. }, SocketAddr::V6(_)) => *is_default, + _ => false, + }, + } + } + + /// Does this configuration match to send to the given `src` and `dst` address. + pub(crate) fn is_valid_send_addr(&self, src: Option, dst: SocketAddr) -> bool { + match src { + Some(src) => match (self, src) { + (Self::V4 { ip_net, .. }, IpAddr::V4(src)) => { + ip_net.addr().is_unspecified() || ip_net.addr() == src + } + (Self::V6 { ip_net, .. }, IpAddr::V6(src)) => { + ip_net.addr().is_unspecified() || ip_net.addr() == src + } + _ => false, + }, + None => { + match (self, dst) { + (Self::V4 { ip_net, .. }, SocketAddr::V4(dst_v4)) => { + ip_net.contains(dst_v4.ip()) + } + ( + Self::V6 { + ip_net, scope_id, .. + }, + SocketAddr::V6(dst_v6), + ) => { + if ip_net.contains(dst_v6.ip()) { + return true; + } + if dst_v6.ip().is_unicast_link_local() { + // If we have a link local interface, use the scope id + if *scope_id == dst_v6.scope_id() { + return true; + } + } + false + } + _ => false, + } + } + } + } +} + +impl From for SocketAddr { + fn from(value: Config) -> Self { + match value { + Config::V4 { ip_net, port, .. } => { + SocketAddr::V4(SocketAddrV4::new(ip_net.addr(), port)) + } + Config::V6 { + ip_net, + scope_id, + port, + .. + } => SocketAddr::V6(SocketAddrV6::new(ip_net.addr(), port, 0, scope_id)), + } + } +} + +impl IpTransport { + pub(crate) fn bind(config: Config, metrics: Arc) -> io::Result { + let addr: SocketAddr = config.into(); + debug!(?addr, "binding"); + let socket = netwatch::UdpSocket::bind_full(addr).inspect_err(|err| { + debug!(%addr, "failed to bind: {err:#}"); + })?; + let local_addr = socket.local_addr()?; + debug!(%addr, %local_addr, "successfully bound"); + // Currently gets updated on manual rebind + // TODO: update when UdpSocket under the hood rebinds automatically + let local_addr = Watchable::new(local_addr); + + Ok(Self { + config, + socket: Arc::new(socket), + local_addr, + metrics, + }) + } + + /// NOTE: Receiving on a closed socket will return [`Poll::Pending`] indefinitely. + pub(super) fn poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + recv_infos: &mut [RecvInfo], + ) -> Poll> { + assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + assert_eq!( + bufs.len(), + recv_infos.len(), + "non matching bufs & recv_infos" + ); + match self.socket.poll_recv_noq(cx, bufs, metas) { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(n)) => { + for i in 0..n { + let meta = &mut metas[i]; + let recv_info = &mut recv_infos[i]; + if meta.addr.is_ipv4() { + // The AsyncUdpSocket is an AF_INET6 socket and needs to show this + // as coming from an IPv4-mapped IPv6 addresses, since Noq will + // use those when sending on an INET6 socket. + let v6_ip = match meta.addr.ip() { + IpAddr::V4(ipv4_addr) => ipv4_addr.to_ipv6_mapped(), + IpAddr::V6(ipv6_addr) => ipv6_addr, + }; + meta.addr = SocketAddr::new(v6_ip.into(), meta.addr.port()); + } + // The transport addresses are internal to iroh and we always want those + // to remain the canonical address. + *recv_info = RecvInfo::from_addr( + SocketAddr::new(meta.addr.ip().to_canonical(), meta.addr.port()).into(), + ); + } + Poll::Ready(Ok(n)) + } + Poll::Ready(Err(err)) => Poll::Ready(Err(err)), + } + } + + pub(super) fn local_addr_watch(&self) -> n0_watcher::Direct { + self.local_addr.watch() + } + + pub(super) fn max_transmit_segments(&self) -> NonZeroUsize { + self.socket.max_gso_segments() + } + + pub(super) fn max_receive_segments(&self) -> NonZeroUsize { + self.socket.gro_segments() + } + + pub(super) fn may_fragment(&self) -> bool { + self.socket.may_fragment() + } + + pub(crate) fn bind_addr(&self) -> SocketAddr { + self.config.into() + } + + pub(super) fn create_network_change_sender(&self) -> IpNetworkChangeSender { + IpNetworkChangeSender { + socket: self.socket.clone(), + local_addr: self.local_addr.clone(), + } + } + + pub(super) fn create_sender(&self) -> IpSender { + let sender = self.socket.clone().create_sender(); + IpSender { + config: self.config, + sender, + metrics: self.metrics.clone(), + } + } +} + +#[derive(Debug)] +pub(super) struct IpNetworkChangeSender { + socket: Arc, + local_addr: Watchable, +} + +impl IpNetworkChangeSender { + pub(super) fn rebind(&self) -> io::Result<()> { + let old_addr = self.local_addr.get(); + self.socket.rebind()?; + let addr = self.socket.local_addr()?; + self.local_addr.set(addr).ok(); + trace!("rebound from {} to {}", old_addr, addr); + + Ok(()) + } + + pub(super) fn on_network_change(&self, _info: &crate::socket::Report) { + // Nothing to do for now + } +} + +#[derive(Debug, Clone)] +#[pin_project] +pub(super) struct IpSender { + config: Config, + #[pin] + sender: UdpSender, + metrics: Arc, +} + +impl IpSender { + pub(super) fn is_valid_send_addr(&self, src: Option, dst: &SocketAddr) -> bool { + self.config.is_valid_send_addr(src, *dst) + } + + pub(super) fn is_valid_default_addr(&self, src: Option, dst: &SocketAddr) -> bool { + self.config.is_valid_default_addr(src, *dst) + } + + /// Creates a canonical socket address. + /// + /// We may be asked to send IPv4-mapped IPv6 addresses. But our sockets are configured + /// to only send their actual family. So we need to map those back to the canonical + /// addresses. + #[inline] + fn canonical_addr(addr: SocketAddr) -> SocketAddr { + SocketAddr::new(addr.ip().to_canonical(), addr.port()) + } + + pub(super) fn poll_send( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context, + dst: SocketAddr, + src: Option, + transmit: &Transmit<'_>, + ) -> Poll> { + let total_bytes = transmit.contents.len() as u64; + let res = Pin::new(&mut self.sender).poll_send( + &noq_udp::Transmit { + destination: Self::canonical_addr(dst), + ecn: transmit.ecn, + contents: transmit.contents, + segment_size: transmit.segment_size, + src_ip: src, + }, + cx, + ); + + match res { + Poll::Ready(Ok(res)) => { + match dst { + SocketAddr::V4(_) => { + self.metrics.send_ipv4.inc_by(total_bytes); + } + SocketAddr::V6(_) => { + self.metrics.send_ipv6.inc_by(total_bytes); + } + } + Poll::Ready(Ok(res)) + } + Poll::Ready(Err(err)) => Poll::Ready(Err(err)), + Poll::Pending => Poll::Pending, + } + } +} + +#[derive(Debug, Clone)] +pub(super) struct IpTransportsSender { + /// Stored sorted by prefix len + v4: Vec, + default_v4_index: Option, + /// Stored sorted by prefix len + v6: Vec, + default_v6_index: Option, +} + +impl IpTransportsSender { + pub(super) fn v4_iter_mut(&mut self) -> impl Iterator { + self.v4.iter_mut() + } + + pub(super) fn v4_default_mut(&mut self) -> Option<&mut IpSender> { + if let Some(i) = self.default_v4_index { + return Some(&mut self.v4[i]); + } + None + } + + pub(super) fn v6_iter_mut(&mut self) -> impl Iterator { + self.v6.iter_mut() + } + + pub(super) fn v6_default_mut(&mut self) -> Option<&mut IpSender> { + if let Some(i) = self.default_v6_index { + return Some(&mut self.v6[i]); + } + None + } +} + +#[derive(Debug)] +pub(super) struct IpTransports { + v4: Vec, + default_v4_index: Option, + v6: Vec, + default_v6_index: Option, +} + +impl IpTransports { + pub(super) fn create_sender(&self) -> IpTransportsSender { + let ip_v4 = self.v4.iter().map(|t| t.create_sender()).collect(); + let ip_v6 = self.v6.iter().map(|t| t.create_sender()).collect(); + + IpTransportsSender { + v4: ip_v4, + default_v4_index: self.default_v4_index, + v6: ip_v6, + default_v6_index: self.default_v6_index, + } + } + + pub(super) fn iter(&self) -> impl Iterator { + self.v4.iter().chain(self.v6.iter()) + } + + pub(super) fn bind( + configs: impl Iterator, + metrics: &EndpointMetrics, + ) -> io::Result { + let mut has_v4_default = false; + let mut ip_v4 = Vec::new(); + + let mut has_v6_default = false; + let mut ip_v6 = Vec::new(); + + for config in configs { + match IpTransport::bind(config, metrics.socket.clone()) { + Ok(transport) => { + if config.is_ipv4() { + if config.is_default() { + if has_v4_default { + return Err(io::Error::other( + "can only have a single IPv4 default transport", + )); + } + has_v4_default = true; + } + ip_v4.push(transport); + } else if config.is_ipv6() { + if config.is_default() { + if has_v6_default { + return Err(io::Error::other( + "can only have a single IPv6 default transport", + )); + } + has_v6_default = true; + } + ip_v6.push(transport); + } + } + Err(err) => { + if config.is_required() { + return Err(err); + } + info!("ignoring non required bind failure: {:?}", err); + } + } + } + + // Sort in descending order by prefix len + ip_v4.sort_by_key(|i| std::cmp::Reverse(i.config.prefix_len())); + ip_v6.sort_by_key(|i| std::cmp::Reverse(i.config.prefix_len())); + + let default_v4_index = ip_v4.iter().position(|i| i.config.is_default()); + let default_v6_index = ip_v6.iter().position(|i| i.config.is_default()); + + Ok(Self { + v4: ip_v4, + default_v4_index, + v6: ip_v6, + default_v6_index, + }) + } + + pub(super) fn iter_mut(&mut self) -> impl Iterator { + self.v4.iter_mut().chain(self.v6.iter_mut()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_bind_sorting() -> n0_error::Result { + let has_ipv6 = tokio::net::UdpSocket::bind("[::1]:0").await.is_ok(); + eprintln!("testing with ipv6? {has_ipv6}"); + + let metrics = EndpointMetrics::default(); + let config = vec![ + Config::V4 { + ip_net: Ipv4Net::new("127.0.0.1".parse().unwrap(), 8).unwrap(), + port: 2222, + is_required: true, + is_default: false, + }, + Config::V4 { + ip_net: Ipv4Net::new("127.0.0.1".parse().unwrap(), 24).unwrap(), + port: 1111, + is_required: true, + is_default: true, + }, + Config::V4 { + ip_net: Ipv4Net::new("127.0.0.1".parse().unwrap(), 0).unwrap(), + port: 9999, + is_required: true, + is_default: false, + }, + Config::V6 { + ip_net: Ipv6Net::new("::1".parse().unwrap(), 4).unwrap(), + port: 2228, + scope_id: 0, + is_required: has_ipv6, + is_default: false, + }, + Config::V6 { + ip_net: Ipv6Net::new("::1".parse().unwrap(), 2).unwrap(), + port: 9998, + scope_id: 0, + is_required: has_ipv6, + is_default: true, + }, + Config::V6 { + ip_net: Ipv6Net::new("::1".parse().unwrap(), 32).unwrap(), + port: 1118, + scope_id: 0, + is_required: has_ipv6, + is_default: false, + }, + ]; + + let transports = IpTransports::bind(config.into_iter(), &metrics)?; + assert_eq!(transports.v4[0].config.prefix_len(), 24); + assert_eq!(transports.v4[1].config.prefix_len(), 8); + assert_eq!(transports.v4[2].config.prefix_len(), 0); + + assert_eq!(transports.default_v4_index, Some(0)); + + if has_ipv6 { + assert_eq!(transports.v6[0].config.prefix_len(), 32); + assert_eq!(transports.v6[1].config.prefix_len(), 4); + assert_eq!(transports.v6[2].config.prefix_len(), 2); + + assert_eq!(transports.default_v6_index, Some(2)); + } + Ok(()) + } +} diff --git a/vendor/iroh/src/socket/transports/relay.rs b/vendor/iroh/src/socket/transports/relay.rs new file mode 100644 index 0000000..77c46ca --- /dev/null +++ b/vendor/iroh/src/socket/transports/relay.rs @@ -0,0 +1,478 @@ +use std::{ + io, + num::NonZeroU16, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use iroh_base::{EndpointId, RelayUrl}; +use iroh_relay::protos::relay::Datagrams; +use n0_future::{ + ready, + task::{self, AbortOnDropHandle}, +}; +use n0_watcher::Watcher as _; +use tokio::sync::mpsc; +use tokio_util::sync::{CancellationToken, PollSender}; +use tracing::{Instrument, error, info_span, warn}; + +use super::{RecvInfo, Transmit}; +use crate::endpoint::RelayStatus; + +mod actor; + +pub(crate) use self::actor::{ + Config as RelayActorConfig, HomeRelayWatch, RelayConnectionFailure, RelayConnectionState, +}; +use self::actor::{RelayActor, RelayActorMessage, RelayRecvDatagram, RelaySendItem}; + +type RelayAddrWatcher = + n0_watcher::Map>, Option<(RelayUrl, EndpointId)>>; + +#[derive(Debug)] +pub(crate) struct RelayTransport { + /// Queue to receive datagrams from relays for [`noq::AsyncUdpSocket::poll_recv`]. + relay_datagram_recv_queue: mpsc::Receiver, + /// Channel on which to send datagrams via a relay server. + relay_datagram_send_channel: mpsc::Sender, + /// A datagram from the last poll_recv that didn't quite fit our buffers. + pending_item: Option, + actor_sender: mpsc::Sender, + _actor_handle: AbortOnDropHandle<()>, + my_relay: HomeRelayWatch, + my_endpoint_id: EndpointId, +} + +impl std::fmt::Display for RelayTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RelayTransport") + } +} + +impl RelayTransport { + pub(crate) fn new(config: RelayActorConfig, cancel_token: CancellationToken) -> Self { + let (relay_datagram_send_tx, relay_datagram_send_rx) = mpsc::channel(256); + + let (relay_datagram_recv_tx, relay_datagram_recv_rx) = mpsc::channel(512); + + let (actor_sender, actor_receiver) = mpsc::channel(256); + + let my_endpoint_id = config.secret_key.public(); + let my_relay = config.my_relay.clone(); + + let relay_actor = RelayActor::new(config, relay_datagram_recv_tx, cancel_token); + + let actor_handle = AbortOnDropHandle::new(task::spawn( + async move { + relay_actor + .run(actor_receiver, relay_datagram_send_rx) + .await; + } + .instrument(info_span!("relay-actor")), + )); + + Self { + relay_datagram_recv_queue: relay_datagram_recv_rx, + relay_datagram_send_channel: relay_datagram_send_tx, + pending_item: None, + actor_sender, + _actor_handle: actor_handle, + my_relay, + my_endpoint_id, + } + } + + pub(crate) fn create_sender(&self) -> RelaySender { + RelaySender { + sender: PollSender::new(self.relay_datagram_send_channel.clone()), + } + } + + pub(super) fn poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + recv_infos: &mut [RecvInfo], + ) -> Poll> { + assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + assert_eq!( + bufs.len(), + recv_infos.len(), + "non matching bufs & recv_infos" + ); + let mut num_msgs = 0; + for i in 0..bufs.len() { + let buf_out = &mut bufs[i]; + let meta_out = &mut metas[i]; + let recv_info = &mut recv_infos[i]; + let dm = match self.poll_recv_queue(cx) { + Poll::Ready(Some(recv)) => recv, + Poll::Ready(None) => { + error!("relay_recv_channel closed"); + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::NotConnected, + "connection closed", + ))); + } + Poll::Pending => { + break; + } + }; + + // This *tries* to make the datagrams fit into our buffer by re-batching them. + let num_segments = dm + .datagrams + .segment_size + .map_or(1, |ss| buf_out.len() / u16::from(ss) as usize); + let datagrams = dm.datagrams.take_segments(num_segments); + let empty_now = datagrams.contents.is_empty(); + let empty_after = dm.datagrams.contents.is_empty(); + + let dm = RelayRecvDatagram { + datagrams, + src: dm.src, + url: dm.url.clone(), + }; + + // If `take_segments` processed the whole contents (empty_after) or none at all + // (empty_now) we shouldn't process what's left in `self.pending_item`. + // In the first case the remaining `pending_item` is empty and can be dropped. + // In the second case a single segment could not fit into the buffer, future + // calls to `poll_recv` would not change this so drop the `pending_item` with + // the oversized segment size. + if empty_after || empty_now { + self.pending_item = None; + } + if empty_now { + warn!( + noq_buf_len = buf_out.len(), + segment_size = ?dm.datagrams.segment_size, + "dropping received datagram: segment_size too large"); + continue; + } + + if buf_out.len() < dm.datagrams.contents.len() { + // Our receive buffer isn't big enough to process this datagram. + // Continuing would cause a panic. + warn!( + noq_buf_len = buf_out.len(), + datagram_len = dm.datagrams.contents.len(), + segment_size = ?dm.datagrams.segment_size, + "dropping received datagram: noq buffer too small" + ); + break; + // In theory we could put some logic in here to fragment the datagram in case + // we still have enough room in our `buf_out` left to fit a couple of + // `dm.datagrams.segment_size`es, but we *should* have cut those datagrams + // to appropriate sizes earlier in the pipeline (just before we put them + // into the `relay_datagram_recv_queue` in the `ActiveRelayActor`). + // So the only case in which this happens is we receive a datagram via the relay + // that's essentially bigger than our configured `max_udp_payload_size`. + // In that case we drop it and let MTU discovery take over. + } + + buf_out[..dm.datagrams.contents.len()].copy_from_slice(&dm.datagrams.contents); + meta_out.len = dm.datagrams.contents.len(); + meta_out.stride = dm + .datagrams + .segment_size + .map_or(dm.datagrams.contents.len(), |s| u16::from(s) as usize); + meta_out.ecn = None; + meta_out.dst_ip = None; + + *recv_info = RecvInfo::from_addr((dm.url, dm.src).into()); + num_msgs += 1; + } + + // If we have any msgs to report, they are in the first `num_msgs_total` slots + if num_msgs > 0 { + assert!(num_msgs <= metas.len()); + Poll::Ready(Ok(num_msgs)) + } else { + Poll::Pending + } + } + + pub(super) fn local_addr_watch(&self) -> RelayAddrWatcher { + let my_endpoint_id = self.my_endpoint_id; + self.my_relay + .watch() + .map(move |status| status.map(|status| (status.url().clone(), my_endpoint_id))) + } + + pub(super) fn my_relay_status(&self) -> n0_watcher::Direct> { + self.my_relay.watch() + } + + pub(super) fn create_network_change_sender(&self) -> RelayNetworkChangeSender { + RelayNetworkChangeSender { + sender: self.actor_sender.clone(), + } + } + + /// Makes sure we have a pending item stored, if not, it'll poll a new one from the queue. + /// + /// Returns a mutable reference to the stored pending item. + #[inline] + fn poll_recv_queue<'a>( + &'a mut self, + cx: &mut Context, + ) -> Poll> { + // Borrow checker doesn't quite understand an if let Some(_)... here + if self.pending_item.is_some() { + return Poll::Ready(self.pending_item.as_mut()); + } + + let item = match self.relay_datagram_recv_queue.poll_recv(cx) { + Poll::Ready(Some(item)) => item, + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => return Poll::Pending, + }; + + Poll::Ready(Some(self.pending_item.insert(item))) + } +} + +#[derive(Debug)] +pub(super) struct RelayNetworkChangeSender { + sender: mpsc::Sender, +} + +impl RelayNetworkChangeSender { + pub(super) fn on_network_change(&self, report: &crate::socket::Report) { + self.send_relay_actor(RelayActorMessage::NetworkChange { + report: report.clone(), + }); + } + + /// Triggers an immediate health check on relay connections after a network change. + pub(super) fn check_connection_after_network_change(&self) { + self.send_relay_actor(RelayActorMessage::CheckConnectionAfterNetworkChange); + } + + pub(super) fn rebind(&self) -> io::Result<()> { + self.send_relay_actor(RelayActorMessage::MaybeCloseRelaysOnRebind); + + Ok(()) + } + + fn send_relay_actor(&self, msg: RelayActorMessage) { + match self.sender.try_send(msg) { + Ok(_) => {} + Err(mpsc::error::TrySendError::Closed(_)) => { + warn!("unable to send to relay actor, already closed"); + } + Err(mpsc::error::TrySendError::Full(_)) => { + warn!("dropping message for relay actor, channel is full"); + } + } + } +} + +/// Sender to send datagrams to the [`RelayActor`]. +/// +/// This includes the waker coordination required to support [`noq::UdpSender::poll_send`]. +#[derive(Debug, Clone)] +pub(crate) struct RelaySender { + sender: PollSender, +} + +impl RelaySender { + pub(super) fn is_valid_send_addr(&self, _url: &RelayUrl, _endpoint_id: &EndpointId) -> bool { + true + } + + pub(super) fn poll_send( + &mut self, + cx: &mut Context, + dest_url: RelayUrl, + dest_endpoint: EndpointId, + transmit: &Transmit<'_>, + ) -> Poll> { + match ready!(self.sender.poll_reserve(cx)) { + Ok(()) => { + let contents = datagrams_from_transmit(transmit); + let item = RelaySendItem { + remote_endpoint: dest_endpoint, + url: dest_url.clone(), + datagrams: contents, + }; + match self.sender.send_item(item) { + Ok(()) => Poll::Ready(Ok(())), + Err(_err) => Poll::Ready(Err(io::Error::new( + io::ErrorKind::ConnectionReset, + "channel to actor is closed", + ))), + } + } + Err(_err) => Poll::Ready(Err(io::Error::new( + io::ErrorKind::ConnectionReset, + "channel to actor is closed", + ))), + } + } +} + +/// Translate a UDP transmit to the `Datagrams` type for sending over the relay. +fn datagrams_from_transmit(transmit: &Transmit<'_>) -> Datagrams { + Datagrams { + ecn: transmit.ecn.map(|ecn| match ecn { + noq_udp::EcnCodepoint::Ect0 => noq_proto::EcnCodepoint::Ect0, + noq_udp::EcnCodepoint::Ect1 => noq_proto::EcnCodepoint::Ect1, + noq_udp::EcnCodepoint::Ce => noq_proto::EcnCodepoint::Ce, + }), + segment_size: transmit + .segment_size + .map(|ss| ss as u16) + .and_then(NonZeroU16::new), + contents: Bytes::copy_from_slice(transmit.contents), + } +} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeSet, + num::NonZeroU16, + sync::{Arc, atomic::AtomicBool}, + time::Duration, + }; + + use iroh_base::{EndpointId, SecretKey}; + use iroh_relay::{ + RelayMap, + tls::{CaTlsConfig, default_provider}, + }; + use tokio::task::JoinSet; + use tracing::debug; + + use super::*; + use crate::{defaults::staging, dns::DnsResolver}; + + impl RelaySender { + pub(in crate::socket::transports) fn bounded_for_test( + capacity: usize, + ) -> (RelaySender, mpsc::Receiver) { + let (sender, receiver) = mpsc::channel(capacity); + ( + RelaySender { + sender: PollSender::new(sender), + }, + receiver, + ) + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_relay_datagram_queue() { + let capacity = 16; + let (sender, mut receiver) = mpsc::channel(capacity); + let url = staging::default_na_east_relay().url; + + let mut tasks = JoinSet::new(); + + tasks.spawn({ + async move { + let mut expected_msgs: BTreeSet = (0..capacity).collect(); + while !expected_msgs.is_empty() { + let datagram: RelayRecvDatagram = receiver.recv().await.unwrap(); + let msg_num = usize::from_le_bytes(datagram.datagrams.contents.as_ref().try_into().unwrap()); + debug!("Received {msg_num}"); + + if !expected_msgs.remove(&msg_num) { + panic!("Received message number {msg_num} twice or more, but expected it only exactly once."); + } + } + } + }); + + for i in 0..capacity { + tasks.spawn({ + let sender = sender.clone(); + let url = url.clone(); + async move { + debug!("Sending {i}"); + sender + .try_send(RelayRecvDatagram { + url, + src: EndpointId::from_bytes(&[0u8; 32]).unwrap(), + datagrams: Datagrams::from(&i.to_le_bytes()), + }) + .unwrap(); + } + }); + } + + // We expect all of this work to be done in 10 seconds max. + if tokio::time::timeout(Duration::from_secs(10), tasks.join_all()) + .await + .is_err() + { + panic!("Timeout - not all messages between 0 and {capacity} received."); + } + } + + /// Builds a [`RelayTransport`] that never actually dials a relay (its home + /// relay watcher is left unset), just so we get a real `poll_recv` to exercise. + fn test_relay_transport() -> RelayTransport { + let config = RelayActorConfig { + my_relay: HomeRelayWatch::default(), + secret_key: SecretKey::from_bytes(&[7u8; 32]), + dns_resolver: DnsResolver::new(), + proxy_url: None, + ipv6_reported: Arc::new(AtomicBool::new(false)), + tls_config: CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"), + metrics: Default::default(), + relay_map: RelayMap::empty(), + }; + RelayTransport::new(config, CancellationToken::new()) + } + + #[tokio::test(flavor = "multi_thread")] + async fn progress_is_made_for_large_segment_size_datagram_batch() { + let mut transport = test_relay_transport(); + + let url = staging::default_na_east_relay().url; + let src = EndpointId::from_bytes(&[3u8; 32]).unwrap(); + + // Fat datagram batch with segment size larger than the buffer + let datagrams = Datagrams { + ecn: None, + segment_size: NonZeroU16::new(2000), + contents: Bytes::from(vec![0u8; 4000]), + }; + + transport.pending_item = Some(RelayRecvDatagram { + url, + src, + datagrams, + }); + + let waker = std::task::Waker::noop(); + let mut cx = Context::from_waker(waker); + + // Deliberately smaller than the 2000-byte segment size above + let mut storage = [0u8; 1500]; + let mut metas = [noq_udp::RecvMeta::default()]; + let mut recv_infos = [RecvInfo::default()]; + + let mut progressed = false; + // progress does not need to be immediate but within a reasonable number of iterations + for _ in 0..10 { + let mut bufs = [io::IoSliceMut::new(&mut storage)]; + match transport.poll_recv(&mut cx, &mut bufs, &mut metas, &mut recv_infos) { + Poll::Ready(Ok(_)) | Poll::Pending => {} + Poll::Ready(Err(err)) => panic!("poll_recv failed: {err}"), + } + if transport.pending_item.is_none() { + progressed = true; + break; + } + } + + assert!(progressed, "poll_recv made no progress on batch too large"); + } +} diff --git a/vendor/iroh/src/socket/transports/relay/actor.rs b/vendor/iroh/src/socket/transports/relay/actor.rs new file mode 100644 index 0000000..5a991fb --- /dev/null +++ b/vendor/iroh/src/socket/transports/relay/actor.rs @@ -0,0 +1,2030 @@ +//! The relay actor. +//! +//! The [`RelayActor`] handles all the relay connections. It is helped by the +//! [`ActiveRelayActor`] which handles a single relay connection. +//! +//! - The [`RelayActor`] manages all connections to relay servers. +//! - It starts a new [`ActiveRelayActor`] for each relay server needed. +//! - The [`ActiveRelayActor`] will exit when unused. +//! - Unless it is for the home relay, this one never exits. +//! - Each [`ActiveRelayActor`] uses a relay [`Client`]. +//! - The relay [`Client`] is a `Stream` and `Sink` directly connected to the +//! `TcpStream` connected to the relay server. +//! - Each [`ActiveRelayActor`] will try and maintain a connection with the relay server. +//! - If connections fail, exponential backoff is used for reconnections. +//! - When `AsyncUdpSocket` needs to send datagrams: +//! - It puts them on a queue to the [`RelayActor`]. +//! - The [`RelayActor`] ensures the correct [`ActiveRelayActor`] is running and +//! forwards datagrams to it. +//! - The ActiveRelayActor sends datagrams directly to the relay server. +//! - The relay receive path is: +//! - Whenever [`ActiveRelayActor`] is connected it reads from the underlying `TcpStream`. +//! - Received datagrams are placed on an mpsc channel that now bypasses the +//! [`RelayActor`] and goes straight to the `AsyncUpdSocket` interface. +//! +//! [`Client`]: iroh_relay::client::Client + +#[cfg(test)] +use std::net::SocketAddr; +use std::{ + collections::{BTreeMap, BTreeSet}, + future::Future, + net::IpAddr, + pin::{Pin, pin}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use backon::{Backoff, BackoffBuilder, ExponentialBuilder}; +use iroh_base::{EndpointId, RelayUrl, SecretKey}; +use iroh_relay::{ + self as relay, PingTracker, RelayMap, + client::{Client, ConnectError, RecvError, SendError}, + protos::{ + handshake, + relay::{ClientToRelayMsg, Datagrams, RelayToClientMsg, Status}, + }, +}; +use n0_error::{AnyError, e, stack_error}; +use n0_future::{ + FuturesUnorderedBounded, MaybeFuture, SinkExt, StreamExt, + task::{JoinError, JoinSet}, + time::{self, Duration, Instant, MissedTickBehavior}, +}; +use n0_watcher::Watchable; +use netwatch::interfaces; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; +use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; +use url::Url; + +#[cfg(not(wasm_browser))] +use crate::dns::DnsResolver; +use crate::{endpoint::RelayStatus, net_report::Report, socket::Metrics as SocketMetrics}; + +/// How long a non-home relay connection needs to be idle (last written to) before we close it. +const RELAY_INACTIVE_CLEANUP_TIME: Duration = Duration::from_secs(60); + +/// Interval in which we ping the relay server to ensure the connection is alive. +/// +/// The default QUIC max_idle_timeout is 30s, so setting that to half this time gives some +/// chance of recovering. +const PING_INTERVAL: Duration = Duration::from_secs(15); + +/// Number of datagrams which can be sent to the relay server in one batch. +/// +/// This means while this batch is sending to the server no other relay protocol frames can +/// be sent to the server, e.g. no Ping frames or so. While the maximum packet size is +/// rather large, each item can typically be expected to up to 1500 or the max GSO size. +const SEND_DATAGRAM_BATCH_SIZE: usize = 20; + +/// Timeout for establishing the relay connection. +/// +/// This includes DNS, dialing the server, upgrading the connection, and completing the +/// handshake. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +/// Time after which the [`ActiveRelayActor`] will drop undeliverable datagrams. +/// +/// When the [`ActiveRelayActor`] is not connected it can not deliver datagrams. However it +/// will still receive datagrams to send from the [`RelayActor`]. If connecting takes +/// longer than this timeout datagrams will be dropped. +/// +/// This value is set to 3 times the QUIC initial Probe Timeout (PTO). +const UNDELIVERABLE_DATAGRAM_TIMEOUT: Duration = Duration::from_secs(3); + +/// An actor which handles the connection to a single relay server. +/// +/// It is responsible for maintaining the connection to the relay server and handling all +/// communication with it. +/// +/// The actor shuts down itself on inactivity: inactivity is determined when no more +/// datagrams are being queued to send. +/// +/// This actor has 3 main states it can be in, each has it's dedicated run loop: +/// +/// - Dialing the relay server. +/// +/// This will continuously dial the server until connected, using exponential backoff if +/// it can not connect. See [`ActiveRelayActor::run_dialing`]. +/// +/// - Connected to the relay server. +/// +/// This state allows receiving from the relay server, though sending is idle in this +/// state. See [`ActiveRelayActor::run_connected`]. +/// +/// - Sending to the relay server. +/// +/// This is a sub-state of `connected` so the actor can still be receiving from the relay +/// server at this time. However it is actively sending data to the server so can not +/// consume any further items from inboxes which will result in sending more data to the +/// server until the actor goes back to the `connected` state. +/// +/// All these are driven from the top-level [`ActiveRelayActor::run`] loop. +#[derive(Debug)] +struct ActiveRelayActor { + // The inboxes and channels this actor communicates over. + /// Inbox for messages which should be handled without any blocking. + prio_inbox: mpsc::Receiver, + /// Inbox for messages which involve sending to the relay server. + inbox: mpsc::Receiver, + /// Queue for received relay datagrams. + relay_datagrams_recv: mpsc::Sender, + /// Channel on which we queue packets to send to the relay. + relay_datagrams_send: mpsc::Receiver, + + // Other actor state. + /// The relay server for this actor. + url: RelayUrl, + /// Builder which can repeatedly build a relay client. + relay_client_builder: relay::client::ClientBuilder, + /// Whether or not this is the home relay server. + /// + /// The home relay server needs to maintain it's connection to the relay server, even if + /// the relay actor is otherwise idle. + is_home_relay: bool, + /// When this expires the actor has been idle and should shut down. + /// + /// Unless it is managing the home relay connection. Inactivity is only tracked on the + /// last datagram sent to the relay, received datagrams will trigger QUIC ACKs which is + /// sufficient to keep active connections open. + inactive_timeout: Pin>, + /// Token indicating the [`ActiveRelayActor`] should stop. + stop_token: CancellationToken, + metrics: Arc, + my_relay: HomeRelayWatch, +} + +#[derive(Debug)] +enum ActiveRelayMessage { + /// Triggers a connection check to the relay server. + /// + /// Sometimes it is known the local network interfaces have changed in which case it + /// might be prudent to check if the relay connection is still working. `Vec` + /// should contain the current local IP addresses. If the connection uses a local + /// socket with an IP address in this list the relay server will be pinged. If the + /// connection uses a local socket with an IP address not in this list the server will + /// always re-connect. + CheckConnection { local_ips: Vec }, + /// Sets this relay as the home relay, or not. + SetHomeRelay(bool), + #[cfg(test)] + GetLocalAddr(oneshot::Sender>), + #[cfg(test)] + PingServer(oneshot::Sender<()>), +} + +/// Messages for the [`ActiveRelayActor`] which should never block. +/// +/// Most messages in the [`ActiveRelayMessage`] enum trigger sending to the relay server, +/// which can be blocking. So the actor may not always be processing that inbox. Messages +/// here are processed immediately. +#[derive(Debug)] +enum ActiveRelayPrioMessage { + /// Returns whether or not this relay can reach the EndpointId. + HasEndpointRoute(EndpointId, oneshot::Sender), +} + +/// Configuration needed to start an [`ActiveRelayActor`]. +#[derive(Debug)] +struct ActiveRelayActorOptions { + url: RelayUrl, + prio_inbox_: mpsc::Receiver, + inbox: mpsc::Receiver, + relay_datagrams_send: mpsc::Receiver, + relay_datagrams_recv: mpsc::Sender, + connection_opts: RelayConnectionOptions, + stop_token: CancellationToken, + metrics: Arc, + my_relay: HomeRelayWatch, +} + +/// Configuration needed to create a connection to a relay server. +#[derive(Debug, Clone)] +struct RelayConnectionOptions { + secret_key: SecretKey, + #[cfg(not(wasm_browser))] + dns_resolver: DnsResolver, + proxy_url: Option, + prefer_ipv6: Arc, + tls_config: rustls::ClientConfig, + auth_token: Option, +} + +/// Possible reasons for a failed relay connection. +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +enum RelayConnectionError { + #[error("Failed to connect to relay server")] + Dial { source: DialError }, + #[error("Failed to handshake with relay server")] + Handshake { source: RunError }, + #[error("Lost connection to relay server")] + Established { source: RunError }, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +enum RunError { + #[error("Send timeout")] + SendTimeout, + #[error("Ping timeout")] + PingTimeout, + #[error("Local IP no longer valid")] + LocalIpInvalid, + #[error("No local address")] + LocalAddrMissing, + #[error("Stream closed by server.")] + StreamClosedServer, + #[error("Client stream read failed")] + ClientStreamRead { + #[error(std_err)] + source: RecvError, + }, + #[error("Client stream write failed")] + ClientStreamWrite { + #[error(std_err)] + source: SendError, + }, +} + +#[allow(missing_docs)] +#[stack_error(derive, add_meta)] +enum DialError { + #[error("timeout (>{timeout:?}) trying to establish a connection")] + Timeout { timeout: Duration }, + #[error("unable to connect")] + Connect { source: ConnectError }, +} + +impl ActiveRelayActor { + fn new(opts: ActiveRelayActorOptions) -> Self { + let ActiveRelayActorOptions { + url, + prio_inbox_: prio_inbox, + inbox, + relay_datagrams_send, + relay_datagrams_recv, + connection_opts, + stop_token, + metrics, + my_relay, + } = opts; + let relay_client_builder = Self::create_relay_builder(url.clone(), connection_opts); + ActiveRelayActor { + prio_inbox, + inbox, + relay_datagrams_recv, + relay_datagrams_send, + url, + relay_client_builder, + is_home_relay: false, + inactive_timeout: Box::pin(time::sleep(RELAY_INACTIVE_CLEANUP_TIME)), + stop_token, + metrics, + my_relay, + } + } + + fn create_relay_builder( + url: RelayUrl, + opts: RelayConnectionOptions, + ) -> relay::client::ClientBuilder { + let RelayConnectionOptions { + secret_key, + #[cfg(not(wasm_browser))] + dns_resolver, + proxy_url, + prefer_ipv6, + tls_config, + auth_token, + } = opts; + + let mut builder = relay::client::ClientBuilder::new( + url, + secret_key, + #[cfg(not(wasm_browser))] + dns_resolver, + ) + .tls_client_config(tls_config) + .address_family_selector(move || prefer_ipv6.load(Ordering::Relaxed)); + if let Some(proxy_url) = proxy_url { + builder = builder.proxy_url(proxy_url); + } + + if let Some(token) = auth_token { + builder = builder.auth_token(token); + } + builder + } + + /// The main actor run loop. + /// + /// Primarily switches between the dialing and connected states. + async fn run(mut self) { + let mut backoff = Self::build_backoff(); + + while let Err(err) = self.run_once().await { + debug!("{err:#}"); + let was_established = matches!(err, RelayConnectionError::Established { .. }); + let last_failure = Some(Arc::new(RelayConnectionFailure::new(err))); + self.my_relay.set_status( + &self.url, + RelayConnectionState::Disconnected { last_failure }, + ); + if !was_established { + // If dialing failed, or if the relay connection failed before we received a pong, + // we wait an exponentially increasing time until we attempt to reconnect again. + let Some(delay) = backoff.next() else { + debug!("retries exceeded"); + break; + }; + debug!("retry in {delay:?}"); + if !self.sleep_backoff(delay).await { + break; + } + } else { + // If the relay connection remained established long enough so that we received a pong + // from the relay server, we reset the backoff and attempt to reconnect immediately. + backoff = Self::build_backoff(); + } + } + debug!("exiting"); + } + + /// Waits out a reconnect backoff delay while still answering priority messages. + /// + /// Other active relays may query [`ActiveRelayPrioMessage::HasEndpointRoute`] on this + /// relay while it is backing off, e.g. when [`RelayActor`] is looking for an existing + /// relay connection to an endpoint. Answering immediately (with `false`, since a + /// disconnected relay cannot have an endpoint route) keeps that lookup from stalling + /// for however long is left of this relay's own unrelated backoff. + /// + /// Returns `false` if the actor should shut down instead of retrying. + async fn sleep_backoff(&mut self, delay: Duration) -> bool { + let sleep = time::sleep(delay); + tokio::pin!(sleep); + loop { + tokio::select! { + biased; + _ = self.stop_token.cancelled() => { + debug!("Shutdown."); + return false; + } + msg = self.prio_inbox.recv() => { + let Some(msg) = msg else { + warn!("Priority inbox closed, shutdown."); + return false; + }; + match msg { + ActiveRelayPrioMessage::HasEndpointRoute(_peer, sender) => { + sender.send(false).ok(); + } + } + } + _ = &mut sleep => return true, + } + } + } + + fn build_backoff() -> impl Backoff { + ExponentialBuilder::new() + .with_min_delay(Duration::from_millis(10)) + .with_max_delay(Duration::from_secs(16)) + .with_jitter() + .without_max_times() + .build() + } + + /// Attempt to connect to the relay, and run the connected actor loop. + /// + /// Returns `Ok(())` if the actor loop should shut down. Returns an error if dialing failed, + /// or if the relay connection failed while connected. In both cases, the connection should + /// be retried with a backoff. + #[allow(clippy::result_large_err)] + async fn run_once(&mut self) -> Result<(), RelayConnectionError> { + self.my_relay + .set_status(&self.url, RelayConnectionState::Connecting); + let client = match self.run_dialing().instrument(info_span!("dialing")).await { + Some(Ok(client)) => client, + Some(Err(err)) => { + self.metrics.relay_conns_failed.inc(); + return Err(e!(RelayConnectionError::Dial, err)); + } + None => return Ok(()), + }; + self.my_relay + .set_status(&self.url, RelayConnectionState::Connected); + self.metrics.relay_conns_success.inc(); + let res = self + .run_connected(client) + .instrument(info_span!("connected")) + .await; + self.metrics.relay_conns_closed.inc(); + res + } + + fn reset_inactive_timeout(&mut self) { + self.inactive_timeout + .as_mut() + .reset(Instant::now() + RELAY_INACTIVE_CLEANUP_TIME); + } + + fn set_home_relay(&mut self, is_home: bool) { + let prev = std::mem::replace(&mut self.is_home_relay, is_home); + if self.is_home_relay != prev { + event!( + target: "iroh::_events::relay::home_changed", + Level::DEBUG, + url = %self.url, + home_relay = self.is_home_relay, + ); + } + } + + /// Actor loop when connecting to the relay server. + /// + /// Returns `None` if the actor needs to shut down. Returns `Some(Ok(client))` when the + /// connection is established, and `Some(Err(err))` if dialing the relay failed. + async fn run_dialing(&mut self) -> Option> { + trace!("Actor loop: connecting to relay."); + + // We regularly flush the relay_datagrams_send queue so it is not full of stale + // packets while reconnecting. Those datagrams are dropped and the QUIC congestion + // controller will have to handle this (DISCO packets do not yet have retry). This + // is not an ideal mechanism, an alternative approach would be to use + // e.g. ConcurrentQueue with force_push, though now you might still send very stale + // packets when eventually connected. So perhaps this is a reasonable compromise. + let mut send_datagram_flush = time::interval(UNDELIVERABLE_DATAGRAM_TIMEOUT); + send_datagram_flush.set_missed_tick_behavior(MissedTickBehavior::Delay); + send_datagram_flush.reset(); // Skip the immediate interval + + let dialing_fut = self.dial_relay(); + tokio::pin!(dialing_fut); + loop { + tokio::select! { + biased; + _ = self.stop_token.cancelled() => { + debug!("Shutdown."); + break None; + } + msg = self.prio_inbox.recv() => { + let Some(msg) = msg else { + warn!("Priority inbox closed, shutdown."); + break None; + }; + match msg { + ActiveRelayPrioMessage::HasEndpointRoute(_peer, sender) => { + sender.send(false).ok(); + } + } + } + res = &mut dialing_fut => { + match res { + Ok(client) => { + break Some(Ok(client)); + } + Err(err) => { + break Some(Err(err)); + } + } + } + msg = self.inbox.recv() => { + let Some(msg) = msg else { + debug!("Inbox closed, shutdown."); + break None; + }; + match msg { + ActiveRelayMessage::SetHomeRelay(is_home) => { + self.set_home_relay(is_home); + } + ActiveRelayMessage::CheckConnection { .. } => {} + #[cfg(test)] + ActiveRelayMessage::GetLocalAddr(sender) => { + sender.send(None).ok(); + } + #[cfg(test)] + ActiveRelayMessage::PingServer(sender) => { + drop(sender); + } + } + } + _ = send_datagram_flush.tick() => { + self.reset_inactive_timeout(); + let mut logged = false; + while self.relay_datagrams_send.try_recv().is_ok() { + if !logged { + debug!(?UNDELIVERABLE_DATAGRAM_TIMEOUT, "Dropping datagrams to send."); + logged = true; + } + } + } + _ = &mut self.inactive_timeout, if !self.is_home_relay => { + debug!(?RELAY_INACTIVE_CLEANUP_TIME, "Inactive, exiting."); + break None; + } + } + } + } + + /// Returns a future which will complete once connected to the relay server. + /// + /// The future only completes once the connection is established and retries + /// connections. It currently does not ever return `Err` as the retries continue + /// forever. + // This is using `impl Future` to return a future without a reference to self. + fn dial_relay(&self) -> impl Future> + use<> { + let client_builder = self.relay_client_builder.clone(); + async move { + match time::timeout(CONNECT_TIMEOUT, client_builder.connect()).await { + Ok(Ok(client)) => Ok(client), + Ok(Err(err)) => Err(e!(DialError::Connect, err)), + Err(_) => Err(e!(DialError::Timeout { + timeout: CONNECT_TIMEOUT + })), + } + } + } + + /// Runs the actor loop when connected to a relay server. + /// + /// Returns `Ok` if the actor needs to shut down. `Err` is returned if the connection + /// to the relay server is lost. + #[allow(clippy::result_large_err)] + async fn run_connected( + &mut self, + client: iroh_relay::client::Client, + ) -> Result<(), RelayConnectionError> { + trace!("Actor loop: connected to relay"); + event!( + target: "iroh::_events::relay::connected", + Level::DEBUG, + url = %self.url, + home_relay = self.is_home_relay, + ); + + let (mut client_stream, client_sink) = client.split(); + let mut client_sink = client_sink.sink_map_err(|e| e!(RunError::ClientStreamWrite, e)); + + let mut state = ConnectedRelayState { + ping_tracker: PingTracker::default(), + endpoints_present: BTreeSet::new(), + last_packet_src: None, + pong_pending: None, + established: false, + rate_limited: false, + #[cfg(test)] + test_pong: None, + }; + + // A buffer to pass through multiple datagrams at once as an optimisation. + let mut send_datagrams_buf = Vec::with_capacity(SEND_DATAGRAM_BATCH_SIZE); + + // Regularly send pings so we know the connection is healthy. + // The first ping will be sent immediately. + let mut ping_interval = time::interval(PING_INTERVAL); + ping_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + + let res = loop { + if let Some(data) = state.pong_pending.take() { + let fut = client_sink.send(ClientToRelayMsg::Pong(data)); + self.run_sending(fut, &mut state, &mut client_stream) + .await?; + } + tokio::select! { + biased; + _ = self.stop_token.cancelled() => { + debug!("Shutdown."); + break Ok(()); + } + msg = self.prio_inbox.recv() => { + let Some(msg) = msg else { + warn!("Priority inbox closed, shutdown."); + break Ok(()); + }; + match msg { + ActiveRelayPrioMessage::HasEndpointRoute(peer, sender) => { + let has_peer = state.endpoints_present.contains(&peer); + sender.send(has_peer).ok(); + } + } + } + _ = state.ping_tracker.timeout() => { + break Err(e!(RunError::PingTimeout)); + } + _ = ping_interval.tick() => { + let data = state.ping_tracker.new_ping(); + let fut = client_sink.send(ClientToRelayMsg::Ping(data)); + self.run_sending(fut, &mut state, &mut client_stream).await?; + } + msg = self.inbox.recv() => { + let Some(msg) = msg else { + warn!("Inbox closed, shutdown."); + break Ok(()); + }; + match msg { + ActiveRelayMessage::SetHomeRelay(is_home) => { + self.set_home_relay(is_home); + // We are in `run_connected`, so if we just became the home + // relay, publish `Connected` (the `RelayActor` only sets + // `Connecting` on the URL change since it cannot know our + // actual state). + if is_home { + self.my_relay + .set_status(&self.url, RelayConnectionState::Connected); + } + } + ActiveRelayMessage::CheckConnection { local_ips } => { + match client_stream.local_addr() { + Some(addr) if local_ips.contains(&addr.ip()) => { + let data = state.ping_tracker.new_ping(); + let fut = client_sink.send(ClientToRelayMsg::Ping(data)); + self.run_sending(fut, &mut state, &mut client_stream).await?; + } + Some(_) => break Err(e!(RunError::LocalIpInvalid)), + None => break Err(e!(RunError::LocalAddrMissing)), + } + } + #[cfg(test)] + ActiveRelayMessage::GetLocalAddr(sender) => { + let addr = client_stream.local_addr(); + sender.send(addr).ok(); + } + #[cfg(test)] + ActiveRelayMessage::PingServer(sender) => { + let data = rand::random(); + state.test_pong = Some((data, sender)); + let fut = client_sink.send(ClientToRelayMsg::Ping(data)); + self.run_sending(fut, &mut state, &mut client_stream).await?; + } + } + } + count = self.relay_datagrams_send.recv_many( + &mut send_datagrams_buf, + SEND_DATAGRAM_BATCH_SIZE, + ) => { + if count == 0 { + warn!("Datagram inbox closed, shutdown"); + break Ok(()); + }; + self.reset_inactive_timeout(); + // TODO(frando): can we avoid the clone here? + let metrics = self.metrics.clone(); + let packet_iter = send_datagrams_buf.drain(..).map(|item| { + metrics.send_relay.inc_by(item.datagrams.contents.len() as _); + Ok(ClientToRelayMsg::Datagrams { + dst_endpoint_id: item.remote_endpoint, + datagrams: item.datagrams, + }) + }); + let mut packet_stream = n0_future::stream::iter(packet_iter); + let fut = client_sink.send_all(&mut packet_stream); + self.run_sending(fut, &mut state, &mut client_stream).await?; + } + msg = client_stream.next() => { + let Some(msg) = msg else { + break Err(e!(RunError::StreamClosedServer)); + }; + match msg { + Ok(msg) => { + self.handle_relay_msg(msg, &mut state); + // reset the ping timer, we have just received a message + ping_interval.reset(); + }, + Err(err) => break Err(e!(RunError::ClientStreamRead, err)), + } + } + _ = &mut self.inactive_timeout, if !self.is_home_relay => { + debug!("Inactive for {RELAY_INACTIVE_CLEANUP_TIME:?}, exiting (running)."); + break Ok(()); + } + } + }; + + if res.is_ok() + && let Err(err) = client_sink.close().await + { + debug!("Failed to close client sink gracefully: {err:#}"); + } + + res.map_err(|err| state.map_err(err)) + } + + fn handle_relay_msg(&mut self, msg: RelayToClientMsg, state: &mut ConnectedRelayState) { + match msg { + RelayToClientMsg::Datagrams { + remote_endpoint_id, + datagrams, + } => { + trace!(len = datagrams.contents.len(), "received msg"); + // If this is a new sender, register a route for this peer. + if state + .last_packet_src + .as_ref() + .map(|p| *p != remote_endpoint_id) + .unwrap_or(true) + { + // Avoid map lookup with high throughput single peer. + state.last_packet_src = Some(remote_endpoint_id); + state.endpoints_present.insert(remote_endpoint_id); + } + + if let Err(err) = self.relay_datagrams_recv.try_send(RelayRecvDatagram { + url: self.url.clone(), + src: remote_endpoint_id, + datagrams, + }) { + warn!("Dropping received relay packet: {err:#}"); + } + } + RelayToClientMsg::EndpointGone(endpoint_id) => { + state.endpoints_present.remove(&endpoint_id); + } + RelayToClientMsg::Ping(data) => state.pong_pending = Some(data), + RelayToClientMsg::Pong(data) => { + #[cfg(test)] + { + if let Some((expected_data, sender)) = state.test_pong.take() { + if data == expected_data { + sender.send(()).ok(); + } else { + state.test_pong = Some((expected_data, sender)); + } + } + } + state.ping_tracker.pong_received(data); + state.established = true; + } + RelayToClientMsg::Status(status) => match status { + Status::Healthy => info!("Relay server reports: {status}"), + Status::RateLimited => { + warn!("{status}"); + // The relay sends this at most once per connection, but do not rely + // on the remote for the metric to count connections. + if !state.rate_limited { + state.rate_limited = true; + self.metrics.relay_conns_ratelimited.inc(); + } + } + _ => warn!("Relay server reports problem: {status}"), + }, + RelayToClientMsg::Restarting { .. } => { + trace!("Ignoring {msg:?}") + } + // Deprecated variants, kept for backwards compatibility with older relay protocol versions. + RelayToClientMsg::Health { problem } => { + warn!("Relay server reports problem: {problem}"); + } + _ => unreachable!( + "got unknown RelayToClientMsg but iroh is released in sync with iroh-relay" + ), + } + } + + /// Run the actor main loop while sending to the relay server. + /// + /// While sending the actor should not read any inboxes which will give it more things + /// to send to the relay server. + /// + /// # Returns + /// + /// On `Err` the relay connection should be disconnected. An `Ok` return means either + /// the actor should shut down, consult the [`ActiveRelayActor::stop_token`] and + /// [`ActiveRelayActor::inactive_timeout`] for this, or the send was successful. + #[instrument(name = "tx", skip_all)] + #[allow(clippy::result_large_err)] + async fn run_sending( + &mut self, + sending_fut: impl Future>, + state: &mut ConnectedRelayState, + client_stream: &mut iroh_relay::client::ClientStream, + ) -> Result<(), RelayConnectionError> { + // we use the same time as for our ping interval + let send_timeout = PING_INTERVAL; + + let mut timeout = pin!(time::sleep(send_timeout)); + let mut sending_fut = pin!(sending_fut); + let res = loop { + tokio::select! { + biased; + _ = self.stop_token.cancelled() => { + break Ok(()); + } + _ = &mut timeout => { + break Err(e!(RunError::SendTimeout)); + } + msg = self.prio_inbox.recv() => { + let Some(msg) = msg else { + warn!("Priority inbox closed, shutdown."); + break Ok(()); + }; + match msg { + ActiveRelayPrioMessage::HasEndpointRoute(peer, sender) => { + let has_peer = state.endpoints_present.contains(&peer); + sender.send(has_peer).ok(); + } + } + } + res = &mut sending_fut => { + match res { + Ok(_) => break Ok(()), + Err(err) => break Err(err), + } + } + _ = state.ping_tracker.timeout() => { + break Err(e!(RunError::PingTimeout)); + } + // No need to read the inbox or datagrams to send. + msg = client_stream.next() => { + let Some(msg) = msg else { + break Err(e!(RunError::StreamClosedServer)); + }; + match msg { + Ok(msg) => self.handle_relay_msg(msg, state), + Err(err) => break Err(e!(RunError::ClientStreamRead, err)), + } + } + _ = &mut self.inactive_timeout, if !self.is_home_relay => { + debug!("Inactive for {RELAY_INACTIVE_CLEANUP_TIME:?}, exiting (sending)."); + break Ok(()); + } + } + }; + res.map_err(|err| state.map_err(err)) + } +} + +/// Shared state when the [`ActiveRelayActor`] is connected to a relay server. +/// +/// Common state between [`ActiveRelayActor::run_connected`] and +/// [`ActiveRelayActor::run_sending`]. +#[derive(Debug)] +struct ConnectedRelayState { + /// Tracks pings we have sent, awaits pong replies. + ping_tracker: PingTracker, + /// Endpoints which are reachable via this relay server. + endpoints_present: BTreeSet, + /// The [`EndpointId`] from whom we received the last packet. + /// + /// This is to avoid a slower lookup in the [`ConnectedRelayState::endpoints_present`] map + /// when we are only communicating to a single remote endpoint. + last_packet_src: Option, + /// A pong we need to send ASAP. + pong_pending: Option<[u8; 8]>, + /// Whether the connection is to be considered established. + /// + /// This is set to `true` once a pong was received from the server. + established: bool, + /// Whether the relay reported that it is rate-limiting this connection. + /// + /// Used to count each affected connection only once. + rate_limited: bool, + #[cfg(test)] + test_pong: Option<([u8; 8], oneshot::Sender<()>)>, +} + +impl ConnectedRelayState { + fn map_err(&self, error: RunError) -> RelayConnectionError { + if self.established { + e!(RelayConnectionError::Established, error) + } else { + e!(RelayConnectionError::Handshake, error) + } + } +} + +pub(super) enum RelayActorMessage { + MaybeCloseRelaysOnRebind, + NetworkChange { + report: Report, + }, + /// Trigger an immediate health check on all relay connections. + /// + /// Sent after a major network change to detect broken connections faster + /// using RTT-based timeouts instead of the default 5s ping timeout. + CheckConnectionAfterNetworkChange, +} + +#[derive(Debug, Clone)] +pub(crate) struct RelaySendItem { + /// The destination for the datagrams. + pub(crate) remote_endpoint: EndpointId, + /// The home relay of the remote endpoint. + pub(crate) url: RelayUrl, + /// One or more datagrams to send. + pub(crate) datagrams: Datagrams, +} + +pub(super) struct RelayActor { + config: Config, + /// Queue on which to put received datagrams. + relay_datagram_recv_queue: mpsc::Sender, + /// The actors managing each currently used relay server. + /// + /// These actors will exit when they have any inactivity. Otherwise they will keep + /// trying to maintain a connection to the relay server as needed. + active_relays: BTreeMap, + /// The tasks for the [`ActiveRelayActor`]s in `active_relays` above. + active_relay_tasks: JoinSet<()>, + cancel_token: CancellationToken, +} + +#[derive(Debug, Clone)] +pub(crate) struct Config { + pub my_relay: HomeRelayWatch, + pub secret_key: SecretKey, + #[cfg(not(wasm_browser))] + pub dns_resolver: DnsResolver, + /// Proxy + pub proxy_url: Option, + /// If the last net_report report, reports IPv6 to be available. + pub ipv6_reported: Arc, + pub tls_config: rustls::ClientConfig, + pub metrics: Arc, + /// Per-relay configuration. Consulted when starting a connection to + /// look up the auth token and any future per-relay options. + pub relay_map: RelayMap, +} + +/// Connection state of the home relay. +/// +/// Published via [`HomeRelayWatch`] so that [`Endpoint::online`] and the public +/// [`Endpoint::home_relay_status`] watcher can observe the connection state. +/// This type is `pub(crate)`; the public surface lives on +/// [`crate::endpoint::RelayStatus`], and this enum is intentionally free to +/// evolve without affecting the public API. +/// +/// [`Endpoint::online`]: crate::Endpoint::online +/// [`Endpoint::home_relay_status`]: crate::Endpoint::home_relay_status +#[derive(Debug, Clone)] +pub(crate) enum RelayConnectionState { + /// Dialing or performing the relay handshake. + Connecting, + /// Connected and handshaked. + Connected, + /// Not connected. Either the connection was lost after having been + /// established, or an attempt to connect failed. + /// + /// `last_failure` carries the most recent connection failure, if any. The + /// initial transition into this state (before any attempt has produced + /// an error) carries `None`. + /// + /// The `Arc` is compared by pointer identity: each new failure produces + /// a fresh allocation, so the watcher fires on every new error. + Disconnected { + last_failure: Option>, + }, +} + +impl RelayConnectionState { + pub(crate) fn is_connected(&self) -> bool { + matches!(self, Self::Connected) + } + + pub(crate) fn last_failure(&self) -> Option<&RelayConnectionFailure> { + match self { + Self::Disconnected { last_failure } => last_failure.as_deref(), + _ => None, + } + } +} + +impl PartialEq for RelayConnectionState { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Connecting, Self::Connecting) | (Self::Connected, Self::Connected) => true, + (Self::Disconnected { last_failure: a }, Self::Disconnected { last_failure: b }) => { + match (a, b) { + (None, None) => true, + (Some(a), Some(b)) => Arc::ptr_eq(a, b), + _ => false, + } + } + _ => false, + } + } +} + +impl Eq for RelayConnectionState {} + +/// A failed attempt to connect to a relay server, or a lost connection. +/// +/// Contains the type-erased error, together with whatever we could classify from it +/// while the concrete error type was still at hand. +#[derive(Debug)] +pub(crate) struct RelayConnectionFailure { + error: AnyError, + auth_denied_reason: Option, +} + +impl RelayConnectionFailure { + fn new(error: RelayConnectionError) -> Self { + Self { + auth_denied_reason: auth_denied_reason(&error).map(ToOwned::to_owned), + error: AnyError::from(error), + } + } + + /// Returns the type-erased error. + pub(crate) fn error(&self) -> &AnyError { + &self.error + } + + /// Returns the reason if the relay server denied our authentication. + pub(crate) fn auth_denied_reason(&self) -> Option<&str> { + self.auth_denied_reason.as_deref() + } +} + +/// Returns the reason if `error` was caused by the relay server denying our authentication. +fn auth_denied_reason(error: &RelayConnectionError) -> Option<&str> { + match error { + RelayConnectionError::Dial { + source: + DialError::Connect { + source: + ConnectError::Handshake { + source: handshake::Error::ServerDeniedAuth { reason, .. }, + .. + }, + .. + }, + .. + } => Some(reason), + _ => None, + } +} + +/// Shared watchable for the home relay URL and connection status. +/// +/// Owned by [`RelayActor`] and cloned into each [`ActiveRelayActor`]. +/// +/// # Write discipline +/// +/// The [`RelayActor`] writes the URL (via [`Self::set`] and [`Self::clear`]). +/// Each [`ActiveRelayActor`] updates only the status (via [`Self::set_status`]), +/// which guards against stale writes: if another relay has become home since this actor +/// was designated, the write is silently dropped. +#[derive(Debug, Clone)] +pub(crate) struct HomeRelayWatch { + inner: Watchable>, +} + +impl Default for HomeRelayWatch { + fn default() -> Self { + Self { + inner: Watchable::new(None), + } + } +} + +impl HomeRelayWatch { + /// Set the home relay URL and status. Used by [`RelayActor`] on relay changes. + fn set(&self, url: RelayUrl, state: RelayConnectionState) { + let _ = self.inner.set(Some(RelayStatus::new(url, state))); + } + + /// Clear the home relay (no preferred relay). Used by [`RelayActor`]. + fn clear(&self) { + let _ = self.inner.set(None); + } + + /// Update the status, but only if `url` is still the current home relay. + /// + /// This is the only write method [`ActiveRelayActor`] should use. It prevents a + /// demoted actor from overwriting a newer home relay's status: the [`RelayActor`] + /// updates the URL in the watchable *before* sending `SetHomeRelay(false)`, so by + /// the time the old actor tries to write, the URL no longer matches. + fn set_status(&self, url: &RelayUrl, state: RelayConnectionState) { + if self.inner.get().as_ref().map(RelayStatus::url) == Some(url) { + let _ = self.inner.set(Some(RelayStatus::new(url.clone(), state))); + } + } + + fn get(&self) -> Option { + self.inner.get() + } + + pub(crate) fn watch(&self) -> n0_watcher::Direct> { + self.inner.watch() + } +} + +impl RelayActor { + pub(super) fn new( + config: Config, + relay_datagram_recv_queue: mpsc::Sender, + cancel_token: CancellationToken, + ) -> Self { + Self { + config, + relay_datagram_recv_queue, + active_relays: Default::default(), + active_relay_tasks: JoinSet::new(), + cancel_token, + } + } + + pub(super) async fn run( + mut self, + mut receiver: mpsc::Receiver, + mut datagram_send_channel: mpsc::Receiver, + ) { + // When this future is present, it is sending pending datagrams to an + // ActiveRelayActor. We can not process further datagrams during this time. + let mut datagram_send_fut = std::pin::pin!(MaybeFuture::None); + + loop { + tokio::select! { + biased; + _ = self.cancel_token.cancelled() => { + debug!("shutting down"); + break; + } + Some(res) = self.active_relay_tasks.join_next() => { + log_active_relay_task_result(res); + self.reap_active_relays(); + } + msg = receiver.recv() => { + let Some(msg) = msg else { + debug!("Inbox dropped, shutting down."); + break; + }; + let cancel_token = self.cancel_token.child_token(); + cancel_token.run_until_cancelled(self.handle_msg(msg)).await; + } + // Only poll for new datagrams if we are not blocked on sending them. + item = datagram_send_channel.recv(), if datagram_send_fut.is_none() => { + let Some(item) = item else { + debug!("Datagram send channel dropped, shutting down."); + break; + }; + let token = self.cancel_token.child_token(); + if let Some(Some(fut)) = token.run_until_cancelled( + self.try_send_datagram(item) + ).await { + datagram_send_fut.as_mut().set_future(fut); + } + } + // Only poll this future if it is in use. + _ = &mut datagram_send_fut, if datagram_send_fut.is_some() => { + datagram_send_fut.as_mut().set_none(); + } + } + } + + // try shutdown + if time::timeout(Duration::from_secs(3), self.close_all_active_relays()) + .await + .is_err() + { + warn!("Failed to shut down all ActiveRelayActors"); + } + } + + async fn handle_msg(&mut self, msg: RelayActorMessage) { + match msg { + RelayActorMessage::NetworkChange { report } => { + self.on_network_change(report).await; + } + RelayActorMessage::MaybeCloseRelaysOnRebind => { + self.maybe_close_relays_on_rebind().await; + } + RelayActorMessage::CheckConnectionAfterNetworkChange => { + self.check_connection_after_network_change().await; + } + } + } + + /// Sends datagrams to the correct [`ActiveRelayActor`], or returns a future. + /// + /// If the datagram can not be sent immediately, because the destination channel is + /// full, a future is returned that will complete once the datagrams have been sent to + /// the [`ActiveRelayActor`]. + async fn try_send_datagram( + &mut self, + item: RelaySendItem, + ) -> Option + use<>> { + let url = item.url.clone(); + let handle = self + .active_relay_handle_for_endpoint(&item.url, &item.remote_endpoint) + .await; + match handle.datagrams_send_queue.try_send(item) { + Ok(()) => None, + Err(mpsc::error::TrySendError::Closed(_)) => { + warn!(?url, "Dropped datagram(s): ActiveRelayActor closed."); + None + } + Err(mpsc::error::TrySendError::Full(item)) => { + let sender = handle.datagrams_send_queue.clone(); + let fut = async move { + if sender.send(item).await.is_err() { + warn!(?url, "Dropped datagram(s): ActiveRelayActor closed."); + } + }; + Some(fut) + } + } + } + + async fn on_network_change(&mut self, report: Report) { + let prev = self.config.my_relay.get(); + let prev_url = prev.as_ref().map(RelayStatus::url); + if report.preferred_relay.as_ref() == prev_url { + // No change. + return; + } + + if let Some(relay_url) = report.preferred_relay { + self.config.metrics.relay_home_change.inc(); + + // On change, notify all currently connected relay servers and + // start connecting to our home relay if we are not already. + info!("home is now relay {}, was {:?}", relay_url, prev_url); + // Publish `Connecting` initially. If an `ActiveRelayActor` already + // exists for this URL it will republish its actual status (e.g. + // `Connected`) when it receives the `SetHomeRelay(true)` message + // sent below. + self.config + .my_relay + .set(relay_url.clone(), RelayConnectionState::Connecting); + self.set_home_relay(relay_url).await; + } else { + self.config.my_relay.clear(); + } + } + + async fn set_home_relay(&mut self, home_url: RelayUrl) { + let home_url_ref = &home_url; + n0_future::join_all(self.active_relays.iter().map(|(url, handle)| async move { + let is_preferred = url == home_url_ref; + handle + .inbox_addr + .send(ActiveRelayMessage::SetHomeRelay(is_preferred)) + .await + .ok() + })) + .await; + // Ensure we have an ActiveRelayActor for the current home relay. + self.active_relay_handle(home_url); + } + + /// Returns the handle for the [`ActiveRelayActor`] to reach `remote_endpoint`. + /// + /// The endpoint is expected to be reachable on `url`, but if no [`ActiveRelayActor`] for + /// `url` exists but another existing [`ActiveRelayActor`] already knows about the endpoint, + /// that other endpoint is used. + async fn active_relay_handle_for_endpoint( + &mut self, + url: &RelayUrl, + remote_endpoint: &EndpointId, + ) -> ActiveRelayHandle { + if let Some(handle) = self.active_relays.get(url) { + return handle.clone(); + } + + let mut found_relay: Option = None; + // If we don't have an open connection to the remote endpoint's home relay, see if + // we have an open connection to a relay endpoint where we'd heard from that peer + // already. E.g. maybe they dialed our home relay recently. + { + // Futures which return Some(RelayUrl) if the relay knows about the remote endpoint. + let check_futs = self.active_relays.iter().map(|(url, handle)| async move { + let (tx, rx) = oneshot::channel(); + handle + .prio_inbox_addr + .send(ActiveRelayPrioMessage::HasEndpointRoute( + *remote_endpoint, + tx, + )) + .await + .ok(); + match rx.await { + Ok(true) => Some(url.clone()), + _ => None, + } + }); + let mut futures = FuturesUnorderedBounded::from_iter(check_futs); + while let Some(maybe_url) = futures.next().await { + if maybe_url.is_some() { + found_relay = maybe_url; + break; + } + } + } + let url = found_relay.unwrap_or(url.clone()); + self.active_relay_handle(url) + } + + /// Returns the handle of the [`ActiveRelayActor`]. + fn active_relay_handle(&mut self, url: RelayUrl) -> ActiveRelayHandle { + match self.active_relays.get(&url) { + Some(e) => e.clone(), + None => { + let handle = self.start_active_relay(url.clone()); + if Some(&url) == self.config.my_relay.get().as_ref().map(RelayStatus::url) + && let Err(err) = handle + .inbox_addr + .try_send(ActiveRelayMessage::SetHomeRelay(true)) + { + error!("Home relay not set, send to new actor failed: {err:#}."); + } + self.active_relays.insert(url, handle.clone()); + self.log_active_relay(); + handle + } + } + } + + fn start_active_relay(&mut self, url: RelayUrl) -> ActiveRelayHandle { + debug!(?url, "Adding relay connection"); + + let auth_token = self + .config + .relay_map + .get(&url) + .and_then(|cfg| cfg.auth_token.clone()); + let connection_opts = RelayConnectionOptions { + secret_key: self.config.secret_key.clone(), + #[cfg(not(wasm_browser))] + dns_resolver: self.config.dns_resolver.clone(), + proxy_url: self.config.proxy_url.clone(), + prefer_ipv6: self.config.ipv6_reported.clone(), + tls_config: self.config.tls_config.clone(), + auth_token, + }; + + // TODO: Replace 64 with PER_CLIENT_SEND_QUEUE_DEPTH once that's unused + let (send_datagram_tx, send_datagram_rx) = mpsc::channel(64); + let (prio_inbox_tx, prio_inbox_rx) = mpsc::channel(32); + let (inbox_tx, inbox_rx) = mpsc::channel(64); + let span = info_span!("active-relay", %url); + let opts = ActiveRelayActorOptions { + url, + prio_inbox_: prio_inbox_rx, + inbox: inbox_rx, + relay_datagrams_send: send_datagram_rx, + relay_datagrams_recv: self.relay_datagram_recv_queue.clone(), + connection_opts, + stop_token: self.cancel_token.child_token(), + metrics: self.config.metrics.clone(), + my_relay: self.config.my_relay.clone(), + }; + let actor = ActiveRelayActor::new(opts); + self.active_relay_tasks.spawn( + async move { + actor.run().await; + } + .instrument(span), + ); + let handle = ActiveRelayHandle { + prio_inbox_addr: prio_inbox_tx, + inbox_addr: inbox_tx, + datagrams_send_queue: send_datagram_tx, + }; + self.log_active_relay(); + handle + } + + /// Triggers an immediate health check on all relay connections after a network change. + async fn check_connection_after_network_change(&mut self) { + self.send_check_connection().await; + } + + /// Closes the relay connections not originating from a local IP address. + /// + /// Called in response to a rebind, any relay connection originating from an address + /// that's not known to be currently a local IP address should be closed. All the other + /// relay connections are pinged. + async fn maybe_close_relays_on_rebind(&mut self) { + self.send_check_connection().await; + self.log_active_relay(); + } + + /// Sends a [`ActiveRelayMessage::CheckConnection`] to all active relays with current + /// local IPs. + async fn send_check_connection(&self) { + #[cfg(not(wasm_browser))] + let ifs = interfaces::State::new().await; + #[cfg(not(wasm_browser))] + let local_ips: Vec<_> = ifs + .interfaces + .values() + .flat_map(|netif| netif.addrs()) + .map(|ipnet| ipnet.addr()) + .collect(); + // In browsers, we don't have this information. This will do the right thing + // in the ActiveRelayActor, though. + #[cfg(wasm_browser)] + let local_ips = Vec::new(); + let send_futs = self.active_relays.values().map(|handle| { + let local_ips = local_ips.clone(); + async move { + handle + .inbox_addr + .send(ActiveRelayMessage::CheckConnection { local_ips }) + .await + .ok(); + } + }); + n0_future::join_all(send_futs).await; + } + + /// Cleans up [`ActiveRelayActor`]s which have stopped running. + fn reap_active_relays(&mut self) { + self.active_relays + .retain(|_url, handle| !handle.inbox_addr.is_closed()); + + // Make sure home relay exists + if let Some(status) = self.config.my_relay.get() { + self.active_relay_handle(status.url().clone()); + } + self.log_active_relay(); + } + + /// Stops all [`ActiveRelayActor`]s and awaits for them to finish. + async fn close_all_active_relays(&mut self) { + self.cancel_token.cancel(); + let mut tasks = std::mem::take(&mut self.active_relay_tasks); + // Drain instead of `join_all`, which panics on any `JoinError`. + while let Some(res) = tasks.join_next().await { + log_active_relay_task_result(res); + } + + self.log_active_relay(); + } + + fn log_active_relay(&self) { + debug!("{} active relay conns{}", self.active_relays.len(), { + let mut s = String::new(); + if !self.active_relays.is_empty() { + s += ":"; + for endpoint in self.active_relay_sorted() { + s += &format!(" relay-{endpoint}"); + } + } + s + }); + } + + fn active_relay_sorted(&self) -> impl Iterator + use<> { + let mut ids: Vec<_> = self.active_relays.keys().cloned().collect(); + ids.sort(); + + ids.into_iter() + } +} + +/// Reports how one [`ActiveRelayActor`] task ended, in the run loop and on shutdown. +fn log_active_relay_task_result(res: Result<(), JoinError>) { + match res { + Ok(()) => (), + Err(err) if err.is_panic() => error!("ActiveRelayActor task panicked: {err:#?}"), + Err(err) if err.is_cancelled() => error!("ActiveRelayActor cancelled: {err:#?}"), + Err(err) => error!("ActiveRelayActor failed: {err:#?}"), + } +} + +/// Handle to one [`ActiveRelayActor`]. +#[derive(Debug, Clone)] +struct ActiveRelayHandle { + prio_inbox_addr: mpsc::Sender, + inbox_addr: mpsc::Sender, + datagrams_send_queue: mpsc::Sender, +} + +/// A single datagram received from a relay server. +/// +/// This could be either a QUIC or DISCO packet. +#[derive(Debug)] +pub(crate) struct RelayRecvDatagram { + pub(crate) url: RelayUrl, + pub(crate) src: EndpointId, + pub(crate) datagrams: Datagrams, +} + +#[cfg(test)] +mod tests { + use std::{ + sync::{Arc, atomic::AtomicBool}, + time::Duration, + }; + + use iroh_base::{EndpointId, RelayUrl, SecretKey}; + use iroh_relay::{ + PingTracker, + protos::relay::Datagrams, + tls::{CaTlsConfig, default_provider}, + }; + use n0_error::{AnyError as Error, Result, StackResultExt, StdResultExt}; + use n0_tracing_test::traced_test; + use tokio::sync::{mpsc, oneshot}; + use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle}; + use tracing::{Instrument, info, info_span}; + + use super::{ + ActiveRelayActor, ActiveRelayActorOptions, ActiveRelayMessage, ActiveRelayPrioMessage, + Config, RELAY_INACTIVE_CLEANUP_TIME, RelayActor, RelayConnectionOptions, RelayMap, + RelayRecvDatagram, RelaySendItem, UNDELIVERABLE_DATAGRAM_TIMEOUT, + }; + use crate::{dns::DnsResolver, metrics::SocketMetrics, test_utils}; + + /// Abort stands in for the runtime drop that triggers this in the wild: + /// `join_next` yields a cancelled `JoinError` either way. + #[tokio::test] + #[traced_test] + async fn close_all_active_relays_survives_a_cancelled_task() { + let (relay_datagram_recv_queue, _recv_rx) = mpsc::channel(1); + let config = Config { + my_relay: Default::default(), + secret_key: SecretKey::from_bytes(&[0u8; 32]), + dns_resolver: DnsResolver::new(), + proxy_url: None, + ipv6_reported: Arc::new(AtomicBool::new(false)), + tls_config: CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"), + metrics: Default::default(), + relay_map: RelayMap::empty(), + }; + let mut actor = + RelayActor::new(config, relay_datagram_recv_queue, CancellationToken::new()); + + let handle = actor.active_relay_tasks.spawn(std::future::pending::<()>()); + handle.abort(); + + // Panicked with `join_all`. + actor.close_all_active_relays().await; + + assert!(actor.active_relay_tasks.is_empty()); + assert!(logs_contain("ActiveRelayActor cancelled")); + } + + /// Starts a new [`ActiveRelayActor`]. + #[allow(clippy::too_many_arguments)] + fn start_active_relay_actor( + secret_key: SecretKey, + stop_token: CancellationToken, + url: RelayUrl, + prio_inbox_rx: mpsc::Receiver, + inbox_rx: mpsc::Receiver, + relay_datagrams_send: mpsc::Receiver, + relay_datagrams_recv: mpsc::Sender, + metrics: Arc, + span: tracing::Span, + ) -> AbortOnDropHandle<()> { + let opts = ActiveRelayActorOptions { + url, + prio_inbox_: prio_inbox_rx, + inbox: inbox_rx, + relay_datagrams_send, + relay_datagrams_recv, + connection_opts: RelayConnectionOptions { + secret_key, + dns_resolver: DnsResolver::new(), + proxy_url: None, + prefer_ipv6: Arc::new(AtomicBool::new(true)), + tls_config: CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"), + auth_token: None, + }, + stop_token, + metrics, + my_relay: Default::default(), + }; + let task = tokio::spawn(ActiveRelayActor::new(opts).run().instrument(span)); + AbortOnDropHandle::new(task) + } + + /// Starts an [`ActiveRelayActor`] as an "iroh echo endpoint". + /// + /// This actor will connect to the relay server, pretending to be an iroh endpoint, and echo + /// back any datagram it receives from the relay. This is used by the + /// [`ActiveRelayActor`] under test to check connectivity works. + fn start_echo_endpoint(relay_url: RelayUrl) -> (EndpointId, AbortOnDropHandle<()>) { + let secret_key = SecretKey::from_bytes(&[8u8; 32]); + let (recv_datagram_tx, mut recv_datagram_rx) = mpsc::channel(16); + let (send_datagram_tx, send_datagram_rx) = mpsc::channel(16); + let (prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8); + let (inbox_tx, inbox_rx) = mpsc::channel(16); + let cancel_token = CancellationToken::new(); + let actor_task = start_active_relay_actor( + secret_key.clone(), + cancel_token.clone(), + relay_url.clone(), + prio_inbox_rx, + inbox_rx, + send_datagram_rx, + recv_datagram_tx, + Default::default(), + info_span!("echo-endpoint"), + ); + let echo_task = tokio::spawn({ + let relay_url = relay_url.clone(); + async move { + loop { + let datagram = recv_datagram_rx.recv().await; + if let Some(recv) = datagram { + let RelayRecvDatagram { + url: _, + src, + datagrams, + } = recv; + info!(from = %src.fmt_short(), "Received datagram"); + let send = RelaySendItem { + remote_endpoint: src, + url: relay_url.clone(), + datagrams, + }; + send_datagram_tx.send(send).await.ok(); + } + } + } + .instrument(info_span!("echo-task")) + }); + let echo_task = AbortOnDropHandle::new(echo_task); + let supervisor_task = tokio::spawn(async move { + let _guard = cancel_token.drop_guard(); + // move the inboxes here so it is not dropped, as this stops the actor. + let _prio_inbox_tx = prio_inbox_tx; + let _inbox_tx = inbox_tx; + tokio::select! { + biased; + _ = actor_task => (), + _ = echo_task => (), + }; + }); + let supervisor_task = AbortOnDropHandle::new(supervisor_task); + (secret_key.public(), supervisor_task) + } + + /// Sends a message to the echo endpoint, receives the response. + /// + /// This takes care of retry and timeout. Because we don't know when both the + /// endpoint-under-test and the echo endpoint will be ready and datagrams aren't queued to send + /// forever, we have to retry a few times. + async fn send_recv_echo( + item: RelaySendItem, + tx: &mpsc::Sender, + rx: &mut mpsc::Receiver, + ) -> Result<()> { + tokio::time::timeout(Duration::from_secs(10), async move { + loop { + let res = tokio::time::timeout(UNDELIVERABLE_DATAGRAM_TIMEOUT, async { + tx.send(item.clone()).await.std_context("send item")?; + let RelayRecvDatagram { + url: _, + src: _, + datagrams, + } = rx.recv().await.unwrap(); + + assert_eq!(datagrams, item.datagrams); + + Ok::<_, Error>(()) + }) + .await; + if res.is_ok() { + break; + } + } + }) + .await + .expect("overall timeout exceeded"); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_active_relay_reconnect() -> Result { + let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?; + let (peer_endpoint, _echo_endpoint_task) = start_echo_endpoint(relay_url.clone()); + + let secret_key = SecretKey::from_bytes(&[1u8; 32]); + let (datagram_recv_tx, mut datagram_recv_rx) = mpsc::channel(16); + let (send_datagram_tx, send_datagram_rx) = mpsc::channel(16); + let (_prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8); + let (inbox_tx, inbox_rx) = mpsc::channel(16); + let cancel_token = CancellationToken::new(); + let metrics = Arc::new(SocketMetrics::default()); + let task = start_active_relay_actor( + secret_key, + cancel_token.clone(), + relay_url.clone(), + prio_inbox_rx, + inbox_rx, + send_datagram_rx, + datagram_recv_tx.clone(), + metrics.clone(), + info_span!("actor-under-test"), + ); + + // Send a datagram to our echo endpoint. + info!("first echo"); + let hello_send_item = RelaySendItem { + remote_endpoint: peer_endpoint, + url: relay_url.clone(), + datagrams: Datagrams::from(b"hello"), + }; + send_recv_echo( + hello_send_item.clone(), + &send_datagram_tx, + &mut datagram_recv_rx, + ) + .await?; + + // Now ask to check the connection, triggering a ping but no reconnect. + let (tx, rx) = oneshot::channel(); + inbox_tx + .send(ActiveRelayMessage::GetLocalAddr(tx)) + .await + .std_context("send get local addr msg")?; + + let local_addr = rx + .await + .std_context("wait for local addr msg")? + .context("no local addr")?; + info!(?local_addr, "check connection with addr"); + inbox_tx + .send(ActiveRelayMessage::CheckConnection { + local_ips: vec![local_addr.ip()], + }) + .await + .std_context("send check connection message")?; + + // Sync the ActiveRelayActor. Ping blocks it and we want to be sure it has handled + // another inbox message before continuing. + let (tx, rx) = oneshot::channel(); + inbox_tx + .send(ActiveRelayMessage::GetLocalAddr(tx)) + .await + .std_context("send get local addr msg")?; + rx.await.std_context("recv send local addr msg")?; + + // Echo should still work. + info!("second echo"); + send_recv_echo( + hello_send_item.clone(), + &send_datagram_tx, + &mut datagram_recv_rx, + ) + .await?; + + // Now ask to check the connection, this will reconnect without pinging because we + // do not supply any "valid" local IP addresses. + info!("check connection"); + inbox_tx + .send(ActiveRelayMessage::CheckConnection { + local_ips: Vec::new(), + }) + .await + .std_context("send check connection msg")?; + + // Give some time to reconnect, mostly to sort logs rather than functional. + tokio::time::sleep(Duration::from_millis(10)).await; + + // Echo should still work. + info!("third echo"); + send_recv_echo( + hello_send_item.clone(), + &send_datagram_tx, + &mut datagram_recv_rx, + ) + .await?; + + // Shut down the actor. + cancel_token.cancel(); + task.await.std_context("wait for task to finish")?; + + // The actor connected once at startup and once more after the connection check + // failed. + assert_eq!(metrics.relay_conns_success.get(), 2); + assert_eq!( + metrics.relay_conns_closed.get(), + 2, + "the connections are counted as closed once the actor stops" + ); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_active_relay_inactive() -> Result { + let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?; + + let secret_key = SecretKey::from_bytes(&[1u8; 32]); + let (datagram_recv_tx, _datagram_recv_rx) = mpsc::channel(16); + let (_send_datagram_tx, send_datagram_rx) = mpsc::channel(16); + let (_prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8); + let (inbox_tx, inbox_rx) = mpsc::channel(16); + let cancel_token = CancellationToken::new(); + let metrics = Arc::new(SocketMetrics::default()); + let mut task = start_active_relay_actor( + secret_key, + cancel_token.clone(), + relay_url, + prio_inbox_rx, + inbox_rx, + send_datagram_rx, + datagram_recv_tx, + metrics.clone(), + info_span!("actor-under-test"), + ); + + // Wait until the actor is connected to the relay server. + tokio::time::timeout(Duration::from_secs(5), async { + loop { + let (tx, rx) = oneshot::channel(); + inbox_tx.send(ActiveRelayMessage::PingServer(tx)).await.ok(); + if tokio::time::timeout(Duration::from_millis(200), rx) + .await + .map(|resp| resp.is_ok()) + .unwrap_or_default() + { + break; + } + } + }) + .await + .std_context("timeout")?; + + assert_eq!(metrics.relay_conns_success.get(), 1); + assert_eq!( + metrics.relay_conns_closed.get(), + 0, + "the connection is only counted as closed once it is gone" + ); + + // We now have an idling ActiveRelayActor. If we advance time just a little it + // should stay alive. + info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME / 2"); + tokio::time::pause(); + tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME / 2).await; + tokio::time::resume(); + + assert!( + tokio::time::timeout(Duration::from_millis(100), &mut task) + .await + .is_err(), + "actor task terminated" + ); + + // If we advance time a lot it should finish. + info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME"); + tokio::time::pause(); + tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME).await; + tokio::time::resume(); + + // We resume time for these timeouts, as there's actual I/O happening, + // for example closing the TCP stream, so we actually need the tokio + // runtime to idle a bit while the kernel is doing its thing. + assert!( + tokio::time::timeout(Duration::from_secs(1), task) + .await + .is_ok(), + "actor task still running" + ); + + // The actor may have reconnected while we advanced time, because a ping to the relay + // server can time out while time is frozen, so we do not assert an exact count here. + assert_eq!( + metrics.relay_conns_closed.get(), + metrics.relay_conns_success.get(), + "all connections are counted as closed once the actor stops" + ); + + cancel_token.cancel(); + Ok(()) + } + + #[tokio::test] + async fn test_ping_tracker() { + tokio::time::pause(); + let mut tracker = PingTracker::default(); + + let ping0 = tracker.new_ping(); + + let res = tokio::time::timeout(Duration::from_secs(1), tracker.timeout()).await; + assert!(res.is_err(), "no ping timeout has elapsed yet"); + + tracker.pong_received(ping0); + let res = tokio::time::timeout(Duration::from_secs(10), tracker.timeout()).await; + assert!(res.is_err(), "ping completed before timeout"); + + let _ping1 = tracker.new_ping(); + + let res = tokio::time::timeout(Duration::from_secs(10), tracker.timeout()).await; + assert!(res.is_ok(), "ping timeout should have happened"); + + let _ping2 = tracker.new_ping(); + + tokio::time::sleep(Duration::from_secs(10)).await; + let res = tokio::time::timeout(Duration::from_millis(1), tracker.timeout()).await; + assert!(res.is_ok(), "ping timeout happened in the past"); + + let res = tokio::time::timeout(Duration::from_secs(10), tracker.timeout()).await; + assert!(res.is_err(), "ping timeout should only happen once"); + } + + #[tokio::test] + #[traced_test] + async fn test_prio_inbox_answered_during_backoff() { + tokio::time::pause(); + let secret_key = SecretKey::from_bytes(&[1u8; 32]); + let peer = SecretKey::from_bytes(&[2u8; 32]).public(); + let url: RelayUrl = "https://relay.invalid".parse().unwrap(); + let (prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8); + let (_inbox_tx, inbox_rx) = mpsc::channel(16); + let (_send_datagram_tx, send_datagram_rx) = mpsc::channel(16); + let (recv_datagram_tx, _recv_datagram_rx) = mpsc::channel(16); + let opts = ActiveRelayActorOptions { + url, + prio_inbox_: prio_inbox_rx, + inbox: inbox_rx, + relay_datagrams_send: send_datagram_rx, + relay_datagrams_recv: recv_datagram_tx, + connection_opts: RelayConnectionOptions { + secret_key, + dns_resolver: DnsResolver::new(), + proxy_url: None, + prefer_ipv6: Arc::new(AtomicBool::new(true)), + tls_config: CaTlsConfig::insecure_skip_verify() + .client_config(default_provider()) + .expect("infallible"), + auth_token: None, + }, + stop_token: CancellationToken::new(), + metrics: Default::default(), + my_relay: Default::default(), + }; + let mut actor = ActiveRelayActor::new(opts); + + let backoff = tokio::spawn(async move { + let keep_running = actor.sleep_backoff(Duration::from_secs(10)).await; + (actor, keep_running) + }); + + // Query the actor while it waits out the backoff delay. The reply must + // arrive well before the delay elapses; before the backoff wait + // serviced the priority inbox, this reply only arrived after the + // remaining backoff delay (up to 16s). + let (tx, rx) = oneshot::channel(); + prio_inbox_tx + .send(ActiveRelayPrioMessage::HasEndpointRoute(peer, tx)) + .await + .expect("actor alive"); + let reply = tokio::time::timeout(Duration::from_secs(1), rx) + .await + .expect("no reply within 1s of a 10s backoff") + .expect("sender dropped"); + assert!(!reply, "a disconnected relay has no endpoint routes"); + + // The backoff itself still runs to completion. + let (_actor, keep_running) = backoff.await.expect("backoff task panicked"); + assert!(keep_running, "backoff should complete normally"); + } + + #[test] + fn test_home_relay_watch_url_guard() { + use super::{HomeRelayWatch, RelayConnectionState}; + use crate::endpoint::RelayStatus; + + let watch = HomeRelayWatch::default(); + let a: RelayUrl = "https://a.example.com".parse().unwrap(); + let b: RelayUrl = "https://b.example.com".parse().unwrap(); + + // Actor A becomes home and connects + watch.set(a.clone(), RelayConnectionState::Connecting); + watch.set_status(&a, RelayConnectionState::Connected); + assert_eq!( + watch.get(), + Some(RelayStatus::new(a.clone(), RelayConnectionState::Connected)), + ); + + // RelayActor migrates home to B + watch.set(b.clone(), RelayConnectionState::Connecting); + + // Old actor A tries to write -- rejected because URL changed + watch.set_status( + &a, + RelayConnectionState::Disconnected { last_failure: None }, + ); + assert_eq!( + watch.get(), + Some(RelayStatus::new( + b.clone(), + RelayConnectionState::Connecting + )), + ); + + // Actor B writes normally + watch.set_status(&b, RelayConnectionState::Connected); + assert_eq!( + watch.get(), + Some(RelayStatus::new(b, RelayConnectionState::Connected)), + ); + } +} diff --git a/vendor/iroh/src/test_utils.rs b/vendor/iroh/src/test_utils.rs new file mode 100644 index 0000000..8609f68 --- /dev/null +++ b/vendor/iroh/src/test_utils.rs @@ -0,0 +1,576 @@ +//! Internal utilities to support testing. +use std::{net::Ipv4Addr, sync::Arc}; + +use iroh_base::RelayUrl; +use iroh_relay::{ + RelayConfig, RelayMap, RelayQuicConfig, + server::{ + AllowAll, CertConfig, DynAccessControl, QuicConfig, RelayConfig as RelayServerConfig, + Server, ServerConfig, SpawnError, TlsConfig, + }, +}; +use tokio::sync::oneshot; + +pub use self::{dns_and_pkarr_servers::DnsPkarrServer, qlog::QlogFileGroup}; + +mod qlog; +#[cfg(feature = "unstable-custom-transports")] +pub mod test_transport; + +/// A drop guard to clean up test infrastructure. +/// +/// After dropping the test infrastructure will asynchronously shutdown and release its +/// resources. +// Nightly sees the sender as dead code currently, but we only rely on Drop of the +// sender. +#[derive(Debug)] +#[allow(dead_code)] +pub struct CleanupDropGuard(pub(crate) oneshot::Sender<()>); + +/// Runs a relay server with QUIC enabled suitable for tests. +/// +/// The returned `Url` is the url of the relay server in the returned [`RelayMap`]. +/// When dropped, the returned [`Server`] does will stop running. +pub async fn run_relay_server() -> Result<(RelayMap, RelayUrl, Server), SpawnError> { + run_relay_server_with(true).await +} + +/// Runs a relay server. +/// +/// If `quic` is set to `true`, it will make the appropriate [`QuicConfig`] from the generated tls certificates and run the quic server at a random free port. +/// +/// +/// The return value is similar to [`run_relay_server`]. +pub async fn run_relay_server_with(quic: bool) -> Result<(RelayMap, RelayUrl, Server), SpawnError> { + run_relay_server_with_access(quic, Arc::new(AllowAll)).await +} + +/// Runs a relay server with a custom access control. +/// +/// See [`run_relay_server_with`] for details on `quic`. +pub async fn run_relay_server_with_access( + quic: bool, + access: Arc, +) -> Result<(RelayMap, RelayUrl, Server), SpawnError> { + let (_certs, server_config) = iroh_relay::server::testing::self_signed_tls_certs_and_config(); + + let tls = TlsConfig::new( + (Ipv4Addr::LOCALHOST, 0), + CertConfig::Manual { server_config }, + ); + + let mut relay = RelayServerConfig::new((Ipv4Addr::LOCALHOST, 0)); + relay.tls = Some(tls); + relay.key_cache_capacity = Some(1024); + relay.access = access; + + let mut config = ServerConfig::default(); + config.relay = Some(relay); + config.quic = quic.then(|| QuicConfig::new((Ipv4Addr::LOCALHOST, 0))); + + let server = Server::spawn(config).await?; + let url: RelayUrl = format!("https://{}", server.https_addr().expect("configured")) + .parse() + .expect("invalid relay url"); + + let quic = server + .quic_addr() + .map(|addr| RelayQuicConfig::new(addr.port())); + let n: RelayMap = RelayConfig::new(url.clone(), quic).into(); + Ok((n, url, server)) +} + +mod dns_and_pkarr_servers { + use std::{net::SocketAddr, time::Duration}; + + use iroh_base::EndpointId; + use url::Url; + + use super::CleanupDropGuard; + use crate::{ + address_lookup::{ + DnsAddressLookup, DnsAddressLookupBuilder, PkarrPublisher, PkarrPublisherBuilder, + }, + dns::DnsResolver, + endpoint::presets::Preset, + test_utils::{ + dns_server::run_dns_server, pkarr_dns_state::State, pkarr_relay::run_pkarr_relay, + }, + }; + + /// Handle and drop guard for test DNS and Pkarr servers. + /// + /// Once the struct is dropped the servers will shut down. + #[derive(Debug)] + pub struct DnsPkarrServer { + /// The endpoint origin domain. + endpoint_origin: String, + /// The shared state of the DNS and Pkarr servers. + state: State, + /// The socket address of the DNS server. + nameserver: SocketAddr, + /// The HTTP URL of the Pkarr server. + pkarr_url: Url, + _dns_drop_guard: CleanupDropGuard, + _pkarr_drop_guard: CleanupDropGuard, + } + + impl DnsPkarrServer { + /// Run DNS and Pkarr servers on localhost. + pub async fn run() -> std::io::Result { + Self::run_with_origin("dns.iroh.test".to_string()).await + } + + /// Run DNS and Pkarr servers on localhost with the specified `endpoint_origin` domain. + pub async fn run_with_origin(endpoint_origin: String) -> std::io::Result { + let state = State::new(endpoint_origin.clone()); + let (nameserver, dns_drop_guard) = run_dns_server(state.clone()).await?; + let (pkarr_url, pkarr_drop_guard) = run_pkarr_relay(state.clone()).await?; + Ok(Self { + endpoint_origin, + nameserver, + pkarr_url, + state, + _dns_drop_guard: dns_drop_guard, + _pkarr_drop_guard: pkarr_drop_guard, + }) + } + + /// Returns a [`Preset`] to apply the DNS address lookup and Pkarr Publisher to an endpoint. + pub fn preset(&self) -> impl Preset { + struct DnsPkarrPreset { + dns_address_lookup: DnsAddressLookupBuilder, + pkarr_address_publisher: PkarrPublisherBuilder, + dns_resolver: DnsResolver, + } + + let preset = DnsPkarrPreset { + dns_address_lookup: DnsAddressLookup::builder(self.endpoint_origin.clone()), + pkarr_address_publisher: PkarrPublisher::builder(self.pkarr_url.clone()), + dns_resolver: self.dns_resolver(), + }; + impl Preset for DnsPkarrPreset { + fn apply(self, builder: crate::endpoint::Builder) -> crate::endpoint::Builder { + builder + .addr_filter(crate::address_lookup::AddrFilter::relay_only()) + .address_lookup(self.dns_address_lookup) + .address_lookup(self.pkarr_address_publisher) + .dns_resolver(self.dns_resolver) + } + } + preset + } + + /// Create a [`DnsResolver`] configured to use the test DNS server. + pub fn dns_resolver(&self) -> DnsResolver { + DnsResolver::with_nameserver(self.nameserver) + } + + /// Returns the PKARR server HTTP URL. + pub fn pkarr_url(&self) -> &Url { + &self.pkarr_url + } + + /// Wait until a Pkarr announce for an endpoint is published to the server. + /// + /// If `timeout` elapses an error is returned. + pub async fn on_endpoint( + &self, + endpoint_id: &EndpointId, + timeout: Duration, + ) -> std::io::Result<()> { + self.state.on_endpoint(endpoint_id, timeout).await + } + } +} + +pub(crate) mod dns_server { + use std::{ + future::Future, + net::{Ipv4Addr, SocketAddr}, + }; + + use n0_future::future::Boxed as BoxFuture; + use simple_dns::{Packet, PacketFlag}; + use tokio::{net::UdpSocket, sync::oneshot}; + use tracing::{debug, error, warn}; + + use super::CleanupDropGuard; + + /// Trait used by [`run_dns_server`] for answering DNS queries. + pub(crate) trait QueryHandler: Send + Sync + 'static { + fn resolve( + &self, + query: &Packet<'_>, + reply: &mut Packet<'static>, + ) -> impl Future> + Send; + } + + pub(crate) type QueryHandlerFunction = Box< + dyn Fn(&Packet<'_>, &mut Packet<'static>) -> BoxFuture> + + Send + + Sync + + 'static, + >; + + impl QueryHandler for QueryHandlerFunction { + fn resolve( + &self, + query: &Packet<'_>, + reply: &mut Packet<'static>, + ) -> impl Future> + Send { + (self)(query, reply) + } + } + + /// Run a DNS server. + /// + /// Must pass a [`QueryHandler`] that answers queries. + pub(crate) async fn run_dns_server( + resolver: impl QueryHandler, + ) -> std::io::Result<(SocketAddr, CleanupDropGuard)> { + let bind_addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 0)); + let socket = UdpSocket::bind(bind_addr).await?; + let bound_addr = socket.local_addr()?; + let s = TestDnsServer { socket, resolver }; + let (tx, mut rx) = oneshot::channel(); + tokio::task::spawn(async move { + tokio::select! { + _ = &mut rx => { + debug!("shutting down dns server"); + } + res = s.run() => { + if let Err(e) = res { + error!("error running dns server {e:?}"); + } + } + } + }); + Ok((bound_addr, CleanupDropGuard(tx))) + } + + struct TestDnsServer { + resolver: R, + socket: UdpSocket, + } + + impl TestDnsServer { + async fn run(self) -> std::io::Result<()> { + let mut buf = [0; 1450]; + loop { + let res = self.socket.recv_from(&mut buf).await; + let (len, from) = res?; + if let Err(err) = self.handle_datagram(from, &buf[..len]).await { + warn!(?err, %from, "failed to handle incoming datagram"); + } + } + } + + async fn handle_datagram( + &self, + from: SocketAddr, + buf: &[u8], + ) -> Result<(), Box> { + let packet = Packet::parse(buf).map_err(std::io::Error::other)?; + debug!(questions = ?packet.questions, %from, "received query"); + let mut reply = Packet::new_reply(packet.id()); + reply.set_flags(PacketFlag::RECURSION_DESIRED | PacketFlag::RECURSION_AVAILABLE); + // Echo the question section, as a real resolver does. The client + // validates that the response question matches its query. + reply.questions = packet + .questions + .iter() + .map(|q| q.clone().into_owned()) + .collect(); + self.resolver.resolve(&packet, &mut reply).await?; + debug!(?reply, %from, "send reply"); + let buf = reply.build_bytes_vec().map_err(std::io::Error::other)?; + let len = self.socket.send_to(&buf, from).await?; + assert_eq!(len, buf.len(), "failed to send complete packet"); + Ok(()) + } + } +} + +pub(crate) mod pkarr_relay { + use std::{ + future::IntoFuture, + net::{Ipv4Addr, SocketAddr}, + }; + + use axum::{ + Router, + extract::{Path, State}, + response::IntoResponse, + routing::put, + }; + use bytes::Bytes; + use iroh_base::EndpointId; + use tokio::sync::oneshot; + use tracing::{debug, error, warn}; + use url::Url; + + use super::CleanupDropGuard; + use crate::test_utils::pkarr_dns_state::State as AppState; + + pub(crate) async fn run_pkarr_relay( + state: AppState, + ) -> std::io::Result<(Url, CleanupDropGuard)> { + let bind_addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 0)); + let app = Router::new() + .route("/pkarr/{key}", put(pkarr_put)) + .with_state(state); + let listener = tokio::net::TcpListener::bind(bind_addr).await?; + let bound_addr = listener.local_addr()?; + let url: Url = format!("http://{bound_addr}/pkarr") + .parse() + .expect("valid url"); + + let (tx, mut rx) = oneshot::channel(); + tokio::spawn(async move { + let serve = axum::serve(listener, app); + tokio::select! { + _ = &mut rx => { + debug!("shutting down pkarr server"); + } + res = serve.into_future() => { + if let Err(e) = res { + error!("pkarr server error: {e:?}"); + } + } + } + }); + Ok((url, CleanupDropGuard(tx))) + } + + async fn pkarr_put( + State(state): State, + Path(key): Path, + body: Bytes, + ) -> Result { + let key = EndpointId::from_z32(&key).map_err(std::io::Error::other)?; + let signed_packet = iroh_dns::pkarr::SignedPacket::from_relay_payload(&key, &body) + .map_err(std::io::Error::other)?; + let _updated = state.upsert(signed_packet)?; + Ok(http::StatusCode::NO_CONTENT) + } + + #[derive(Debug)] + struct AppError(std::io::Error); + impl> From for AppError { + fn from(value: T) -> Self { + Self(value.into()) + } + } + impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + warn!(err = ?self, "request failed"); + (http::StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response() + } + } +} + +pub(crate) mod pkarr_dns_state { + use std::{ + collections::{HashMap, hash_map}, + future::Future, + sync::{Arc, Mutex}, + time::Duration, + }; + + use iroh_base::EndpointId; + use iroh_dns::{IROH_TXT_NAME, endpoint_info::EndpointInfo, pkarr::SignedPacket}; + use simple_dns::{ + CLASS, Name, Packet, ResourceRecord, + rdata::{RData, TXT}, + }; + use tracing::debug; + + use crate::test_utils::dns_server::QueryHandler; + + #[derive(Debug, Clone)] + pub(crate) struct State { + packets: Arc>>, + origin: String, + notify: Arc, + } + + impl State { + pub(crate) fn new(origin: String) -> Self { + Self { + packets: Default::default(), + origin, + notify: Arc::new(tokio::sync::Notify::new()), + } + } + + pub(crate) fn on_update(&self) -> tokio::sync::futures::Notified<'_> { + self.notify.notified() + } + + pub(crate) async fn on_endpoint( + &self, + endpoint: &EndpointId, + timeout: Duration, + ) -> std::io::Result<()> { + let timeout = tokio::time::sleep(timeout); + tokio::pin!(timeout); + while self.get(endpoint, |p| { + let endpoint_info = p + .as_ref() + .and_then(|p| EndpointInfo::from_pkarr_signed_packet(p).ok()); + debug!("got info {:#?}", endpoint_info); + // Wait until we have endpoint info with actual addressing data, + // not just an empty initial publish. + endpoint_info + .as_ref() + .is_none_or(|info| info.data.addrs().next().is_none()) + }) { + tokio::select! { + _ = &mut timeout => return Err(std::io::Error::other("timeout")), + _ = self.on_update() => {} + } + } + Ok(()) + } + + pub(crate) fn upsert(&self, signed_packet: SignedPacket) -> std::io::Result { + let endpoint_id = signed_packet.public_key(); + let mut map = self.packets.lock().expect("poisoned"); + let updated = match map.entry(endpoint_id) { + hash_map::Entry::Vacant(e) => { + e.insert(signed_packet); + true + } + hash_map::Entry::Occupied(mut e) => { + if signed_packet.more_recent_than(e.get()) { + e.insert(signed_packet); + true + } else { + false + } + } + }; + if updated { + self.notify.notify_waiters(); + } + Ok(updated) + } + + /// Returns a mutex guard, do not hold over await points + pub(crate) fn get(&self, endpoint_id: &EndpointId, cb: F) -> T + where + F: FnOnce(Option<&mut SignedPacket>) -> T, + { + let mut map = self.packets.lock().expect("poisoned"); + let packet = map.get_mut(endpoint_id); + cb(packet) + } + + pub(crate) fn resolve_dns( + &self, + query: &Packet<'_>, + reply: &mut Packet<'static>, + ttl: u32, + ) -> std::io::Result<()> { + for question in &query.questions { + let domain_name = question.qname.to_string(); + let Some(endpoint_id) = endpoint_id_from_domain_name(&domain_name) else { + continue; + }; + + self.get(&endpoint_id, |packet| { + if let Some(packet) = packet { + let endpoint_info = EndpointInfo::from_pkarr_signed_packet(packet) + .map_err(std::io::Error::other)?; + for record in + endpoint_info_to_dns_records(&endpoint_info, &self.origin, ttl) + { + reply.answers.push(record); + } + } + Ok::<_, std::io::Error>(()) + })?; + } + Ok(()) + } + } + + impl QueryHandler for State { + fn resolve( + &self, + query: &Packet<'_>, + reply: &mut Packet<'static>, + ) -> impl Future> + Send { + const TTL: u32 = 30; + let res = self.resolve_dns(query, reply, TTL); + std::future::ready(res) + } + } + + /// Parses a [`EndpointId`] from a DNS domain name. + /// + /// Splits the domain name into labels on each dot. Expects the first label to be + /// [`IROH_TXT_NAME`] and the second label to be a z32 encoded [`EndpointId`]. Ignores + /// subsequent labels. + /// + /// Returns a [`EndpointId`] if parsed successfully, otherwise `None`. + fn endpoint_id_from_domain_name(name: &str) -> Option { + let mut labels = name.split("."); + let label = labels.next()?; + if label != IROH_TXT_NAME { + return None; + } + let label = labels.next()?; + let endpoint_id = EndpointId::from_z32(label).ok()?; + Some(endpoint_id) + } + + /// Converts an [`EndpointInfo`] into simple-dns [`ResourceRecord`]s. + fn endpoint_info_to_dns_records( + endpoint_info: &EndpointInfo, + origin: &str, + ttl: u32, + ) -> Vec> { + let txt_strings = endpoint_info.to_txt_strings(); + to_dns_records(txt_strings, endpoint_info.endpoint_id, origin, ttl) + } + + /// Converts TXT strings to simple-dns [`ResourceRecord`]s. + fn to_dns_records( + txt_strings: Vec, + endpoint_id: EndpointId, + origin: &str, + ttl: u32, + ) -> Vec> { + let name = format!("{IROH_TXT_NAME}.{}.{origin}", endpoint_id.to_z32()); + let name = Name::new(&name).expect("invalid name").into_owned(); + txt_strings + .into_iter() + .map(move |s| { + let txt = TXT::new() + .with_string(&s) + .expect("invalid TXT string") + .into_owned(); + let rdata = RData::TXT(txt); + ResourceRecord::new(name.clone(), CLASS::IN, ttl, rdata) + }) + .collect() + } + + #[cfg(test)] + mod tests { + use iroh_base::EndpointId; + use n0_error::Result; + + #[test] + fn test_endpoint_id_from_domain_name() -> Result { + let name = "_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link."; + let endpoint_id = super::endpoint_id_from_domain_name(name); + let expected: EndpointId = + "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18".parse()?; + assert_eq!(endpoint_id, Some(expected)); + Ok(()) + } + } +} diff --git a/vendor/iroh/src/test_utils/qlog.rs b/vendor/iroh/src/test_utils/qlog.rs new file mode 100644 index 0000000..321b0be --- /dev/null +++ b/vendor/iroh/src/test_utils/qlog.rs @@ -0,0 +1,87 @@ +//! Utils for emitting qlog files from iroh endpoint. + +use std::path::{Path, PathBuf}; +#[cfg(feature = "qlog")] +use std::sync::Arc; + +use n0_error::Result; +#[cfg(feature = "qlog")] +use n0_future::time::Instant; + +#[cfg(feature = "qlog")] +use crate::endpoint::QlogFileFactory; +use crate::endpoint::QuicTransportConfig; + +/// Builder to create one or more related qlog configs. +/// +/// This struct is available independently of feature flags, but if the "qlog" feature is not enabled +/// it does not do anything. +#[derive(Debug, Clone)] +pub struct QlogFileGroup { + #[cfg(feature = "qlog")] + directory: PathBuf, + #[cfg(feature = "qlog")] + title: String, + #[cfg(feature = "qlog")] + start: Instant, +} + +impl QlogFileGroup { + /// Creates a new [`QlogFileGroup`] that is only enabled if feature flags and environment variables match. + /// + /// The qlog files will be written to `CARGO_MANIFEST_DIR/qlog`. + /// + /// The [`QlogFileGroup`] can be used independent of feature flags, but it will only emit qlog files + /// if the "qlog" feature is enabled and the environment variable IROH_TEST_QLOG is set to 1. + pub fn from_env(title: impl ToString) -> Self { + let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("qlog"); + Self::new(directory, title) + } + + /// Creates a new [`QlogFileGroup`] that writes qlog files to the specified directory. + /// + /// The [`QlogFileGroup`] can be used independent of feature flags, but it will only emit qlog files + /// if the "qlog" feature is enabled and the environment variable IROH_TEST_QLOG is set to 1. + pub fn new(directory: impl AsRef, title: impl ToString) -> Self { + #[cfg(not(feature = "qlog"))] + let this = { + let _ = (directory, title); + Self {} + }; + + #[cfg(feature = "qlog")] + let this = Self { + title: title.to_string(), + directory: directory.as_ref().to_owned(), + start: Instant::now(), + }; + + this + } + + /// Creates a [`QuicTransportConfig`] that emits qlog files, if enabled. + /// + /// If the "qlog" feature is enabled, and the environment variable IROH_TEST_QLOG is set, + /// this returns a transport config that writes qlog configs to the configured output directory. + /// Otherwise, a default transport config is returned. + pub fn create(&self, name: impl ToString) -> Result { + #[cfg(not(feature = "qlog"))] + { + let _name = name; + Ok(QuicTransportConfig::default()) + } + #[cfg(feature = "qlog")] + { + let mut builder = QuicTransportConfig::builder(); + + if std::env::var("IROH_TEST_QLOG").is_ok() { + let prefix = format!("{}.{}", self.title, name.to_string()); + let factory = QlogFileFactory::new(self.directory.clone()) + .with_prefix(prefix) + .with_start_instant(self.start.into()); + builder = builder.qlog_factory(Arc::new(factory)); + } + Ok(builder.build()) + } + } +} diff --git a/vendor/iroh/src/test_utils/test_transport.rs b/vendor/iroh/src/test_utils/test_transport.rs new file mode 100644 index 0000000..a06b68b --- /dev/null +++ b/vendor/iroh/src/test_utils/test_transport.rs @@ -0,0 +1,744 @@ +//! In-memory test transport for testing. +//! +//! This module provides [`TestNetwork`] and [`TestTransport`] for testing +//! using in-memory channels instead of real network transports. + +use std::{ + collections::BTreeMap, + io, + sync::{Arc, Mutex}, + task::Poll, +}; + +use bytes::Bytes; +use iroh_base::{CustomAddr, EndpointId, TransportAddr}; +use tokio::sync::mpsc::{self, error::TrySendError}; +use tracing::info; + +use crate::{ + address_lookup::{AddressLookup, EndpointData, EndpointInfo, Item}, + endpoint::{ + Builder, + presets::Preset, + transports::{CustomEndpoint, CustomSender, CustomTransport, RecvInfo, Transmit}, + }, +}; + +/// The transport ID used by [`TestNetwork`]. +/// +/// See `TRANSPORTS.md` for the registry of transport IDs. +pub const TEST_TRANSPORT_ID: u64 = 0x20; + +/// An outgoing packet that can be sent across channels. +#[derive(Debug, Clone)] +pub(crate) struct Packet { + pub(crate) data: Bytes, + pub(crate) from: CustomAddr, +} + +/// A test transport for use with [`TestNetwork`]. +/// +/// Implements [`CustomTransport`] and [`CustomEndpoint`] for testing. +#[derive(Debug, Clone)] +pub struct TestTransport { + id: EndpointId, + id_watchable: n0_watcher::Watchable>, + network: TestNetwork, +} + +impl Preset for Arc { + /// Configures the builder with this transport and the network's address lookup. + /// + /// # Example + /// + /// ```ignore + /// let network = TestNetwork::new(); + /// let transport = network.create_transport(secret_key.public())?; + /// let ep = Endpoint::builder() + /// .secret_key(secret_key) + /// .preset(transport) + /// .bind() + /// .await?; + /// ``` + fn apply(self, builder: Builder) -> Builder { + builder + .add_custom_transport(self.clone()) + .address_lookup(self.network.address_lookup()) + } +} + +/// A simulated network for testing custom transports. +/// +/// This allows creating multiple [`TestTransport`] instances that can communicate +/// with each other through in-memory channels. +/// +/// # Example +/// +/// ```ignore +/// use iroh::test_utils::custom_transport::TestNetwork; +/// +/// let network = TestNetwork::new(); +/// let transport1 = network.create_transport(endpoint_id1)?; +/// let transport2 = network.create_transport(endpoint_id2)?; +/// // transport1 and transport2 can now communicate via the network +/// ``` +#[derive(Debug, Clone, Default)] +pub struct TestNetwork { + inner: Arc>, +} + +impl TestNetwork { + /// Creates a new empty test network. + pub fn new() -> Self { + Self::default() + } + + /// Creates an address lookup service for this network. + pub fn address_lookup(&self) -> impl AddressLookup { + TestAddrLookup { + network: self.clone(), + } + } + + /// Creates a new test transport for the given endpoint ID. + /// + /// Returns an error if the ID already exists in the network. + pub fn create_transport(&self, id: EndpointId) -> io::Result> { + let id_custom = to_custom_addr(id); + let mut guard = self.inner.lock().expect("poisoned"); + if guard.channels.contains_key(&id) { + return Err(io::Error::other("endpoint ID already exists in network")); + } + guard.channels.insert(id, mpsc::channel(256)); + drop(guard); + Ok(Arc::new(TestTransport { + id_watchable: n0_watcher::Watchable::new(vec![id_custom]), + network: self.clone(), + id, + })) + } +} + +#[derive(Debug)] +struct TestAddrLookup { + network: TestNetwork, +} + +#[derive(Debug, Default)] +struct TestNetworkInner { + channels: BTreeMap, mpsc::Receiver)>, +} + +impl AddressLookup for TestAddrLookup { + fn publish(&self, _data: &EndpointData) {} + + fn resolve( + &self, + endpoint_id: EndpointId, + ) -> Option>> { + if self + .network + .inner + .lock() + .expect("poisoned") + .channels + .contains_key(&endpoint_id) + { + Some(Box::pin(n0_future::stream::once(Ok(Item::new( + EndpointInfo { + endpoint_id, + data: EndpointData::from_iter([TransportAddr::Custom(CustomAddr::from_parts( + TEST_TRANSPORT_ID, + endpoint_id.as_bytes(), + ))]), + }, + "test discovery", + None, + ))))) + } else { + None + } + } +} + +#[derive(Debug, Clone)] +struct TestSender { + id: EndpointId, + network: TestNetwork, +} + +/// Converts an endpoint ID to a custom address for this test transport. +pub fn to_custom_addr(endpoint: EndpointId) -> CustomAddr { + CustomAddr::from((TEST_TRANSPORT_ID, &endpoint.as_bytes()[..])) +} + +fn try_parse_custom_addr(addr: &CustomAddr) -> io::Result { + if addr.id() != TEST_TRANSPORT_ID { + return Err(io::Error::other("unexpected transport id")); + } + let key_bytes: &[u8; 32] = addr + .data() + .try_into() + .map_err(|_| io::Error::other("wrong key length"))?; + EndpointId::from_bytes(key_bytes).map_err(|_| io::Error::other("KeyParseError")) +} + +impl TestSender { + fn send_sync(&self, dst: &CustomAddr, packets: Vec) -> io::Result<()> { + let to_id = try_parse_custom_addr(dst)?; + let guard = self.network.inner.lock().expect("poisoned"); + let (s, _) = guard + .channels + .get(&to_id) + .ok_or_else(|| io::Error::other("Unknown endpoint"))?; + for packet in packets { + let len = packet.data.len(); + match s.try_send(packet) { + Ok(_) => info!( + "send {} -> {}: sent {} bytes", + self.id.fmt_short(), + to_id.fmt_short(), + len + ), + Err(TrySendError::Full(_)) => info!( + "send {} -> {}: dropped {} bytes", + self.id.fmt_short(), + to_id.fmt_short(), + len + ), + Err(TrySendError::Closed(_)) => return Err(io::Error::other("channel closed")), + } + } + Ok(()) + } + + fn split(&self, transmit: &Transmit) -> impl Iterator { + let from = to_custom_addr(self.id); + let segment_size = transmit.segment_size.unwrap_or(transmit.contents.len()); + transmit + .contents + .chunks(segment_size) + .map(move |slice| Packet { + from: from.clone(), + data: Bytes::copy_from_slice(slice), + }) + } +} + +impl CustomSender for TestSender { + fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool { + addr.id() == TEST_TRANSPORT_ID + } + + fn poll_send( + &self, + _cx: &mut std::task::Context, + dst: &CustomAddr, + _src: Option<&CustomAddr>, + transmit: &Transmit<'_>, + ) -> Poll> { + let packets = self.split(transmit).collect(); + Poll::Ready(self.send_sync(dst, packets)) + } +} + +impl CustomTransport for TestTransport { + fn bind(&self) -> io::Result> { + Ok(Box::new(self.clone())) + } +} + +impl CustomEndpoint for TestTransport { + fn watch_local_addrs(&self) -> n0_watcher::Direct> { + self.id_watchable.watch() + } + + fn create_sender(&self) -> Arc { + Arc::new(TestSender { + id: self.id, + network: self.network.clone(), + }) + } + + fn poll_recv( + &mut self, + cx: &mut std::task::Context, + bufs: &mut [io::IoSliceMut<'_>], + metas: &mut [noq_udp::RecvMeta], + recv_infos: &mut [RecvInfo], + ) -> Poll> { + assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + assert_eq!( + bufs.len(), + recv_infos.len(), + "non matching bufs & recv_infos" + ); + let n = bufs.len(); + if n == 0 { + return Poll::Ready(Ok(0)); + } + let mut guard = self.network.inner.lock().expect("poisoned"); + let Some((_, r)) = guard.channels.get_mut(&self.id) else { + info!("me: {} not found in channels", self.id.fmt_short()); + return Poll::Ready(Ok(0)); + }; + let mut packets = Vec::new(); + match r.poll_recv_many(cx, &mut packets, n) { + Poll::Pending => return Poll::Pending, + Poll::Ready(0) => return Poll::Ready(Err(io::Error::other("channel closed"))), + Poll::Ready(n) => n, + }; + let mut count = 0; + for (i, packet) in packets.into_iter().enumerate() { + let meta = &mut metas[i]; + let buf = &mut bufs[i]; + let recv_info = &mut recv_infos[i]; + if buf.len() < packet.data.len() { + break; + } + let from = try_parse_custom_addr(&packet.from).expect("valid custom addr"); + info!( + "recv {} -> {}: copying {} bytes", + from.fmt_short(), + self.id.fmt_short(), + packet.data.len() + ); + buf[..packet.data.len()].copy_from_slice(&packet.data); + *recv_info = RecvInfo::new(packet.from, Some(to_custom_addr(self.id))); + meta.len = packet.data.len(); + meta.stride = packet.data.len(); + count += 1; + } + if count > 0 { + info!("recv {}: filled {count} slots", self.id.fmt_short()); + Poll::Ready(Ok(count)) + } else { + Poll::Pending + } + } +} + +#[cfg(test)] +mod tests { + use std::{sync::Arc, time::Duration}; + + use iroh_relay::RelayMap; + use n0_error::{Result, StdResultExt}; + use n0_tracing_test::traced_test; + + use super::*; + use crate::{ + Endpoint, EndpointAddr, RelayMode, SecretKey, TransportAddr, + endpoint::{Builder, Connection, presets, transports::AddrKind}, + protocol::{AcceptError, ProtocolHandler, Router}, + socket::biased_rtt_path_selector::{BiasedRttPathSelector, TransportBias}, + test_utils::run_relay_server, + }; + + const ECHO_ALPN: &[u8] = b"test/echo"; + + #[derive(Debug, Clone)] + struct Echo; + + impl ProtocolHandler for Echo { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let (mut send, mut recv) = connection.accept_bi().await?; + tokio::io::copy(&mut recv, &mut send).await?; + send.finish()?; + connection.closed().await; + Ok(()) + } + } + + /// Configuration for endpoint builder. + #[derive(Clone, Default)] + struct EndpointConfig { + custom_bias: Option, + keep_ip: bool, + relay_map: Option, + } + + impl EndpointConfig { + fn with_custom_bias(mut self, bias: TransportBias) -> Self { + self.custom_bias = Some(bias); + self + } + + fn with_ip(mut self) -> Self { + self.keep_ip = true; + self + } + + fn with_relay(mut self, relay_map: RelayMap) -> Self { + self.relay_map = Some(relay_map); + self + } + } + + /// Creates a basic endpoint builder with the given secret key and custom transport. + fn endpoint_builder( + secret_key: SecretKey, + transport: Arc, + config: EndpointConfig, + ) -> Builder { + let relay_mode = match config.relay_map { + Some(map) => RelayMode::Custom(map), + None => RelayMode::Disabled, + }; + let mut builder = Endpoint::builder(presets::N0) + .secret_key(secret_key) + .relay_mode(relay_mode) + .ca_tls_config(crate::tls::CaTlsConfig::insecure_skip_verify()) + .add_custom_transport(transport); + if let Some(bias) = config.custom_bias { + builder = builder.path_selector(Arc::new( + BiasedRttPathSelector::default() + .with_bias(AddrKind::Custom(TEST_TRANSPORT_ID), bias), + )); + } + if !config.keep_ip { + builder = builder.clear_ip_transports(); + } + builder + } + + /// Creates an address with both IP (from endpoint) and custom transport addresses. + fn mixed_addr(ep: &Endpoint, endpoint_id: EndpointId) -> EndpointAddr { + let ep_addr = ep.addr(); + let custom_addr = to_custom_addr(endpoint_id); + EndpointAddr::from_parts( + endpoint_id, + ep_addr + .addrs + .iter() + .cloned() + .chain(std::iter::once(TransportAddr::Custom(custom_addr))), + ) + } + + /// Creates an address with only the custom transport address. + fn custom_only_addr(endpoint_id: EndpointId) -> EndpointAddr { + EndpointAddr::from_parts( + endpoint_id, + std::iter::once(TransportAddr::Custom(to_custom_addr(endpoint_id))), + ) + } + + /// Returns true if the selected path is the custom transport. + fn is_custom_selected(conn: &crate::endpoint::Connection) -> bool { + let paths = conn.paths(); + paths.iter().find(|p| p.is_selected()).is_some_and( + |p| matches!(p.remote_addr(), TransportAddr::Custom(a) if a.id() == TEST_TRANSPORT_ID), + ) + } + + /// Returns true if either + /// - we have both IP and custom paths, and the selected path is IP. + /// - we only have one path + fn is_ip_selected_from_ip_and_custom(conn: &crate::endpoint::Connection) -> bool { + let paths = conn.paths(); + let has_ip = paths.iter().any(|p| p.remote_addr().is_ip()); + let has_custom = paths.iter().any(|p| p.remote_addr().is_custom()); + if !has_ip || !has_custom { + return true; + } + paths + .iter() + .any(|p| p.is_selected() && p.remote_addr().is_ip()) + } + + /// Returns true if the selected path is a relay transport. + fn is_relay_selected(conn: &crate::endpoint::Connection) -> bool { + let paths = conn.paths(); + paths + .iter() + .find(|p| p.is_selected()) + .is_some_and(|p| p.is_relay()) + } + + /// Verifies echo works over the connection. + async fn verify_echo(conn: &crate::endpoint::Connection, msg: &[u8]) -> Result<()> { + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(msg).await.anyerr()?; + send.finish().anyerr()?; + let response = recv.read_to_end(100).await.anyerr()?; + assert_eq!(response, msg); + Ok(()) + } + + /// Test custom transport only - no IP, no relay, dial by custom address. + #[tokio::test] + #[traced_test] + async fn test_custom_transport_only() -> Result<()> { + let network = TestNetwork::new(); + let s1 = SecretKey::generate(); + let s2 = SecretKey::generate(); + + let t1 = network.create_transport(s1.public())?; + let t2 = network.create_transport(s2.public())?; + + let ep1 = endpoint_builder(s1, t1, EndpointConfig::default()) + .bind() + .await?; + let ep2 = endpoint_builder(s2.clone(), t2, EndpointConfig::default()) + .bind() + .await?; + let router = Router::builder(ep2).accept(ECHO_ALPN, Echo).spawn(); + + let conn = ep1 + .connect(custom_only_addr(s2.public()), ECHO_ALPN) + .await?; + + // Verify exactly one path exists and it's the custom transport + let paths = conn.paths(); + assert_eq!(paths.len(), 1, "Expected exactly one path"); + assert!( + is_custom_selected(&conn), + "Custom transport should be selected" + ); + + verify_echo(&conn, b"custom only").await?; + conn.close(0u32.into(), b"done"); + router.shutdown().await.anyerr()?; + Ok(()) + } + + /// Test that custom transports can surface a local address per incoming packet. + #[tokio::test] + #[traced_test] + async fn test_custom_transport_local_addr() -> Result<()> { + use crate::endpoint::LocalTransportAddr; + + let network = TestNetwork::new(); + let s1 = SecretKey::generate(); + let s2 = SecretKey::generate(); + + let t1 = network.create_transport(s1.public())?; + let t2 = network.create_transport(s2.public())?; + + let ep1 = endpoint_builder(s1, t1, EndpointConfig::default()) + .bind() + .await?; + let ep2 = endpoint_builder(s2.clone(), t2, EndpointConfig::default()) + .alpns(vec![ECHO_ALPN.to_vec()]) + .bind() + .await?; + + let connect = tokio::spawn({ + let ep1 = ep1.clone(); + let dst = custom_only_addr(s2.public()); + async move { ep1.connect(dst, ECHO_ALPN).await } + }); + + let incoming = ep2.accept().await.expect("incoming"); + assert_eq!( + incoming.local_addr(), + LocalTransportAddr::Custom(Some(to_custom_addr(s2.public()))), + ); + let _conn = incoming.accept().anyerr()?.await.anyerr()?; + + connect.await.anyerr()??; + Ok(()) + } + + /// Test that custom transport is selected over IP when given an RTT advantage. + #[tokio::test] + #[traced_test] + async fn test_custom_transport_wins_over_ip() -> Result<()> { + let network = TestNetwork::new(); + let s1 = SecretKey::generate(); + let s2 = SecretKey::generate(); + + let t1 = network.create_transport(s1.public())?; + let t2 = network.create_transport(s2.public())?; + + // Strong RTT advantage for custom transport + let custom_bias = TransportBias::primary().with_rtt_advantage(Duration::from_secs(10)); + let config = EndpointConfig::default() + .with_ip() + .with_custom_bias(custom_bias); + + let ep1 = endpoint_builder(s1, t1, config.clone()).bind().await?; + let ep2 = endpoint_builder(s2.clone(), t2, config).bind().await?; + let router = Router::builder(ep2.clone()).accept(ECHO_ALPN, Echo).spawn(); + + let conn = ep1 + .connect(mixed_addr(&ep2, s2.public()), ECHO_ALPN) + .await?; + + // Wait for paths to settle + tokio::time::sleep(Duration::from_millis(100)).await; + + assert!( + is_custom_selected(&conn), + "Custom transport should be selected with RTT advantage" + ); + + verify_echo(&conn, b"custom wins").await?; + conn.close(0u32.into(), b"done"); + router.shutdown().await.anyerr()?; + Ok(()) + } + + /// Test that IP is selected over custom transport when custom has an RTT disadvantage. + #[tokio::test] + #[traced_test] + async fn test_ip_wins_over_custom() -> Result<()> { + let network = TestNetwork::new(); + let s1 = SecretKey::generate(); + let s2 = SecretKey::generate(); + + let t1 = network.create_transport(s1.public())?; + let t2 = network.create_transport(s2.public())?; + + // Strong RTT disadvantage for custom transport + let custom_bias = TransportBias::primary().with_rtt_disadvantage(Duration::from_secs(10)); + let config = EndpointConfig::default() + .with_ip() + .with_custom_bias(custom_bias); + + let ep1 = endpoint_builder(s1, t1, config.clone()).bind().await?; + let ep2 = endpoint_builder(s2.clone(), t2, config).bind().await?; + let router = Router::builder(ep2.clone()).accept(ECHO_ALPN, Echo).spawn(); + + let conn = ep1 + .connect(mixed_addr(&ep2, s2.public()), ECHO_ALPN) + .await?; + + // Wait for paths to settle + tokio::time::sleep(Duration::from_millis(200)).await; + + assert!( + is_ip_selected_from_ip_and_custom(&conn), + "IP transport should be selected when custom has RTT disadvantage" + ); + + verify_echo(&conn, b"ip wins").await?; + conn.close(0u32.into(), b"done"); + router.shutdown().await.anyerr()?; + Ok(()) + } + + /// Test that custom transport (primary) is selected over relay (backup). + /// + /// This test first connects using only the relay address, then reconnects with + /// both relay and custom addresses to verify the custom transport (primary) wins + /// over the relay (backup). + #[tokio::test] + #[traced_test] + async fn test_custom_transport_wins_over_relay() -> Result<()> { + let (relay_map, _relay_url, _guard) = run_relay_server().await?; + let network = TestNetwork::new(); + let s1 = SecretKey::generate(); + let s2 = SecretKey::generate(); + + let t1 = network.create_transport(s1.public())?; + let t2 = network.create_transport(s2.public())?; + + // Custom transport is primary by default, relay is backup + let config = EndpointConfig::default().with_relay(relay_map.clone()); + + let ep1 = endpoint_builder(s1, t1, config.clone()).bind().await?; + let ep2 = endpoint_builder(s2.clone(), t2, config).bind().await?; + + // Wait for relay connection to be established + ep1.online().await; + ep2.online().await; + + let router = Router::builder(ep2.clone()).accept(ECHO_ALPN, Echo).spawn(); + + // Get all addresses including relay and custom + let ep2_addr = ep2.addr(); + let custom_addr = to_custom_addr(s2.public()); + + // Debug: print ep2 address to see what's available + eprintln!("ep2 address: {:?}", ep2_addr); + + // Create address with both relay and custom + let all_addrs = EndpointAddr::from_parts( + s2.public(), + ep2_addr + .addrs + .iter() + .cloned() + .chain(std::iter::once(TransportAddr::Custom(custom_addr))), + ); + eprintln!("Connecting with all addresses: {:?}", all_addrs); + + // First, connect with relay-only to verify relay works + let relay_addrs: Vec<_> = ep2_addr + .addrs + .iter() + .filter(|a| matches!(a, TransportAddr::Relay(_))) + .cloned() + .collect(); + eprintln!("Relay addresses in ep2_addr: {:?}", relay_addrs); + + // If there are no relay addresses, skip the relay-first test + if relay_addrs.is_empty() { + eprintln!( + "WARNING: No relay addresses found in ep2_addr, skipping relay-first connection test" + ); + } else { + // Connect with relay-only address first to verify relay works + let relay_only_addr = EndpointAddr::from_parts(s2.public(), relay_addrs.into_iter()); + eprintln!("Connecting with relay-only address: {:?}", relay_only_addr); + + let conn = ep1.connect(relay_only_addr, ECHO_ALPN).await?; + + // Wait for relay path to be established + tokio::time::sleep(Duration::from_millis(200)).await; + + // Debug: print paths after relay-only connect + let paths = conn.paths(); + eprintln!("Paths after relay-only connect:"); + for path in paths.iter() { + eprintln!( + " {} selected={} rtt={:?}", + path.remote_addr(), + path.is_selected(), + path.rtt() + ); + } + + // Verify relay is currently selected + assert!( + is_relay_selected(&conn), + "Relay should be selected after connecting with relay-only address" + ); + + verify_echo(&conn, b"relay test").await?; + conn.close(0u32.into(), b"done with relay test"); + tokio::time::sleep(Duration::from_millis(100)).await; + } + + // Now connect with all addresses (relay + custom) + let conn = ep1.connect(all_addrs, ECHO_ALPN).await?; + + // Wait for paths to settle + tokio::time::sleep(Duration::from_millis(200)).await; + + // Debug: print all paths + let paths = conn.paths(); + eprintln!("Paths after connecting with all addresses:"); + for path in paths.iter() { + eprintln!( + " {} selected={} rtt={:?}", + path.remote_addr(), + path.is_selected(), + path.rtt() + ); + } + + // Custom (primary) should win over relay (backup) + assert!( + is_custom_selected(&conn), + "Custom transport (primary) should be selected over relay (backup)" + ); + + verify_echo(&conn, b"custom wins over relay").await?; + conn.close(0u32.into(), b"done"); + router.shutdown().await.anyerr()?; + Ok(()) + } +} diff --git a/vendor/iroh/src/tls.rs b/vendor/iroh/src/tls.rs new file mode 100644 index 0000000..f031ba9 --- /dev/null +++ b/vendor/iroh/src/tls.rs @@ -0,0 +1,147 @@ +//! TLS configuration for iroh. +//! +//! Currently there is one mechanism available: +//! - Raw Public Keys, using the TLS extension described in [RFC 7250] +//! +//! [RFC 7250]: https://datatracker.ietf.org/doc/html/rfc7250 + +use std::sync::Arc; + +use iroh_base::SecretKey; +use noq::crypto::rustls::{QuicClientConfig, QuicServerConfig}; +use tracing::warn; + +use self::resolver::ResolveRawPublicKeyCert; + +pub(crate) mod misc; +pub(crate) mod name; +mod resolver; +mod verifier; + +#[allow(deprecated)] // Re-export of backwards-compatibility item +pub use iroh_relay::tls::CaRootsConfig; +pub use iroh_relay::tls::CaTlsConfig; +#[cfg(with_crypto_provider)] +pub use iroh_relay::tls::default_provider; + +/// Maximum amount of TLS tickets we will cache (by default) for 0-RTT connection +/// establishment. +/// +/// 8 tickets per remote endpoint, 32 different endpoints would max out the required storage: +/// ~200 bytes per session + certificates (which are ~387 bytes) +/// So 8 * 32 * (200 + 387) = 150.272 bytes, assuming pointers to certificates +/// are never aliased pointers (they're Arc'ed). +/// I think 150KB is an acceptable default upper limit for such a cache. +pub(crate) const DEFAULT_MAX_TLS_TICKETS: usize = 8 * 32; + +/// Configuration for TLS. +/// +/// The main point of this struct is to keep state that should be kept the same +/// over multiple TLS sessions the same. +/// E.g. the `server_verifier` and `client_verifier` Arc pointers are checked to be +/// the same between different TLS session calls with 0-RTT data in rustls. +/// This makes sure that's the case. +#[derive(Debug)] +pub(crate) struct TlsConfig { + pub(crate) secret_key: SecretKey, + cert_resolver: Arc, + server_verifier: Arc, + client_verifier: Arc, + session_store: Arc, + crypto_provider: Arc, +} + +impl TlsConfig { + pub(crate) fn new( + secret_key: SecretKey, + max_tls_tickets: usize, + crypto_provider: Arc, + ) -> Self { + Self { + cert_resolver: Arc::new(ResolveRawPublicKeyCert::new(&secret_key)), + server_verifier: Arc::new(verifier::ServerCertificateVerifier), + client_verifier: Arc::new(verifier::ClientCertificateVerifier), + session_store: Arc::new(rustls::client::ClientSessionMemoryCache::new( + max_tls_tickets, + )), + crypto_provider, + secret_key, + } + } + + /// Create a TLS client configuration. + /// + /// If *keylog* is `true` this will enable logging of the pre-master key to the file in the + /// `SSLKEYLOGFILE` environment variable. This can be used to inspect the traffic for + /// debugging purposes. + pub(crate) fn make_client_config( + &self, + keylog: bool, + ) -> Result { + let mut crypto = rustls::ClientConfig::builder_with_provider(self.crypto_provider.clone()) + .with_protocol_versions(verifier::PROTOCOL_VERSIONS)? + .dangerous() + .with_custom_certificate_verifier(self.server_verifier.clone()) + .with_client_cert_resolver(self.cert_resolver.clone()); + + // TODO: enable/disable 0-RTT/storing tickets + crypto.resumption = rustls::client::Resumption::store(self.session_store.clone()); + crypto.enable_early_data = true; + + // The synthetic server name is used locally to select the expected endpoint ID + // and to partition the session cache. Iroh servers do not use SNI, so do not + // disclose the endpoint ID in the ClientHello. + crypto.enable_sni = false; + + if keylog { + warn!("enabling SSLKEYLOGFILE for TLS pre-master keys"); + crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let quic = QuicClientConfig::try_from(crypto)?; + Ok(quic) + } + + /// Create a TLS server configuration. + /// + /// If *keylog* is `true` this will enable logging of the pre-master key to the file in the + /// `SSLKEYLOGFILE` environment variable. This can be used to inspect the traffic for + /// debugging purposes. + pub(crate) fn make_server_config( + &self, + keylog: bool, + ) -> Result { + let mut crypto = rustls::ServerConfig::builder_with_provider(self.crypto_provider.clone()) + .with_protocol_versions(verifier::PROTOCOL_VERSIONS)? + .with_client_cert_verifier(self.client_verifier.clone()) + .with_cert_resolver(self.cert_resolver.clone()); + if keylog { + warn!("enabling SSLKEYLOGFILE for TLS pre-master keys"); + crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + // must be u32::MAX or 0 (the default). Any other value panics with QUIC + // This is specified in RFC 9001: https://www.rfc-editor.org/rfc/rfc9001#section-4.6.1 + crypto.max_early_data_size = u32::MAX; + let quic = QuicServerConfig::try_from(crypto)?; + Ok(quic) + } +} + +#[allow(missing_docs)] +#[n0_error::stack_error(derive, add_meta, from_sources)] +#[non_exhaustive] +pub enum TlsConfigError { + #[error( + "The configured crypto provider is missing support for TLS13_AES_128_GCM_SHA256, which is required for QUIC initial packets." + )] + CryptoProviderNoInitialCipherSuite { + #[error(std_err)] + source: noq::crypto::rustls::NoInitialCipherSuite, + }, + #[error("The configured crypto provider is incompatible with iroh and QUIC encryption")] + CryptoProviderIncompatible { + #[error(std_err)] + source: rustls::Error, + }, +} diff --git a/vendor/iroh/src/tls/misc.rs b/vendor/iroh/src/tls/misc.rs new file mode 100644 index 0000000..6a6fddd --- /dev/null +++ b/vendor/iroh/src/tls/misc.rs @@ -0,0 +1,125 @@ +use ctutils::CtEq; +use noq_proto::crypto; +use rand::RngExt; +use rustls::crypto::cipher::{ + AeadKey, InboundOpaqueMessage, Iv, NONCE_LEN, OutboundPlainMessage, Tls13AeadAlgorithm, +}; + +/// Implements [`crypto::HandshakeTokenKey`] using a [`Tls13AeadAlgorithm`]. +/// +/// This can be obtained from looking through available ciphers from a +/// [`rustls::crypto::CryptoProvider`]. +pub(crate) struct RustlsTokenKey { + key: [u8; 32], + aead: &'static dyn Tls13AeadAlgorithm, +} + +impl RustlsTokenKey { + /// Constructs [`crypto::HandshakeTokenKey`] from a [`rustls::crypto::CryptoProvider`]. + /// + /// Tries to find a suitable TLS 1.3 cipher suite from the provided crypto provider, + /// then uses it to extract an AEAD to use as the token key encryption method. + /// + /// Then generates a random master key to use. + /// + /// Returns `None` when this can't find a suitable TLS cipher suite in the given crypto + /// provider. + pub(crate) fn new( + rng: &mut impl rand::CryptoRng, + crypto_provider: &rustls::crypto::CryptoProvider, + ) -> Option { + let suite = crypto_provider + .cipher_suites + .iter() + .filter_map(|suite| suite.tls13()) + .next()?; + let aead = suite.aead_alg; + Some(Self { + key: rng.random(), + aead, + }) + } +} + +impl crypto::HandshakeTokenKey for RustlsTokenKey { + fn seal(&self, token_nonce: u128, data: &mut Vec) -> Result<(), crypto::CryptoError> { + let key = AeadKey::from(self.key); + let nonce: [u8; NONCE_LEN] = *token_nonce + .to_le_bytes() + .first_chunk() + .expect("expected u128 > 96 bit"); + let iv = Iv::from(nonce); + let msg = OutboundPlainMessage { + typ: rustls::ContentType::ApplicationData, + version: rustls::ProtocolVersion::TLSv1_3, + payload: rustls::crypto::cipher::OutboundChunks::Single(&*data), + }; + let out = self + .aead + .encrypter(key, iv) + .encrypt(msg, 0) + .map_err(|_| crypto::CryptoError)?; + + data.clear(); + data.extend(out.payload.as_ref()); + + Ok(()) + } + + fn open<'a>( + &self, + token_nonce: u128, + data: &'a mut [u8], + ) -> Result<&'a [u8], crypto::CryptoError> { + let key = AeadKey::from(self.key); + let nonce: [u8; NONCE_LEN] = *token_nonce + .to_le_bytes() + .first_chunk() + .expect("expected u128 > 96 bit"); + let iv = Iv::from(nonce); + + let msg = InboundOpaqueMessage::new( + rustls::ContentType::ApplicationData, + rustls::ProtocolVersion::TLSv1_3, + data, + ); + let plain = self + .aead + .decrypter(key, iv) + .decrypt(msg, 0) + .map_err(|_| crypto::CryptoError)?; + + Ok(plain.payload) + } +} + +pub(crate) struct Blake3HmacKey([u8; 32]); + +impl Blake3HmacKey { + pub(crate) fn new(rng: &mut impl rand::CryptoRng) -> Self { + let mut key = [0u8; 32]; + rng.fill_bytes(&mut key); + Self(key) + } +} + +impl crypto::HmacKey for Blake3HmacKey { + fn sign(&self, data: &[u8], signature_out: &mut [u8]) { + signature_out.copy_from_slice(blake3::keyed_hash(&self.0, data).as_slice()); + } + + fn signature_len(&self) -> usize { + blake3::OUT_LEN // 32 bytes + } + + fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), crypto::CryptoError> { + let reference = blake3::keyed_hash(&self.0, data); + // to_bool is fine here, because it's the last thing we do to + // distinguish success or failure (see to_bool documentation) + if signature.ct_eq(reference.as_slice()).to_bool() { + Ok(()) + } else { + Err(crypto::CryptoError) + } + } +} diff --git a/vendor/iroh/src/tls/name.rs b/vendor/iroh/src/tls/name.rs new file mode 100644 index 0000000..dc94ec5 --- /dev/null +++ b/vendor/iroh/src/tls/name.rs @@ -0,0 +1,67 @@ +//! Implementation of encoding iroh EndpointIds as domain names. +//! +//! We used to send this name via SNI up until iroh version 1.0.3, but stopped doing so in +//! versions after that. It is now used only locally by the `ServerCertificateVerifier` and +//! to separate the buckets for 0-RTT session tickets. +//! +//! We used to use a constant "localhost" for the TLS server name - however, that affects +//! 0-RTT and would put all of the TLS session tickets we receive into the same bucket in +//! the TLS session ticket cache. +//! So we choose something that'd dependent on the EndpointId. +//! We cannot use hex to encode the EndpointId, as that'd encode to 64 characters, but we only +//! have 63 maximum per DNS subdomain. Base32 is the next best alternative. +//! We use the `.invalid` TLD, as that's specified (in RFC 2606) to never actually resolve +//! "for real", unlike `.localhost` which is allowed to resolve to `127.0.0.1`. +//! We also add "iroh" as a subdomain, although those 5 bytes might not be necessary. +//! We *could* decide to remove that indicator in the future likely without breakage. + +use data_encoding::BASE32_DNSSEC; +use iroh_base::EndpointId; + +pub(crate) fn encode(endpoint_id: EndpointId) -> String { + format!( + "{}.iroh.invalid", + BASE32_DNSSEC.encode(endpoint_id.as_bytes()) + ) +} + +pub(crate) fn decode(name: &str) -> Option { + let [base32_endpoint_id, "iroh", "invalid"] = name.split(".").collect::>()[..] else { + return None; + }; + EndpointId::from_bytes( + &BASE32_DNSSEC + .decode(base32_endpoint_id.as_bytes()) + .ok()? + .try_into() + .ok()?, + ) + .ok() +} + +#[cfg(test)] +mod tests { + use iroh_base::SecretKey; + use rand::{RngExt, SeedableRng}; + + #[test] + fn test_roundtrip() { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let key = SecretKey::from_bytes(&rng.random()); + let endpoint_id = key.public(); + println!("{}", super::encode(endpoint_id)); + assert_eq!( + Some(endpoint_id), + super::decode(&super::encode(endpoint_id)) + ); + } + + #[test] + fn test_snapshot() { + let key = SecretKey::from_bytes(&[0; 32]); + assert_eq!( + super::encode(key.public()), + "7dl2ff6emqi2qol3l382krodedij45bn3nh479hqo14a32qpr8kg.iroh.invalid", + ); + } +} diff --git a/vendor/iroh/src/tls/resolver.rs b/vendor/iroh/src/tls/resolver.rs new file mode 100644 index 0000000..1ee853c --- /dev/null +++ b/vendor/iroh/src/tls/resolver.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use iroh_base::SecretKey; +use webpki_types::CertificateDer; + +#[derive(Debug)] +pub(super) struct ResolveRawPublicKeyCert { + key: Arc, +} + +impl ResolveRawPublicKeyCert { + pub(super) fn new(secret_key: &SecretKey) -> Self { + let client_private_key = Arc::new(IrohSecretKey::from(secret_key.clone())); + let client_public_key = client_private_key.spki_public_key(); + let client_public_key_as_cert = CertificateDer::from(client_public_key.to_vec()); + + let certified_key = + rustls::sign::CertifiedKey::new(vec![client_public_key_as_cert], client_private_key); + + let key = Arc::new(certified_key); + + Self { key } + } +} + +impl rustls::client::ResolvesClientCert for ResolveRawPublicKeyCert { + fn resolve( + &self, + _root_hint_subjects: &[&[u8]], + _sigschemes: &[rustls::SignatureScheme], + ) -> Option> { + Some(Arc::clone(&self.key)) + } + + fn only_raw_public_keys(&self) -> bool { + true + } + + fn has_certs(&self) -> bool { + true + } +} + +impl rustls::server::ResolvesServerCert for ResolveRawPublicKeyCert { + fn resolve( + &self, + _client_hello: rustls::server::ClientHello<'_>, + ) -> Option> { + Some(Arc::clone(&self.key)) + } + + fn only_raw_public_keys(&self) -> bool { + true + } +} + +#[derive(Debug, Clone, derive_more::From)] +struct IrohSecretKey { + #[from] + key: SecretKey, +} + +impl IrohSecretKey { + fn spki_public_key(&self) -> webpki_types::SubjectPublicKeyInfoDer<'static> { + rustls::sign::public_key_to_spki( + &webpki_types::alg_id::ED25519, + self.key.public().as_bytes(), + ) + } +} +impl rustls::sign::SigningKey for IrohSecretKey { + fn choose_scheme( + &self, + offered: &[rustls::SignatureScheme], + ) -> Option> { + if offered.contains(&rustls::SignatureScheme::ED25519) { + Some(Box::new(self.clone())) + } else { + None + } + } + + fn algorithm(&self) -> rustls::SignatureAlgorithm { + rustls::SignatureAlgorithm::ED25519 + } + + fn public_key(&self) -> Option> { + Some(self.spki_public_key()) + } +} + +impl rustls::sign::Signer for IrohSecretKey { + fn sign(&self, message: &[u8]) -> Result, rustls::Error> { + Ok(self.key.sign(message).to_bytes().to_vec()) + } + + fn scheme(&self) -> rustls::SignatureScheme { + rustls::SignatureScheme::ED25519 + } +} diff --git a/vendor/iroh/src/tls/verifier.rs b/vendor/iroh/src/tls/verifier.rs new file mode 100644 index 0000000..52e5ff9 --- /dev/null +++ b/vendor/iroh/src/tls/verifier.rs @@ -0,0 +1,211 @@ +//! TLS 1.3 certificates and handshakes handling. +//! +//! This module handles a verification of a client/server certificate chain +//! and signatures allegedly by the given certificates, or using raw public keys. + +use iroh_base::{PublicKey, Signature}; +use rustls::{ + CertificateError, DigitallySignedStruct, DistinguishedName, SignatureScheme, + SupportedProtocolVersion, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::{WebPkiSupportedAlgorithms, verify_tls13_signature_with_raw_key}, + pki_types::CertificateDer as Certificate, + server::danger::{ClientCertVerified, ClientCertVerifier}, +}; +use webpki_types::SubjectPublicKeyInfoDer; + +/// The only TLS version we support is 1.3 +pub(super) const PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&rustls::version::TLS13]; + +const ED25519_DALEK: Ed25519Dalek = Ed25519Dalek; +const SUPPORTED_SIG_ALGS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { + all: &[&ED25519_DALEK], + mapping: &[(SignatureScheme::ED25519, &[&ED25519_DALEK])], +}; + +/// Implementation of the `rustls` certificate verification traits +/// +/// Only TLS 1.3 is supported. TLS 1.2 should be disabled in the configuration of `rustls`. +#[derive(Default, Debug)] +pub(super) struct ServerCertificateVerifier; + +impl ServerCertVerifier for ServerCertificateVerifier { + fn verify_server_cert( + &self, + end_entity: &Certificate, + intermediates: &[Certificate], + server_name: &rustls::pki_types::ServerName, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + let rustls::pki_types::ServerName::DnsName(dns_name) = server_name else { + return Err(rustls::Error::UnsupportedNameType); + }; + let Some(remote_peer_id) = super::name::decode(dns_name.as_ref()) else { + return Err(rustls::Error::InvalidCertificate( + CertificateError::NotValidForName, + )); + }; + + if !intermediates.is_empty() { + return Err(rustls::Error::InvalidCertificate( + CertificateError::UnknownIssuer, + )); + } + + let end_entity_as_spki = SubjectPublicKeyInfoDer::from(end_entity.as_ref()); + let remote_public_spki = rustls::sign::public_key_to_spki( + &webpki_types::alg_id::ED25519, + remote_peer_id.as_bytes(), + ); + + // This effectively checks that the `end_entity_as_spki` bytes have the expected + // (constant) 12 byte prefix (consisting of the Ed25519 public key ASN.1 object + // identifier, some ASN.1 DER encoding bytes signaling that this is a SPKI and + // consists of the object identifier and a bit sequence, a zero byte indicating + // that the bit sequence is padded with 0 additional bits) matches, as well as + // the public key bytes match the `remote_peer_id` public key bytes. + if remote_public_spki != end_entity_as_spki { + return Err(rustls::Error::InvalidCertificate( + CertificateError::UnknownIssuer, + )); + } + + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &Certificate, + _dss: &DigitallySignedStruct, + ) -> Result { + Err(rustls::Error::PeerIncompatible( + rustls::PeerIncompatible::Tls12NotOffered, + )) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &Certificate, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature_with_raw_key( + message, + &SubjectPublicKeyInfoDer::from(cert.as_ref()), + dss, + &SUPPORTED_SIG_ALGS, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + SUPPORTED_SIG_ALGS.supported_schemes() + } + + fn requires_raw_public_keys(&self) -> bool { + true + } +} + +/// Implementation of the `rustls` certificate verification traits. +/// +/// Only TLS 1.3 is supported. TLS 1.2 should be disabled in the configuration of `rustls`. +#[derive(Default, Debug)] +pub(super) struct ClientCertificateVerifier; + +/// We require one of the following client certificate configurations: +/// +/// - a valid raw public key configuration +impl ClientCertVerifier for ClientCertificateVerifier { + fn offer_client_auth(&self) -> bool { + true + } + + fn verify_client_cert( + &self, + _end_entity: &Certificate, + intermediates: &[Certificate], + _now: rustls::pki_types::UnixTime, + ) -> Result { + if !intermediates.is_empty() { + return Err(rustls::Error::InvalidCertificate( + CertificateError::UnknownIssuer, + )); + } + + // Beyond checking for no intermediates, we don't check the client certificate. + // The actual signatures are already verified - this ensures authentication. + + Ok(ClientCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &Certificate, + _dss: &DigitallySignedStruct, + ) -> Result { + Err(rustls::Error::PeerIncompatible( + rustls::PeerIncompatible::Tls12NotOffered, + )) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &Certificate, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature_with_raw_key( + message, + &SubjectPublicKeyInfoDer::from(cert.as_ref()), + dss, + &SUPPORTED_SIG_ALGS, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + SUPPORTED_SIG_ALGS.supported_schemes() + } + + fn root_hint_subjects(&self) -> &[DistinguishedName] { + &[][..] + } + + fn requires_raw_public_keys(&self) -> bool { + true + } +} + +#[derive(Debug)] +struct Ed25519Dalek; + +impl webpki_types::SignatureVerificationAlgorithm for Ed25519Dalek { + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), webpki_types::InvalidSignature> { + let public_key = + PublicKey::try_from(public_key).map_err(|_| webpki_types::InvalidSignature)?; + let signature = + Signature::try_from(signature).map_err(|_| webpki_types::InvalidSignature)?; + public_key + .verify(message, &signature) + .map_err(|_| webpki_types::InvalidSignature) + } + + fn public_key_alg_id(&self) -> webpki_types::AlgorithmIdentifier { + webpki_types::alg_id::ED25519 + } + + fn signature_alg_id(&self) -> webpki_types::AlgorithmIdentifier { + webpki_types::alg_id::ED25519 + } + + fn fips(&self) -> bool { + false + } +} diff --git a/vendor/iroh/src/util.rs b/vendor/iroh/src/util.rs new file mode 100644 index 0000000..4651a10 --- /dev/null +++ b/vendor/iroh/src/util.rs @@ -0,0 +1,71 @@ +//! Utilities used in [`iroh`](crate). + +/// Creates a [`reqwest::ClientBuilder`] from a [`rustls::ClientConfig`] and our [`DnsResolver`]. +/// +/// In a browser context these options are not supported, so this function takes no arguments +/// if `wasm_browser` is enabled. +/// +/// [`DnsResolver`]: crate::dns::DnsResolver +#[cfg(not(wasm_browser))] +pub(crate) fn reqwest_client_builder( + tls_client_config: rustls::ClientConfig, + dns_resolver: crate::dns::DnsResolver, +) -> reqwest::ClientBuilder { + use self::reqwest_dns_resolver::ReqwestDnsResolver; + + reqwest::Client::builder() + .tls_backend_preconfigured(tls_client_config) + .dns_resolver(ReqwestDnsResolver(dns_resolver)) +} + +#[cfg(wasm_browser)] +pub(crate) fn reqwest_client_builder() -> reqwest::ClientBuilder { + reqwest::Client::builder() +} + +#[cfg(not(wasm_browser))] +mod reqwest_dns_resolver { + use std::net::SocketAddr; + + use iroh_dns::dns::{DNS_TIMEOUT, DnsResolver}; + + use crate::address_lookup::DNS_STAGGERING_MS; + + /// Implementation of [`reqwest::dns::Resolve`] for [`DnsResolver`]. + /// + /// Wrapped in a newtype to not expose this in the public iroh API. + pub(super) struct ReqwestDnsResolver(pub(super) DnsResolver); + + impl reqwest::dns::Resolve for ReqwestDnsResolver { + fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving { + let this = self.0.clone(); + let name = name.as_str().to_string(); + Box::pin(async move { + // Staggered so that a single unresponsive DNS server does not stall the + // request for the full timeout. Ideally this resolves **both** IPv4 and + // IPv6 rather than racing them, but our resolver has no function for that + // yet. + let res = this + .lookup_ipv4_ipv6_staggered(name, DNS_TIMEOUT, DNS_STAGGERING_MS) + .await + // Collected eagerly: the returned iterator borrows the resolver, which + // does not outlive this future. + .map(|addrs| { + addrs + .map(|addr| SocketAddr::new(addr, 0)) + .collect::>() + }); + match res { + Ok(addrs) => { + let addrs: reqwest::dns::Addrs = Box::new(addrs.into_iter()); + Ok(addrs) + } + Err(err) => { + let err: Box = Box::new(err); + Err(err) + } + } + }) + } + } +} diff --git a/vendor/iroh/tests/integration.rs b/vendor/iroh/tests/integration.rs new file mode 100644 index 0000000..6a302b5 --- /dev/null +++ b/vendor/iroh/tests/integration.rs @@ -0,0 +1,154 @@ +//! Basic integration tests for iroh that can be run both in browsers & natively. +//! +//! At the moment, these tests unfortunately interact with deployed services, specifically +//! the "real" DNS server infrastructure and "real" relays. +//! +//! The main reason is that running rust code natively and simultaneously in endpoint.js via +//! wasm-bindgen-test is not trivial. We want to avoid a situation where you need to +//! remember to run *another* binary simultaneously to running `cargo test --test integration`. +//! +//! In the past we've hit relay rate-limits from all the tests in our CI, but I expect +//! we won't hit these with only this integration test. +use iroh::{ + Endpoint, RelayMode, + address_lookup::{AddressLookup, pkarr::PkarrResolver}, + endpoint::presets, +}; +use n0_error::{Result, StdResultExt}; +use n0_future::{ + StreamExt, task, + time::{self, Duration}, +}; +#[cfg(not(wasm_browser))] +use tokio::test; +use tracing::{Instrument, info_span}; +#[cfg(wasm_browser)] +use wasm_bindgen_test::wasm_bindgen_test as test; + +// Enable this if you want to run these tests in the browser. +// Unfortunately it's either-or: Enable this and you can run in the browser, disable to run in nodejs. +// #[cfg(wasm_browser)] +// wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +const ECHO_ALPN: &[u8] = b"echo"; + +#[test] +async fn simple_endpoint_id_based_connection_transfer() -> Result { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + setup_logging(); + let client = Endpoint::builder(presets::N0) + .relay_mode(RelayMode::Staging) + .bind() + .await?; + tracing::info!("started client, id {}", client.id().fmt_short()); + let server = Endpoint::builder(presets::N0) + .relay_mode(RelayMode::Staging) + .alpns(vec![ECHO_ALPN.to_vec()]) + .bind() + .await?; + tracing::info!("started server, id {}", server.id().fmt_short()); + + // ensure the server has connected to a relay + // and therefore has enough information to publish + tracing::info!("waiting for server to go online"); + time::timeout(Duration::from_secs(20), server.online()) + .await + .std_context("server endpoint took too long to get online")?; + + // Make the server respond to requests with an echo + task::spawn({ + tracing::info!("waiting for incoming connections on the server"); + let server = server.clone(); + async move { + while let Some(incoming) = server.accept().await { + tracing::info!("accepting connection"); + let conn = incoming.await?; + let endpoint_id = conn.remote_id(); + tracing::info!(endpoint_id = %endpoint_id.fmt_short(), "Accepted connection"); + + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let mut bytes_sent = 0; + while let Some(chunk) = recv.read_chunk(10_000).await.anyerr()? { + bytes_sent += chunk.len(); + send.write_chunk(chunk).await.anyerr()?; + } + send.finish().anyerr()?; + tracing::info!("Copied over {bytes_sent} byte(s)"); + + let code = conn.closed().await; + tracing::info!("Closed with code: {code:?}"); + } + + n0_error::Ok(()) + } + .instrument(info_span!("server")) + }); + + // Wait for pkarr records to be published + time::timeout(Duration::from_secs(20), { + let endpoint_id = server.id(); + tracing::info!( + "start timeout waiting for records to be published, waiting for {endpoint_id} address lookup" + ); + let tls_config = server.tls_config().clone(); + async move { + let resolver = PkarrResolver::n0_dns().build(tls_config); + loop { + // Very rudimentary non-backoff algorithm + time::sleep(Duration::from_secs(1)).await; + + let Some(mut stream) = resolver.resolve(endpoint_id) else { + tracing::info!("unable to get resolver stream, looping"); + continue; + }; + let Ok(Some(item)) = stream.try_next().await else { + tracing::info!("no items on stream when resolving, looping"); + continue; + }; + if item.relay_urls().next().is_some() { + tracing::info!("home relay found"); + break; + } + } + } + }) + .await + .anyerr()?; + + tracing::info!(to = %server.id().fmt_short(), "Opening a connection"); + let conn = client.connect(server.id(), ECHO_ALPN).await?; + tracing::info!("Connection opened"); + + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(b"Hello, World!").await.anyerr()?; + send.finish().anyerr()?; + tracing::info!("Sent request"); + + let response = recv.read_to_end(10_000).await.anyerr()?; + tracing::info!(len = response.len(), "Received response"); + assert_eq!(&response, b"Hello, World!"); + + tracing::info!("Closing connection"); + conn.close(1u32.into(), b"thank you, bye"); + + client.close().await; + server.close().await; + + Ok(()) +} + +#[cfg(wasm_browser)] +fn setup_logging() { + use tracing::Level; + + let mut config = wasm_tracing::WasmLayerConfig::new(); + config.set_max_level(Level::TRACE); + wasm_tracing::set_as_global_default_with_config(config).unwrap(); +} + +#[cfg(not(wasm_browser))] +fn setup_logging() { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); +} diff --git a/vendor/iroh/tests/patchbay.rs b/vendor/iroh/tests/patchbay.rs new file mode 100644 index 0000000..a003a50 --- /dev/null +++ b/vendor/iroh/tests/patchbay.rs @@ -0,0 +1,416 @@ +//! Patchbay network simulation tests. +//! +//! These tests use the [`patchbay`] crate to create virtual network topologies +//! in Linux user namespaces, testing iroh's NAT traversal, holepunching, +//! and connectivity under various network conditions. +//! +//! These tests require Linux with user namespace support. On non-Linux systems, you can use +//! the `patchbay` CLI to get a Linux container or VM with the required capabilities. +//! See patchbay docs for details. +//! +//! To run: +//! +//! ```sh +//! # On Linux (with user namespace support): +//! cargo nextest run -p iroh --test patchbay --profile patchbay +//! # or use the `cargo make` alias: +//! cargo make patchbay +//! # can also pass additional args: +//! cargo make patchbay holepunch_simple --no-capture +//! +//! # On macOS (runs in container via patchbay CLI): +//! patchbay test --release -p iroh --test patchbay +//! ``` + +// patchbay only runs on linux, and is skipped in cross-compile environments +// via a cfg directive +#![cfg(all(target_os = "linux", not(skip_patchbay)))] + +use std::{net::Ipv4Addr, time::Duration}; + +use ipnet::Ipv4Net; +use iroh::endpoint::Side; +use n0_error::{Result, StackResultExt}; +use n0_tracing_test::traced_test; +use patchbay::{IfaceConfig, LinkCondition, LinkDirection, Nat}; +use testdir::testdir; +use tracing::info; + +use self::util::{Pair, PathConnectionExt, lab_with_relay, ping_accept, ping_open}; +use crate::util::is_relayed; + +// Because we're in an integration test, we can't declare modules under patchbay/ +// without setting an explicit path. +#[path = "patchbay/degrade.rs"] +mod degrade; +#[path = "patchbay/nat.rs"] +mod nat; +#[path = "patchbay/relay.rs"] +mod relay; +#[path = "patchbay/switch-uplink.rs"] +mod switch_uplink; +#[path = "patchbay/util.rs"] +mod util; + +/// Init the user namespace before any threads are spawned. +/// +/// This gives us all permissions we need for the patchbay tests. +#[ctor::ctor(unsafe)] +fn userns_ctor() { + patchbay::init_userns().expect("failed to init userns"); +} + +// --- +// Holepunch tests +// --- + +/// Two devices behind destination-independent NATs holepunch a direct connection. +#[tokio::test] +#[traced_test] +async fn holepunch_simple() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Moderate).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Moderate).build().await?; + let server = lab.add_device("server").uplink(nat1.id()).build().await?; + let client = lab.add_device("client").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(relay_map) + .server(server, async |_dev, _ep, conn| { + conn.closed().await; + Ok(()) + }) + .client(client, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + conn.wait_ip(timeout).await.context("holepunch to direct")?; + info!("connection became direct"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +/// Adds a faster LAN interface on one side and verifies the path switches to it. +/// +/// The active side has two uplinks: eth0 (4G-impaired) and eth1 (LAN to the +/// peer's NAT, starts down). After holepunching over 4G, eth1 comes up and +/// the selected path should switch to the faster LAN link. +async fn run_add_faster_link(active_side: Side) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat_a = lab.add_router("nat_a").nat(Nat::Moderate).build().await?; + let nat_b = lab.add_router("nat_b").nat(Nat::Moderate).build().await?; + + let active = lab + .add_device("active") + .iface( + "eth0", + IfaceConfig::routed(nat_a.id()) + .condition(LinkCondition::mobile_4g(), LinkDirection::Both), + ) + .iface("eth1", IfaceConfig::routed(nat_b.id()).down()) + .build() + .await?; + let passive = lab + .add_device("passive") + .iface("eth0", nat_b.id()) + .build() + .await?; + + let timeout = Duration::from_secs(15); + Pair::new(relay_map) + .left(active_side, active, async move |dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + let first = conn + .wait_ip(timeout) + .await + .context("did not become direct")?; + info!(addr=?first, "connection became direct"); + ping_accept(&conn, timeout) + .await + .context("ping_accept before switch")?; + + info!("bring up faster link (eth1)"); + dev.iface("eth1").unwrap().link_up().await?; + + let next = conn + .wait_selected(timeout, |p| p.is_ip() && p.remote_addr() != &first) + .await + .context("did not switch paths")?; + info!(addr=?next, "new direct path established"); + ping_accept(&conn, timeout) + .await + .context("ping_accept after switch")?; + + conn.closed().await; + Ok(()) + }) + .right(passive, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + let first = conn + .wait_ip(timeout) + .await + .context("did not become direct")?; + info!(addr=?first, "connection became direct"); + ping_open(&conn, timeout) + .await + .context("ping_open before switch")?; + + let next = conn + .wait_selected(timeout, |p| p.is_ip() && p.remote_addr() != &first) + .await + .context("did not switch paths")?; + info!(addr=?next, "new direct path established"); + ping_open(&conn, timeout) + .await + .context("ping_open after switch")?; + + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn add_faster_link_client() -> Result { + run_add_faster_link(Side::Client).await +} + +#[tokio::test] +#[traced_test] +async fn add_faster_link_server() -> Result { + run_add_faster_link(Side::Server).await +} + +/// Takes one side's link down after holepunching, then brings it back. +/// +/// After recovery, verifies connectivity (via relay fallback or re-established +/// direct path), then waits for a direct path to be selected again. +async fn run_link_outage_recovery(outage_side: Side, downtime: Duration) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Moderate).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Moderate).build().await?; + let outage = lab.add_device("outage").uplink(nat1.id()).build().await?; + let peer = lab.add_device("peer").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(15); + Pair::new(relay_map) + .left(outage_side, outage, async move |dev, _ep, conn| { + conn.wait_ip(timeout).await.context("initial holepunch")?; + info!("holepunched, now killing link for {downtime:?}"); + dev.iface("eth0").unwrap().link_down().await?; + tokio::time::sleep(downtime).await; + dev.iface("eth0").unwrap().link_up().await?; + info!("link restored, waiting for recovery"); + + ping_open(&conn, timeout) + .await + .context("ping_open after link_up")?; + info!("connection recovered after link outage"); + + conn.wait_ip(timeout) + .await + .context("did not re-establish direct path")?; + ping_open(&conn, timeout) + .await + .context("ping_open after direct")?; + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .right(peer, async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await.context("ping_accept 1")?; + ping_accept(&conn, timeout).await.context("ping_accept 2")?; + conn.closed().await; + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn link_outage_recovery_client() -> Result { + run_link_outage_recovery(Side::Client, Duration::from_secs(5)).await +} + +#[tokio::test] +#[traced_test] +async fn link_outage_recovery_server() -> Result { + run_link_outage_recovery(Side::Server, Duration::from_secs(5)).await +} + +/// Starts one side behind a symmetric NAT (no holepunch possible), then replugs +/// it to a Home NAT and verifies a direct path is established. +async fn run_hard_nat_to_holepunchable(replug_side: Side) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat_easy = lab + .add_router("nat_easy") + .nat(Nat::Moderate) + .build() + .await?; + let nat_hard = lab.add_router("nat_hard").nat(Nat::Strict).build().await?; + let nat_peer = lab + .add_router("nat_peer") + .nat(Nat::Moderate) + .build() + .await?; + + let replug = lab + .add_device("replug") + .uplink(nat_hard.id()) + .build() + .await?; + let stable = lab + .add_device("stable") + .uplink(nat_peer.id()) + .build() + .await?; + + let timeout = Duration::from_secs(15); + Pair::new(relay_map) + .left(replug_side, replug, async move |dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + + ping_accept(&conn, timeout) + .await + .context("ping 1 (relay)")?; + + tokio::time::sleep(Duration::from_secs(3)).await; + assert!( + conn.paths() + .iter() + .find(|p| p.is_selected()) + .expect("no selected path") + .is_relay(), + "should still be relayed behind symmetric NAT" + ); + + info!("replug to holepunchable NAT"); + dev.iface("eth0").unwrap().replug(nat_easy.id()).await?; + + conn.wait_ip(timeout) + .await + .context("did not become direct after replug")?; + info!("connection became direct"); + + ping_accept(&conn, timeout) + .await + .context("ping 2 (direct)")?; + conn.closed().await; + Ok(()) + }) + .right(stable, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_open(&conn, timeout).await.context("ping 1 (relay)")?; + conn.wait_ip(timeout) + .await + .context("did not become direct after replug")?; + info!("connection became direct"); + ping_open(&conn, timeout).await.context("ping 2 (direct)")?; + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn hard_nat_to_holepunchable_client() -> Result { + run_hard_nat_to_holepunchable(Side::Client).await +} + +#[tokio::test] +#[traced_test] +async fn hard_nat_to_holepunchable_server() -> Result { + run_hard_nat_to_holepunchable(Side::Server).await +} + +/// Holepunching succeeds despite many unreachable local addresses on one side. +async fn run_holepunch_many_addrs(many_addrs_side: Side, addr_count: u8) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Moderate).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Moderate).build().await?; + + let mut builder = lab.add_device("many_addrs").uplink(nat1.id()); + for i in 0..addr_count { + builder = builder.iface( + &format!("virt{i}"), + IfaceConfig::dummy().addr(Ipv4Net::new_assert(Ipv4Addr::new(172, 16, 0, i + 1), 24)), + ); + } + let many_addrs = builder.build().await?; + let plain = lab.add_device("plain").uplink(nat2.id()).build().await?; + + let timeout = Duration::from_secs(15); + Pair::new(relay_map) + .left(many_addrs_side, many_addrs, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + conn.wait_ip(timeout) + .await + .context("holepunch to direct with many addrs")?; + info!("connection became direct"); + ping_accept(&conn, timeout).await.context("ping_accept")?; + conn.closed().await; + Ok(()) + }) + .right(plain, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + conn.wait_ip(timeout) + .await + .context("holepunch to direct with many addrs")?; + info!("connection became direct"); + ping_open(&conn, timeout).await.context("ping_accept")?; + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn holepunch_many_addrs_client_8() -> Result { + run_holepunch_many_addrs(Side::Client, 8).await +} + +#[tokio::test] +#[traced_test] +async fn holepunch_many_addrs_server_8() -> Result { + run_holepunch_many_addrs(Side::Server, 8).await +} + +#[tokio::test] +#[traced_test] +async fn holepunch_many_addrs_client_16() -> Result { + run_holepunch_many_addrs(Side::Client, 16).await +} + +#[tokio::test] +#[traced_test] +async fn holepunch_many_addrs_server_16() -> Result { + run_holepunch_many_addrs(Side::Server, 16).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing"] +async fn holepunch_many_addrs_client_32() -> Result { + run_holepunch_many_addrs(Side::Client, 32).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing"] +async fn holepunch_many_addrs_server_32() -> Result { + run_holepunch_many_addrs(Side::Server, 32).await +} diff --git a/vendor/iroh/tests/patchbay/degrade.rs b/vendor/iroh/tests/patchbay/degrade.rs new file mode 100644 index 0000000..bcdb712 --- /dev/null +++ b/vendor/iroh/tests/patchbay/degrade.rs @@ -0,0 +1,309 @@ +//! Degradation ladder: find where holepunching breaks under worsening conditions + +use std::time::Duration; + +use iroh::endpoint::Side; +use n0_error::{Result, StackResultExt, StdResultExt}; +use n0_tracing_test::traced_test; +use patchbay::{LinkCondition, LinkDirection, Nat}; +use testdir::testdir; +use tracing::info; + +use super::util::{Pair, PathConnectionExt, lab_with_relay, ping_accept, ping_open}; + +/// A ladder of increasingly degraded real-world links, used to find where +/// hole-punching breaks. +/// +/// Each rung models a plausible last-mile scenario rather than a synthetic +/// ramp. Loss is bursty (Gilbert-Elliott), the way real radio links drop +/// packets in fades and handovers, rather than independent per-packet loss, and +/// the mean burst grows as conditions worsen. Every rung is bandwidth-capped +/// with a buffer sized to its round-trip time (via [`rtt_ms`]), so queueing +/// delay and congestion loss appear as they do on a real bottleneck. +/// +/// The first rungs walk through common degraded links, from good wifi to a +/// congested cellular connection, with loss rates that track measured medians: +/// good LTE and wifi sit under 1 %, cell-edge and congested links reach a few +/// percent. The last three cover distinct extremes: a geostationary satellite +/// (defined by its ~600 ms round trip), a barely-usable link at the edge of +/// coverage, and an absurd stress case past what any real link sustains, there +/// to find the point where the connection gives up entirely. Reordering, which +/// real links rarely exhibit above a fraction of a percent, is dropped. +/// +/// [`rtt_ms`]: patchbay::LinkCondition::rtt_ms +const DEGRADE_LEVELS: &[LinkCondition] = &[ + // 0: good home wifi, 5 GHz, close to the access point. + LinkCondition::new() + .rate_mbit(100) + .rtt_ms(16) + .bursty_loss(0.1) + .label("good-wifi"), + // 1: congested 2.4 GHz wifi, interference and distance. + LinkCondition::new() + .rate_mbit(25) + .rtt_ms(40) + .jitter_ms(12) + .loss_pct(1.0) + .loss_burst_pkts(6) + .label("congested-wifi"), + // 2: LTE at the cell edge, weak signal. + LinkCondition::new() + .rate_mbit(8) + .rtt_ms(90) + .jitter_ms(15) + .loss_pct(2.0) + .loss_burst_pkts(8) + .label("weak-4g"), + // 3: degraded 3G, deep buffers (bufferbloat). + LinkCondition::new() + .rate_mbit(3) + .rtt_ms(200) + .jitter_ms(30) + .loss_pct(3.0) + .loss_burst_pkts(10) + .label("slow-3g"), + // 4: oversubscribed cellular under load, heavy bufferbloat. + LinkCondition::new() + .rate_kbit(1500) + .rtt_ms(300) + .jitter_ms(40) + .loss_pct(5.0) + .loss_burst_pkts(12) + .label("congested-cellular"), + // 5: a very bad cellular link, such as a moving vehicle in poor coverage: + // low bandwidth, high latency, and heavy bursty loss. + LinkCondition::new() + .rate_kbit(800) + .rtt_ms(450) + .jitter_ms(60) + .loss_pct(8.0) + .loss_burst_pkts(20) + .label("very-bad-cellular"), + // 6: geostationary satellite (Viasat / HughesNet class). The ~600 ms round + // trip is the defining impairment; loss stays modest. + LinkCondition::new() + .rate_mbit(25) + .rtt_ms(600) + .jitter_ms(40) + .loss_pct(1.5) + .loss_burst_pkts(6) + .label("satellite"), + // 7: barely-usable cellular or wifi at the edge of coverage. Low bandwidth, + // heavy bufferbloat, and long loss bursts. + LinkCondition::new() + .rate_kbit(500) + .rtt_ms(450) + .jitter_ms(80) + .loss_pct(12.0) + .loss_burst_pkts(25) + .label("barely-usable"), + // 8: absurd stress case, past what any real link sustains, to find where the + // connection gives up entirely. + LinkCondition::new() + .rate_kbit(256) + .rtt_ms(800) + .jitter_ms(150) + .loss_pct(25.0) + .loss_burst_pkts(40) + .label("absurd"), +]; + +/// Runs a single degradation level. +/// +/// Creates two devices behind Home NATs, applies the given [`LinkCondition`] to +/// `impaired_side`, then attempts to holepunch and ping. Returns the +/// [`TestGuard`] on success so the caller can mark it as passed. +async fn run_degrade_level(impaired_side: Side, level: usize) -> Result<()> { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Moderate).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Moderate).build().await?; + let timeout = Duration::from_secs(20 + level as u64 * 10); + + let limits = DEGRADE_LEVELS[level]; + + let server = lab + .add_device("server") + .iface("eth0", nat1.id()) + .build() + .await?; + let client = lab + .add_device("client") + .iface("eth0", nat2.id()) + .build() + .await?; + let impaired_device = match impaired_side { + Side::Client => &client, + Side::Server => &server, + }; + impaired_device + .iface("eth0") + .unwrap() + .set_condition(limits, LinkDirection::Both) + .await?; + + info!(?impaired_side, ?limits, %level, ?timeout, "degrade test start"); + + let result = tokio::time::timeout( + timeout, + Pair::new(relay_map) + .server(server, async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await.context("ping_accept")?; + conn.closed().await; + Ok(()) + }) + .client(client, async move |_dev, _ep, conn| { + info!("waiting for connection to become direct"); + conn.wait_ip(timeout).await.context("holepunch to direct")?; + info!("direct path established, sending ping"); + ping_open(&conn, timeout).await.context("ping_open")?; + info!("ping complete"); + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run(), + ) + .await + .std_context("pair timed out") + .flatten(); + + match &result { + Ok(()) => tracing::event!( + target: "test::_events::ladder_pass", + tracing::Level::INFO, + level, + latency_ms = limits.latency_ms, + loss_pct = limits.loss_pct, + loss_burst_pkts = ?limits.loss_burst_pkts, + impaired_side = ?impaired_side, + "PASSED", + ), + Err(err) => tracing::event!( + target: "test::_events::ladder_fail", + tracing::Level::WARN, + level, + latency_ms = limits.latency_ms, + loss_pct = limits.loss_pct, + loss_burst_pkts = ?limits.loss_burst_pkts, + impaired_side = ?impaired_side, + error = format!("{err:#}"), + "FAILED", + ), + } + + result?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn degrade_server_good_wifi() -> Result { + run_degrade_level(Side::Server, 0).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_server_congested_wifi() -> Result { + run_degrade_level(Side::Server, 1).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_server_weak_4g() -> Result { + run_degrade_level(Side::Server, 2).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_server_slow_3g() -> Result { + run_degrade_level(Side::Server, 3).await +} + +// Still too flaky. +// #[tokio::test] +// #[traced_test] +// async fn degrade_server_congested_cellular() -> Result { +// run_degrade_level(Side::Server, 4).await +// } + +#[tokio::test] +#[traced_test] +async fn degrade_server_very_bad_cellular() -> Result { + run_degrade_level(Side::Server, 5).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_server_satellite() -> Result { + run_degrade_level(Side::Server, 6).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing"] +async fn degrade_server_barely_usable() -> Result { + run_degrade_level(Side::Server, 7).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing"] +async fn degrade_server_absurd() -> Result { + run_degrade_level(Side::Server, 8).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_good_wifi() -> Result { + run_degrade_level(Side::Client, 0).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_congested_wifi() -> Result { + run_degrade_level(Side::Client, 1).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_weak_4g() -> Result { + run_degrade_level(Side::Client, 2).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_slow_3g() -> Result { + run_degrade_level(Side::Client, 3).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_congested_cellular() -> Result { + run_degrade_level(Side::Client, 4).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_very_bad_cellular() -> Result { + run_degrade_level(Side::Client, 5).await +} + +#[tokio::test] +#[traced_test] +async fn degrade_client_satellite() -> Result { + run_degrade_level(Side::Client, 6).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing"] +async fn degrade_client_barely_usable() -> Result { + run_degrade_level(Side::Client, 7).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing"] +async fn degrade_client_absurd() -> Result { + run_degrade_level(Side::Client, 8).await +} diff --git a/vendor/iroh/tests/patchbay/nat.rs b/vendor/iroh/tests/patchbay/nat.rs new file mode 100644 index 0000000..9e5f5e4 --- /dev/null +++ b/vendor/iroh/tests/patchbay/nat.rs @@ -0,0 +1,174 @@ +//! NAT traversal matrix tests. +//! +//! Tests holepunching across combinations of the upstream [`patchbay::Nat`] +//! behavior tiers: +//! +//! - `None`: no NAT, publicly routable. +//! - `Open`: endpoint-independent mapping (EIM), endpoint-independent +//! filtering (EIF); RFC 3489 full cone. Typical of routers with UPnP or +//! static port forwarding. +//! - `Moderate`: EIM, address-and-port-dependent filtering (APDF); RFC 3489 +//! port-restricted cone. The typical home router. +//! - `Strict`: endpoint-dependent mapping (EDM) with random ports, APDF; RFC +//! 3489 symmetric. Holepunching between two `Strict` NATs requires a relay. +//! +//! Every test expects a direct path to be established. Tests where holepunching +//! is not yet working are marked `#[ignore]`. + +use std::time::Duration; + +use n0_error::{Result, StackResultExt}; +use n0_tracing_test::traced_test; +use patchbay::Nat; +use testdir::testdir; +use tracing::info; + +use super::util::{Pair, PathConnectionExt, lab_with_relay}; +use crate::util::{is_relayed, ping_accept, ping_open}; + +async fn run_nat_holepunch(nat_server: Nat, nat_client: Nat) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let router_server = lab.add_router("nat_server").nat(nat_server).build().await?; + let router_client = lab.add_router("nat_client").nat(nat_client).build().await?; + let server = lab + .add_device("server") + .uplink(router_server.id()) + .build() + .await?; + let client = lab + .add_device("client") + .uplink(router_client.id()) + .build() + .await?; + + let timeout = Duration::from_secs(15); + Pair::new(relay_map) + .server(server, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + conn.wait_ip(timeout).await.context("holepunch to direct")?; + info!("connection became direct"); + ping_accept(&conn, timeout).await?; + conn.closed().await; + Ok(()) + }) + .client(client, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + conn.wait_ip(timeout).await.context("holepunch to direct")?; + info!("connection became direct"); + ping_open(&conn, timeout).await?; + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + + guard.ok(); + Ok(()) +} + +// None x * + +#[tokio::test] +#[traced_test] +async fn nat_none_x_none() -> Result { + run_nat_holepunch(Nat::None, Nat::None).await +} + +#[tokio::test] +#[traced_test] +async fn nat_none_x_open() -> Result { + run_nat_holepunch(Nat::None, Nat::Open).await +} + +#[tokio::test] +#[traced_test] +async fn nat_none_x_moderate() -> Result { + run_nat_holepunch(Nat::None, Nat::Moderate).await +} + +#[tokio::test] +#[traced_test] +async fn nat_none_x_strict() -> Result { + run_nat_holepunch(Nat::None, Nat::Strict).await +} + +// Open x * + +#[tokio::test] +#[traced_test] +async fn nat_open_x_none() -> Result { + run_nat_holepunch(Nat::Open, Nat::None).await +} + +#[tokio::test] +#[traced_test] +async fn nat_open_x_open() -> Result { + run_nat_holepunch(Nat::Open, Nat::Open).await +} + +#[tokio::test] +#[traced_test] +async fn nat_open_x_moderate() -> Result { + run_nat_holepunch(Nat::Open, Nat::Moderate).await +} + +#[tokio::test] +#[traced_test] +async fn nat_open_x_strict() -> Result { + run_nat_holepunch(Nat::Open, Nat::Strict).await +} + +// Moderate x * + +#[tokio::test] +#[traced_test] +async fn nat_moderate_x_none() -> Result { + run_nat_holepunch(Nat::Moderate, Nat::None).await +} + +#[tokio::test] +#[traced_test] +async fn nat_moderate_x_open() -> Result { + run_nat_holepunch(Nat::Moderate, Nat::Open).await +} + +#[tokio::test] +#[traced_test] +async fn nat_moderate_x_moderate() -> Result { + run_nat_holepunch(Nat::Moderate, Nat::Moderate).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing (and likely can't without port guessing)"] +async fn nat_moderate_x_strict() -> Result { + run_nat_holepunch(Nat::Moderate, Nat::Strict).await +} + +// Strict x * + +#[tokio::test] +#[traced_test] +async fn nat_strict_x_none() -> Result { + run_nat_holepunch(Nat::Strict, Nat::None).await +} + +#[tokio::test] +#[traced_test] +async fn nat_strict_x_open() -> Result { + run_nat_holepunch(Nat::Strict, Nat::Open).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing (and likely can't without port guessing)"] +async fn nat_strict_x_moderate() -> Result { + run_nat_holepunch(Nat::Strict, Nat::Moderate).await +} + +#[tokio::test] +#[traced_test] +#[ignore = "not yet passing (and likely can't without port guessing)"] +async fn nat_strict_x_strict() -> Result { + run_nat_holepunch(Nat::Strict, Nat::Strict).await +} diff --git a/vendor/iroh/tests/patchbay/relay.rs b/vendor/iroh/tests/patchbay/relay.rs new file mode 100644 index 0000000..fabb928 --- /dev/null +++ b/vendor/iroh/tests/patchbay/relay.rs @@ -0,0 +1,348 @@ +//! Relay connectivity and reconnect tests. +//! +//! The relay from [`util::relay::run_relay_server`] binds `[::]` and is +//! reachable as `https://relay.test` through lab-wide DNS A and AAAA records. +//! +//! The connect tests place both peers on access networks that support only +//! one IP family, so all traffic to the relay uses that family. The +//! reconnect tests pin the connection to the relay by putting both peers +//! behind symmetric (`Nat::Strict`) NATs, for which holepunching fails +//! even though IP transports stay enabled, and then break either one +//! peer's access link or the relay server itself. + +use std::time::Duration; + +use iroh::endpoint::Side; +use n0_error::{Result, StackResultExt, StdResultExt, anyerr}; +use n0_future::task::AbortOnDropHandle; +use n0_tracing_test::traced_test; +use patchbay::{FirewallConfigBuilder, IpSupport, Lab, Nat, OutDir}; +use testdir::testdir; +use tokio::sync::oneshot; +use tracing::info; + +use super::util::{self, Pair, is_relayed, lab_with_relay, ping_accept, ping_open}; + +/// Firewall rules that block all outbound UDP except DNS, leaving TCP open. +/// +/// The lab DNS server is UDP-only, so port 53 must stay open. Everything else +/// over UDP (QAD, holepunch probes) is dropped, forcing traffic onto the relay +/// over TCP. Pass to [`RouterBuilder::firewall_custom`](patchbay::RouterBuilder). +fn block_udp_except_dns(f: &mut FirewallConfigBuilder) -> &mut FirewallConfigBuilder { + f.block_inbound().allow_udp(&[53]) +} + +/// Connects two peers through the relay over a single IP family. +/// +/// The relay's own network is dual-stack; both access routers support only +/// the family under test. Their firewall drops all UDP except DNS, so QAD +/// and holepunching are impossible and the relay is the only usable path: +/// the ping exchange proves the relay carries data over that family. +/// Direct-path coverage per family lives in the switch_uplink and nat +/// matrices. +async fn run_relay_connect(ip_support: IpSupport) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let access1 = lab + .add_router("access1") + .ip_support(ip_support) + .firewall_custom(block_udp_except_dns) + .build() + .await?; + let access2 = lab + .add_router("access2") + .ip_support(ip_support) + .firewall_custom(block_udp_except_dns) + .build() + .await?; + let server = lab + .add_device("server") + .uplink(access1.id()) + .build() + .await?; + let client = lab + .add_device("client") + .uplink(access2.id()) + .build() + .await?; + // Sanity-check the harness: only the family under test is assigned. + for dev in [&server, &client] { + assert_eq!(dev.ip().is_some(), ip_support.has_v4(), "IPv4 assignment"); + assert_eq!(dev.ip6().is_some(), ip_support.has_v6(), "IPv6 assignment"); + } + let timeout = Duration::from_secs(10); + Pair::new(relay_map) + .server(server, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_accept(&conn, timeout).await.context("ping_accept")?; + assert!(is_relayed(&conn), "still relayed with UDP blocked"); + conn.closed().await; + Ok(()) + }) + .client(client, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_open(&conn, timeout).await.context("ping_open")?; + assert!(is_relayed(&conn), "still relayed with UDP blocked"); + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn relay_connect_ipv4() -> Result { + run_relay_connect(IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn relay_connect_ipv6() -> Result { + run_relay_connect(IpSupport::V6Only).await +} + +/// Which peers sit behind a UDP-blocking access router. +#[derive(Debug, Clone, Copy)] +enum UdpBlocked { + Both, + ServerOnly, + ClientOnly, +} + +/// Connects two peers of which one or both cannot use UDP beyond DNS. +/// +/// The blocked peers sit behind a home NAT that drops unsolicited inbound and +/// all outbound UDP except DNS (the lab DNS server is UDP-only), the hotel or +/// airport guest WiFi shape. That rules out QAD and holepunching, so they must +/// reach each other through the relay over TCP. Any unblocked peer is behind a +/// plain home NAT; a direct path needs UDP on both ends, so the connection +/// stays relayed in every variant. +async fn run_relay_udp_blocked(blocked: UdpBlocked) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let server_blocked = matches!(blocked, UdpBlocked::Both | UdpBlocked::ServerOnly); + let client_blocked = matches!(blocked, UdpBlocked::Both | UdpBlocked::ClientOnly); + let mut net1 = lab.add_router("net1").nat(Nat::Moderate); + if server_blocked { + net1 = net1.firewall_custom(block_udp_except_dns); + } + let net1 = net1.build().await?; + let mut net2 = lab.add_router("net2").nat(Nat::Moderate); + if client_blocked { + net2 = net2.firewall_custom(block_udp_except_dns); + } + let net2 = net2.build().await?; + let server = lab.add_device("server").uplink(net1.id()).build().await?; + let client = lab.add_device("client").uplink(net2.id()).build().await?; + + // Long enough for a holepunch to complete if one were possible. + let hold = Duration::from_secs(3); + let timeout = Duration::from_secs(15); + Pair::new(relay_map) + .server(server, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_accept(&conn, timeout).await.context("ping 1")?; + tokio::time::sleep(hold).await; + assert!(is_relayed(&conn), "still relayed with UDP blocked"); + ping_accept(&conn, timeout).await.context("ping 2")?; + conn.closed().await; + Ok(()) + }) + .client(client, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_open(&conn, timeout).await.context("ping 1")?; + tokio::time::sleep(hold).await; + assert!(is_relayed(&conn), "still relayed with UDP blocked"); + ping_open(&conn, timeout).await.context("ping 2")?; + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn relay_udp_blocked_both() -> Result { + run_relay_udp_blocked(UdpBlocked::Both).await +} + +#[tokio::test] +#[traced_test] +async fn relay_udp_blocked_server() -> Result { + run_relay_udp_blocked(UdpBlocked::ServerOnly).await +} + +#[tokio::test] +#[traced_test] +async fn relay_udp_blocked_client() -> Result { + run_relay_udp_blocked(UdpBlocked::ClientOnly).await +} + +/// Takes one peer's link down and verifies the relay path recovers. +/// +/// Both peers sit behind symmetric NATs, so the connection cannot escape to +/// a direct path; the affected peer's relay client notices the dead link and +/// reconnects once the link is back. +async fn run_relay_reconnect_link_outage(outage_side: Side, downtime: Duration) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Strict).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Strict).build().await?; + let outage = lab.add_device("outage").uplink(nat1.id()).build().await?; + let peer = lab.add_device("peer").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(15); + // The reconnect backoff can sleep through the link returning: dials during + // the outage fail fast and grow the backoff to several seconds plus jitter, + // so the post-outage pings need generous headroom. + let recovery_timeout = Duration::from_secs(30); + Pair::new(relay_map) + .left(outage_side, outage, async move |dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_open(&conn, timeout) + .await + .context("ping before outage")?; + info!("killing link for {downtime:?}"); + dev.iface("eth0").unwrap().link_down().await?; + tokio::time::sleep(downtime).await; + dev.iface("eth0").unwrap().link_up().await?; + info!("link restored, waiting for relay reconnect"); + ping_open(&conn, recovery_timeout) + .await + .context("ping after link restored")?; + assert!(is_relayed(&conn), "still relayed behind symmetric NAT"); + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .right(peer, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_accept(&conn, timeout) + .await + .context("ping before outage")?; + ping_accept(&conn, recovery_timeout) + .await + .context("ping after link restored")?; + conn.closed().await; + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn relay_reconnect_link_outage_client() -> Result { + run_relay_reconnect_link_outage(Side::Client, Duration::from_secs(5)).await +} + +#[tokio::test] +#[traced_test] +async fn relay_reconnect_link_outage_server() -> Result { + run_relay_reconnect_link_outage(Side::Server, Duration::from_secs(5)).await +} + +/// Restarts the relay server underneath an established relay-pinned +/// connection and verifies data flows again once the clients reconnect. +/// +/// The replacement relay binds the same ports and is reachable under the +/// same `https://relay.test` URL, like a relay deployment restarting in +/// place. Both peers' relay actors must notice the dead connection and +/// reconnect on their own. +#[tokio::test] +#[traced_test] +async fn relay_reconnect_relay_restart() -> Result { + let mut builder = Lab::builder().outdir(OutDir::Exact(testdir!())); + if let Some(name) = std::thread::current().name() { + builder = builder.label(name); + } + let lab = builder.build().await?; + let guard = lab.test_guard(); + + let dc = lab + .add_router("dc") + .ip_support(IpSupport::DualStack) + .build() + .await?; + let dev_relay = lab.add_device("relay").uplink(dc.id()).build().await?; + // Register DNS before the endpoint devices below; only later devices + // resolve it. + let dns = lab.dns_server()?; + dns.set_host("relay.test", dev_relay.ip().expect("relay has IPv4").into())?; + dns.set_host( + "relay.test", + dev_relay.ip6().expect("relay has IPv6").into(), + )?; + + let (map_tx, map_rx) = oneshot::channel(); + let (restart_tx, restart_rx) = oneshot::channel::<()>(); + let (restarted_tx, restarted_rx) = oneshot::channel::<()>(); + let relay_task = dev_relay.spawn(async move |_dev| { + let (relay_map, server) = util::relay::run_relay_server().await.expect("relay spawn"); + map_tx.send(relay_map).expect("test task alive"); + restart_rx.await.expect("restart signal"); + server.shutdown().await.expect("relay shutdown"); + info!("relay stopped, starting a new relay on the same ports"); + // The old server's sockets are released asynchronously after + // shutdown() returns; retry briefly if a port is still taken. + let mut attempts = 0; + let (_relay_map, _server) = loop { + match util::relay::run_relay_server().await { + Ok(relay) => break relay, + Err(err) if attempts < 20 => { + attempts += 1; + info!("relay respawn attempt {attempts} failed: {err:#}"); + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(err) => panic!("relay respawn: {err:#}"), + } + }; + restarted_tx.send(()).expect("test task alive"); + std::future::pending::<()>().await; + })?; + let _relay_guard = AbortOnDropHandle::new(relay_task); + let relay_map = map_rx.await.std_context("relay task died before binding")?; + + let nat1 = lab.add_router("nat1").nat(Nat::Strict).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Strict).build().await?; + let server = lab.add_device("server").uplink(nat1.id()).build().await?; + let client = lab.add_device("client").uplink(nat2.id()).build().await?; + + let timeout = Duration::from_secs(20); + Pair::new(relay_map) + .server(server, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_accept(&conn, timeout) + .await + .context("ping before restart")?; + ping_accept(&conn, timeout) + .await + .context("ping after restart")?; + conn.closed().await; + Ok(()) + }) + .client(client, async move |_dev, _ep, conn| { + assert!(is_relayed(&conn), "connection started relayed"); + ping_open(&conn, timeout) + .await + .context("ping before restart")?; + restart_tx + .send(()) + .map_err(|_| anyerr!("relay task died"))?; + restarted_rx.await.std_context("relay did not restart")?; + info!("relay restarted, waiting for reconnect"); + ping_open(&conn, timeout) + .await + .context("ping after relay restart")?; + assert!(is_relayed(&conn), "still relayed behind symmetric NAT"); + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + guard.ok(); + Ok(()) +} diff --git a/vendor/iroh/tests/patchbay/switch-uplink.rs b/vendor/iroh/tests/patchbay/switch-uplink.rs new file mode 100644 index 0000000..77c3a5c --- /dev/null +++ b/vendor/iroh/tests/patchbay/switch-uplink.rs @@ -0,0 +1,239 @@ +//! Uplink switch tests. +//! +//! Each test verifies that an iroh connection survives a network change on one +//! side: the switching device replugs from one router to another, and we verify +//! that a new direct path is established and data flows over it. +//! +//! We test every combination of: +//! - which side switches (client or server) +//! - which IP families are involved (v4, v6, dual-stack) +//! +//! The non-switching side is always behind a dual-stack Home NAT, so it is +//! reachable on both address families regardless of what the switcher does. + +use std::time::Duration; + +use iroh::{TransportAddr, endpoint::Side}; +use n0_error::{Result, StackResultExt}; +use n0_tracing_test::traced_test; +use patchbay::{IpSupport, RouterPreset}; +use testdir::testdir; +use tracing::info; + +use crate::util::{Pair, PathConnectionExt, lab_with_relay, ping_accept, ping_open}; + +/// Builds the lab topology and runs a single uplink switch test. +/// +/// The topology has three routers: +/// - "observer": dual-stack Home NAT for the non-switching side +/// - "from": the switching side's initial router (determined by `from`) +/// - "to": the router the switching side replugs to (determined by `to`) +/// +/// After both sides holepunch and exchange a ping, the switching side replugs +/// from "from" to "to". The observer waits for the selected path to change, +/// then both sides exchange another ping to confirm the new path works. +async fn run_switch_uplink(switching_side: Side, from: IpSupport, to: IpSupport) -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let timeout = Duration::from_secs(30); + + let observer_id = lab + .add_router("observer") + .preset(RouterPreset::Home) + .ip_support(IpSupport::DualStack) + .build() + .await? + .id(); + + let from_id = lab + .add_router("from") + .preset(router_preset(from)) + .ip_support(from) + .build() + .await? + .id(); + let to_id = lab + .add_router("to") + .preset(router_preset(to)) + .ip_support(to) + .build() + .await? + .id(); + + let switcher = lab.add_device("switcher").uplink(from_id).build().await?; + let observer = lab + .add_device("observer") + .uplink(observer_id) + .build() + .await?; + + info!(?switching_side, ?from, ?to, "switch uplink test start"); + + Pair::new(relay_map) + .left(switching_side, switcher, async move |dev, _ep, conn| { + conn.wait_ip(timeout).await.context("initial holepunch")?; + ping_accept(&conn, timeout) + .await + .context("ping_accept before switch")?; + dev.iface("eth0").unwrap().replug(to_id).await?; + ping_accept(&conn, timeout) + .await + .context("ping_accept after switch")?; + conn.closed().await; + Ok(()) + }) + .right(observer, async move |_dev, _ep, conn| { + conn.wait_ip(timeout).await.context("initial holepunch")?; + let previous: Vec = conn + .paths() + .iter() + .map(|p| p.remote_addr().clone()) + .collect(); + ping_open(&conn, timeout) + .await + .context("ping_open before switch")?; + conn.wait_selected(timeout, |p| path_switched(to, &previous, p.remote_addr())) + .await + .context("path did not switch")?; + ping_open(&conn, timeout) + .await + .context("ping_open after switch")?; + conn.close(0u32.into(), b"bye"); + Ok(()) + }) + .run() + .await?; + + guard.ok(); + Ok(()) +} + +fn router_preset(ip: IpSupport) -> RouterPreset { + match ip { + IpSupport::V4Only => RouterPreset::Home, + IpSupport::V6Only => RouterPreset::IspV6, + IpSupport::DualStack => RouterPreset::Home, + } +} + +fn path_switched(to: IpSupport, previous: &[TransportAddr], new: &TransportAddr) -> bool { + if previous.contains(new) { + return false; + } + match to { + IpSupport::V4Only => matches!(new, TransportAddr::Ip(a) if a.ip().is_ipv4()), + IpSupport::V6Only => matches!(new, TransportAddr::Ip(a) if a.ip().is_ipv6()), + IpSupport::DualStack => matches!(new, TransportAddr::Ip(_)), + } +} + +// --- Client switches uplink --- + +#[tokio::test] +#[traced_test] +async fn switch_client_v4_to_v4() -> Result { + run_switch_uplink(Side::Client, IpSupport::V4Only, IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_v4_to_v6() -> Result { + run_switch_uplink(Side::Client, IpSupport::V4Only, IpSupport::V6Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_v4_to_dual() -> Result { + run_switch_uplink(Side::Client, IpSupport::V4Only, IpSupport::DualStack).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_v6_to_v4() -> Result { + run_switch_uplink(Side::Client, IpSupport::V6Only, IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_v6_to_v6() -> Result { + run_switch_uplink(Side::Client, IpSupport::V6Only, IpSupport::V6Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_v6_to_dual() -> Result { + run_switch_uplink(Side::Client, IpSupport::V6Only, IpSupport::DualStack).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_dual_to_v4() -> Result { + run_switch_uplink(Side::Client, IpSupport::DualStack, IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_dual_to_v6() -> Result { + run_switch_uplink(Side::Client, IpSupport::DualStack, IpSupport::V6Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_client_dual_to_dual() -> Result { + run_switch_uplink(Side::Client, IpSupport::DualStack, IpSupport::DualStack).await +} + +// --- Server switches uplink --- + +#[tokio::test] +#[traced_test] +async fn switch_server_v4_to_v4() -> Result { + run_switch_uplink(Side::Server, IpSupport::V4Only, IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_v4_to_v6() -> Result { + run_switch_uplink(Side::Server, IpSupport::V4Only, IpSupport::V6Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_v4_to_dual() -> Result { + run_switch_uplink(Side::Server, IpSupport::V4Only, IpSupport::DualStack).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_v6_to_v4() -> Result { + run_switch_uplink(Side::Server, IpSupport::V6Only, IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_v6_to_v6() -> Result { + run_switch_uplink(Side::Server, IpSupport::V6Only, IpSupport::V6Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_v6_to_dual() -> Result { + run_switch_uplink(Side::Server, IpSupport::V6Only, IpSupport::DualStack).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_dual_to_v4() -> Result { + run_switch_uplink(Side::Server, IpSupport::DualStack, IpSupport::V4Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_dual_to_v6() -> Result { + run_switch_uplink(Side::Server, IpSupport::DualStack, IpSupport::V6Only).await +} + +#[tokio::test] +#[traced_test] +async fn switch_server_dual_to_dual() -> Result { + run_switch_uplink(Side::Server, IpSupport::DualStack, IpSupport::DualStack).await +} diff --git a/vendor/iroh/tests/patchbay/util.rs b/vendor/iroh/tests/patchbay/util.rs new file mode 100644 index 0000000..2d5e3bc --- /dev/null +++ b/vendor/iroh/tests/patchbay/util.rs @@ -0,0 +1,531 @@ +use std::{future::Future, path::PathBuf, sync::Arc, time::Duration}; + +use iroh::{ + Endpoint, EndpointAddr, RelayMap, RelayMode, TransportAddr, + endpoint::{Connection, Path, PathEvent, presets}, + tls::CaTlsConfig, +}; +use iroh_metrics::MetricsGroupSet; +use n0_error::{Result, StackResultExt, StdResultExt, anyerr, ensure_any}; +use n0_future::{StreamExt, boxed::BoxFuture, task::AbortOnDropHandle}; +use noq::Side; +use patchbay::{Device, IpSupport, Lab, TestGuard}; +use tokio::sync::{Barrier, oneshot}; +use tracing::{Instrument, debug, error, error_span, event, info}; + +use self::relay::run_relay_server; + +const TEST_ALPN: &[u8] = b"test"; + +/// Upper bound on waiting for the peer task at the end-of-run barrier in [`Pair::run`]. +/// +/// Generous compared to the run functions' own timeouts; it only triggers when the +/// peer task died before reaching the barrier. +const BARRIER_TIMEOUT: Duration = Duration::from_secs(30); + +/// Creates a lab with a relay server. +/// +/// Returns the lab, relay map, a drop guard that keeps the relay alive, +/// and a [`TestGuard`] that records pass/fail. +/// +/// The relay binds on `[::]` and is reachable via `https://relay.test` +/// (resolved through lab-wide DNS entries for both IPv4 and IPv6). +pub(crate) async fn lab_with_relay( + outdir: PathBuf, +) -> Result<(Lab, RelayMap, AbortOnDropHandle<()>, TestGuard)> { + // `for_test` writes into `outdir`, labels the lab with the current thread + // (the test name under the default current-thread runtime), and returns the + // pass/fail guard. + let (lab, guard) = Lab::for_test(outdir).await?; + let (relay_map, relay_guard) = spawn_relay(&lab).await?; + Ok((lab, relay_map, relay_guard, guard)) +} + +/// Creates a router `dc` and device `relay` and spawns a relay server on the device. +/// +/// Also creates a lab-wide DNS entry `relay.test` that resolves to the relay server's +/// IPv4 and IPv6 addresses. +/// +/// Returns a [`RelayMap`] with an entry for the relay, and a drop handle that will +/// stop the relay server once dropped. +async fn spawn_relay(lab: &Lab) -> Result<(RelayMap, AbortOnDropHandle<()>)> { + let dc = lab + .add_router("dc") + .ip_support(IpSupport::DualStack) + .build() + .await?; + let dev_relay = lab.add_device("relay").uplink(dc.id()).build().await?; + + // Register both v4 and v6 addresses under "relay.test" lab-wide. + // Devices created after this will resolve "relay.test" to both addresses. + let relay_v4 = dev_relay.ip().expect("relay has IPv4"); + let relay_v6 = dev_relay.ip6().expect("relay has IPv6"); + let dns = lab.dns_server()?; + dns.set_host("relay.test", relay_v4.into())?; + dns.set_host("relay.test", relay_v6.into())?; + info!(%relay_v4, %relay_v6, "DNS entries for relay.test registered"); + + let (relay_map_tx, relay_map_rx) = oneshot::channel(); + let task_relay = dev_relay.spawn(async move |_ctx| { + let (relay_map, _server) = run_relay_server().await.unwrap(); + relay_map_tx.send(relay_map).unwrap(); + std::future::pending::<()>().await; + })?; + let relay_map = relay_map_rx.await.unwrap(); + Ok((relay_map, AbortOnDropHandle::new(task_relay))) +} + +/// Type alias for boxed run functions used in [`Pair`]. +type RunFn = Box BoxFuture>; + +fn box_fn(f: F) -> RunFn +where + F: FnOnce(Device, Endpoint, Connection) -> Fut + Send + 'static, + Fut: Future + Send + 'static, +{ + Box::new(move |dev, ep, conn| Box::pin(f(dev, ep, conn))) +} + +/// Builder for two connected endpoints in a lab. +/// +/// Use this to quickly create two endpoints on two different devices and create a +/// connection between them that starts as relay-only. +/// +/// Two construction paths: +/// +/// ```ignore +/// // Explicit server/client assignment: +/// Pair::new(relay_map) +/// .server(server_dev, async |dev, ep, conn| { ... }) +/// .client(client_dev, async |dev, ep, conn| { ... }) +/// .run().await?; +/// +/// // Side-swapped assignment (for matrix tests): +/// Pair::new(relay_map) +/// .left(some_side, dev_a, async |dev, ep, conn| { ... }) +/// .right(dev_b, async |dev, ep, conn| { ... }) +/// .run().await?; +/// ``` +pub(crate) struct Pair { + relay_map: RelayMap, + server_dev: Option, + client_dev: Option, + server_fn: Option, + client_fn: Option, +} + +impl Pair { + /// Creates a new pair builder with a shared [`RelayMap`]. + pub(crate) fn new(relay_map: RelayMap) -> Self { + Self { + relay_map, + server_dev: None, + client_dev: None, + server_fn: None, + client_fn: None, + } + } + + /// Places a device and closure on the given [`Side`]. + /// + /// Use with [`.right()`](Self::right) for matrix tests that swap sides. + pub(crate) fn left(mut self, side: Side, device: Device, run_fn: F) -> Self + where + F: FnOnce(Device, Endpoint, Connection) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + let (dev_slot, fn_slot) = match side { + Side::Server => (&mut self.server_dev, &mut self.server_fn), + Side::Client => (&mut self.client_dev, &mut self.client_fn), + }; + *dev_slot = Some(device); + *fn_slot = Some(box_fn(run_fn)); + self + } + + /// Places a device and closure on whichever [`Side`] was not set by [`.left()`](Self::left). + pub(crate) fn right(self, device: Device, run_fn: F) -> Self + where + F: FnOnce(Device, Endpoint, Connection) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + let remaining = match (&self.server_dev, &self.client_dev) { + (Some(_), None) => Side::Client, + (None, Some(_)) => Side::Server, + (None, None) => panic!("call .left() before .right()"), + (Some(_), Some(_)) => panic!("both sides already assigned"), + }; + self.left(remaining, device, run_fn) + } + + /// Sets the server device and run function. + pub(crate) fn server(mut self, device: Device, run_fn: F) -> Self + where + F: FnOnce(Device, Endpoint, Connection) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + self.server_dev = Some(device); + self.server_fn = Some(box_fn(run_fn)); + self + } + + /// Sets the client device and run function. + pub(crate) fn client(mut self, device: Device, run_fn: F) -> Self + where + F: FnOnce(Device, Endpoint, Connection) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + self.client_dev = Some(device); + self.client_fn = Some(box_fn(run_fn)); + self + } + + /// Runs the pair to completion. + /// + /// This will bind an endpoint on each device, wait for the server endpoint to be online, + /// then send a relay-only [`EndpointAddr`] to the client task. + /// The client task will connect to the server, and the server will accept a connection. + /// Once a connection is established on either side, its run function is invoked. + /// Once both run functions completed, the endpoints are dropped without awaiting + /// [`Endpoint::close`], so the corresponding ERROR logs are expected. + /// + /// After completion, this will: + /// - log the result of the run functions + /// - record the endpoint metrics as a `patchbay::_metrics` tracing event + /// - emit a `test::_events::pass` or `test::_events::fail` event for each device + /// + /// Returns an error if any step or run function failed. + pub(crate) async fn run(mut self) -> Result { + let server_device = self.server_dev.take().context("Missing server device")?; + let server_run = self + .server_fn + .take() + .context("Missing server run function")?; + let client_device = self.client_dev.take().context("Missing client device")?; + let client_run = self + .client_fn + .take() + .context("Missing client run function")?; + + let (addr_tx, addr_rx) = oneshot::channel(); + let relay_map2 = self.relay_map.clone(); + + // Create an in-memory synchronization barrier to wait for both run functions to complete + // before dropping endpoints. We use this to guarantee completion without awaiting + // `Endpoint::close` on both sides. `Endpoint::close` often takes several seconds, + // which increases test runtime for all tests significantly, and closing behavior + // should be tested for separately from the tests that use `Pair`. + // + // The barrier waits are bounded: a task that fails or panics before reaching the + // barrier would otherwise hang the healthy side until the nextest timeout kills + // the whole test instead of letting it report the failure. + let barrier_server = Arc::new(Barrier::new(2)); + let barrier_client = barrier_server.clone(); + + let server_task = server_device.spawn(|dev| { + async move { + let endpoint = endpoint_builder(&dev, relay_map2) + .bind() + .await + .context("server endpoint bind")?; + info!( + id=%endpoint.id().fmt_short(), + bound_sockets=?endpoint.bound_sockets(), + "server endpoint bound", + ); + endpoint.online().await; + info!("endpoint online"); + + // Send address to client task. Make it a relay-only address, + // like in the default address lookup services. + addr_tx.send(addr_relay_only(endpoint.addr())).unwrap(); + let incoming = endpoint.accept().await.context("server accept incoming")?; + let conn = incoming + .accept() + .anyerr()? + .await + .context("server accept handshake")?; + + info!(remote=%conn.remote_id().fmt_short(), "accepted, executing run function"); + watch_selected_path(&conn); + let res = server_run(dev.clone(), endpoint.clone(), conn).await; + match &res { + Ok(()) => info!("run function completed successfully"), + Err(err) => error!("run function failed: {err:#}"), + } + + // Wait until the client run function completed before dropping the endpoint. + let _ = tokio::time::timeout(BARRIER_TIMEOUT, barrier_server.wait()).await; + for group in endpoint.metrics().groups() { + dev.record_iroh_metrics(group); + } + res + } + .instrument(error_span!("ep-server")) + })?; + let client_task = client_device.spawn(move |dev| { + async move { + let endpoint = endpoint_builder(&dev, self.relay_map) + .bind() + .await + .context("client endpoint bind")?; + info!( + id=%endpoint.id().fmt_short(), + bound_sockets=?endpoint.bound_sockets(), + "client endpoint bound", + ); + + let addr = addr_rx + .await + .std_context("server did not send its address")?; + info!(?addr, "connecting to server"); + let conn = endpoint + .connect(addr, TEST_ALPN) + .await + .context("client connect")?; + watch_selected_path(&conn); + info!( + remote=%conn.remote_id().fmt_short(), + "connected, executing run function", + ); + + let res = client_run(dev.clone(), endpoint.clone(), conn).await; + match &res { + Ok(()) => info!("run function completed successfully"), + Err(err) => error!("run function failed: {err:#}"), + } + + // Wait until the server run function completed before dropping the endpoint. + let _ = tokio::time::timeout(BARRIER_TIMEOUT, barrier_client.wait()).await; + for group in endpoint.metrics().groups() { + dev.record_iroh_metrics(group); + } + res + } + .instrument(error_span!("ep-client")) + })?; + + let (server_res, client_res) = tokio::join!(server_task, client_task); + + // Map the results to include the device name, and emit a tracing event within the device context. + let [server_res, client_res] = [(&server_device, server_res), (&client_device, client_res)] + .map(|(dev, res)| { + let res = match res { + Err(err) => Err(anyerr!(err, "device {} panicked", dev.name())), + Ok(Err(err)) => Err(anyerr!(err, "device {} failed", dev.name())), + Ok(Ok(())) => Ok(()), + }; + let res_str = res.as_ref().map_err(|err| format!("{err:#}")).cloned(); + log_result_on_device(dev, res_str); + res + }); + server_res?; + client_res?; + Ok(()) + } +} + +fn log_result_on_device(dev: &Device, res: Result<(), E>) { + let _ = dev.run_sync(move || { + match res { + Ok(_) => event!( + target: "test::_events::pass", + tracing::Level::INFO, + msg = %"device passed" + ), + Err(error) => event!( + target: "test::_events::fail", + tracing::Level::ERROR, + %error, + msg = %"device failed" + ), + } + Ok(()) + }); +} + +/// Extension trait on [`Connection`] providing timeout-bounded wait helpers +/// on top of [`Connection::paths`] and [`PathList::stream`]. +pub(crate) trait PathConnectionExt { + /// Waits until the selected path satisfies `f`. Returns the matching + /// path's [`TransportAddr`]. + async fn wait_selected( + &self, + timeout: Duration, + f: impl FnMut(&Path<'_>) -> bool, + ) -> Result; + + /// Waits until the selected path is a direct (IP) path. + async fn wait_ip(&self, timeout: Duration) -> Result { + self.wait_selected(timeout, |p| p.is_ip()) + .await + .context("wait_ip") + } +} + +impl PathConnectionExt for Connection { + async fn wait_selected( + &self, + timeout: Duration, + mut f: impl FnMut(&Path<'_>) -> bool, + ) -> Result { + let mut stream = self.paths_stream(); + tokio::time::timeout(timeout, async { + while let Some(paths) = stream.next().await { + let selected = paths + .iter() + .find(|p| p.is_selected()) + .expect("no selected path"); + if f(&selected) { + return Ok(selected.remote_addr().clone()); + } + } + Err(anyerr!("path stream ended")) + }) + .await + .with_std_context(|_| format!("wait_selected timed out after {timeout:?}"))? + } +} + +/// Returns `true` if the currently selected path is a relay path. +pub(crate) fn is_relayed(conn: &iroh::endpoint::Connection) -> bool { + conn.paths() + .iter() + .find(|p| p.is_selected()) + .expect("no selected path") + .is_relay() +} + +/// Opens a bidi stream, sends 8 bytes of data, and waits to receive the same data back. +pub(crate) async fn ping_open(conn: &Connection, timeout: Duration) -> Result { + tokio::time::timeout(timeout, async { + let data: [u8; 8] = rand::random(); + debug!("open_bi"); + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + debug!("write_all"); + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + debug!("read_to_end"); + let r = recv.read_to_end(8).await.anyerr()?; + ensure_any!(r == data, "reply matches"); + debug!("done"); + Ok(()) + }) + .instrument(error_span!("ping_open")) + .await + .with_std_context(|_| format!("ping_open timed out after {timeout:?}"))? +} + +/// Accepts a bidi stream, reads 8 bytes of data, and sends the same data back. +pub(crate) async fn ping_accept(conn: &Connection, timeout: Duration) -> Result { + tokio::time::timeout(timeout, async { + debug!("accept_bi"); + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + debug!("read_to_end"); + let data = recv.read_to_end(8).await.anyerr()?; + debug!("write_all"); + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + debug!("done"); + Ok(()) + }) + .instrument(error_span!("ping_accept")) + .await + .with_std_context(|_| format!("ping_accept timed out after {timeout:?}"))? +} + +fn watch_selected_path(conn: &Connection) { + let mut events = conn.path_events(); + if let Some(path) = conn.paths().iter().find(|p| p.is_selected()) { + debug!("selected path: [{}] {}", path.id(), path.remote_addr()); + } + tokio::spawn( + async move { + while let Some(event) = events.next().await { + if let PathEvent::Selected { + id, remote_addr, .. + } = event + { + debug!("selected path: [{id}] {remote_addr}"); + } + } + } + .instrument(tracing::Span::current()), + ); +} + +fn endpoint_builder(device: &Device, relay_map: RelayMap) -> iroh::endpoint::Builder { + #[allow(unused_mut)] + let mut builder = Endpoint::builder(presets::Minimal) + .relay_mode(RelayMode::Custom(relay_map)) + .ca_tls_config(CaTlsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]); + + #[cfg(not(feature = "qlog"))] + let _ = device; + + #[cfg(feature = "qlog")] + { + if let Some(path) = device.filepath("qlog") { + let prefix = path.file_name().unwrap().to_str().unwrap(); + let directory = path.parent().unwrap(); + let transport_config = iroh::endpoint::QuicTransportConfig::builder() + .qlog_from_path(directory, prefix) + .build(); + builder = builder.transport_config(transport_config); + } + } + + builder +} + +fn addr_relay_only(addr: EndpointAddr) -> EndpointAddr { + EndpointAddr::from_parts(addr.id, addr.addrs.into_iter().filter(|a| a.is_relay())) +} + +pub(crate) mod relay { + use std::{ + net::{IpAddr, Ipv6Addr}, + sync::Arc, + }; + + use iroh_base::RelayUrl; + use iroh_relay::{ + RelayConfig, RelayMap, RelayQuicConfig, + server::{ + AllowAll, CertConfig, QuicConfig, RelayConfig as RelayServerConfig, Server, + ServerConfig, SpawnError, TlsConfig, testing::self_signed_tls_certs_and_config, + }, + }; + + /// Spawn a relay server bound on `[::]` that accepts both IPv4 and IPv6. + /// + /// The returned [`RelayMap`] uses `https://relay.test` as the relay URL. + /// Callers are responsible for ensuring that a DNS entry for `relay.test` + /// exists and points to the relay's IP addresses. + pub(crate) async fn run_relay_server() -> Result<(RelayMap, Server), SpawnError> { + let bind_ip: IpAddr = Ipv6Addr::UNSPECIFIED.into(); + + let (_certs, server_config) = self_signed_tls_certs_and_config(); + + let tls = TlsConfig::new((bind_ip, 443), CertConfig::Manual { server_config }); + let mut relay = RelayServerConfig::new((bind_ip, 80)); + relay.tls = Some(tls); + relay.key_cache_capacity = Some(1024); + relay.access = Arc::new(AllowAll); + + let mut config = ServerConfig::default(); + config.relay = Some(relay); + config.quic = Some(QuicConfig::new((bind_ip, 7842))); + + let server = Server::spawn(config).await?; + + let url: RelayUrl = "https://relay.test".parse().expect("valid relay url"); + let quic = server + .quic_addr() + .map(|addr| RelayQuicConfig::new(addr.port())); + let relay_map: RelayMap = RelayConfig::new(url, quic).into(); + + Ok((relay_map, server)) + } +} From 2e915199b85105cdbbb15684efc44a03e9934a0e Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 07:53:51 +0500 Subject: [PATCH 2/7] docs(net): retain exact Iroh BSD license provenance --- docs/reports/rds-latency-path-preference-20261006.md | 2 +- vendor/iroh/RDS-PATCH.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reports/rds-latency-path-preference-20261006.md b/docs/reports/rds-latency-path-preference-20261006.md index 01bd72f..b2d75b6 100644 --- a/docs/reports/rds-latency-path-preference-20261006.md +++ b/docs/reports/rds-latency-path-preference-20261006.md @@ -10,7 +10,7 @@ with opt-in bounded actor refresh. Backend defaults remain; single-path pins and transport eligibility dominate preference. Noq already periodically ranks validated paths by RTT. No identity/authentication/wire/OS-route change. -Exact published Iroh1.3 source and MIT/Apache-2.0 licenses are retained with the +Exact published Iroh1.3 source and BSD-3-Clause license are retained with the small default-None refresh hook; intervals are clamped250ms..60s and unchanged selection is not reapplied. The patch is a temporary substrate repair, not a QUIC/TLS fork or completion of owned-Noq migration. No new third-party package. diff --git a/vendor/iroh/RDS-PATCH.md b/vendor/iroh/RDS-PATCH.md index 8f31d2b..8f1402f 100644 --- a/vendor/iroh/RDS-PATCH.md +++ b/vendor/iroh/RDS-PATCH.md @@ -2,7 +2,7 @@ Upstream: published crates.io `iroh`1.3.0, checksum 885787b892b5e2507c701f132ecbd45d2bad4bbb75157a19427087c16dadb833. -Original MIT/Apache-2.0 notices and source retained. No QUIC/TLS/relay wire change. +Original BSD-3-Clause notices and source retained. No QUIC/TLS/relay wire change. The published selector only runs after connection/path topology events. Add one optional default-None selector refresh interval, bounded250ms..60s. The RDS From 77f1511ec749493c8c9d6f298606aec481590219 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 07:55:14 +0500 Subject: [PATCH 3/7] docs(net): preserve upstream dual license and Tailscale notice --- .../rds-latency-path-preference-20261006.md | 3 +- vendor/iroh/LICENSE-APACHE | 201 ++++++++++++++++++ vendor/iroh/LICENSE-MIT | 25 +++ vendor/iroh/RDS-PATCH.md | 3 +- 4 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 vendor/iroh/LICENSE-APACHE create mode 100644 vendor/iroh/LICENSE-MIT diff --git a/docs/reports/rds-latency-path-preference-20261006.md b/docs/reports/rds-latency-path-preference-20261006.md index b2d75b6..930fab4 100644 --- a/docs/reports/rds-latency-path-preference-20261006.md +++ b/docs/reports/rds-latency-path-preference-20261006.md @@ -10,7 +10,8 @@ with opt-in bounded actor refresh. Backend defaults remain; single-path pins and transport eligibility dominate preference. Noq already periodically ranks validated paths by RTT. No identity/authentication/wire/OS-route change. -Exact published Iroh1.3 source and BSD-3-Clause license are retained with the +Exact published Iroh1.3 source, MIT/Apache-2.0 licenses and the additional +BSD-3-Clause Tailscale-derived notice are retained with the small default-None refresh hook; intervals are clamped250ms..60s and unchanged selection is not reapplied. The patch is a temporary substrate repair, not a QUIC/TLS fork or completion of owned-Noq migration. No new third-party package. diff --git a/vendor/iroh/LICENSE-APACHE b/vendor/iroh/LICENSE-APACHE new file mode 100644 index 0000000..885ed28 --- /dev/null +++ b/vendor/iroh/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [2025] [N0, INC] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/iroh/LICENSE-MIT b/vendor/iroh/LICENSE-MIT new file mode 100644 index 0000000..c7edc9f --- /dev/null +++ b/vendor/iroh/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright 2025 N0, INC. + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/iroh/RDS-PATCH.md b/vendor/iroh/RDS-PATCH.md index 8f1402f..99f85a3 100644 --- a/vendor/iroh/RDS-PATCH.md +++ b/vendor/iroh/RDS-PATCH.md @@ -2,7 +2,8 @@ Upstream: published crates.io `iroh`1.3.0, checksum 885787b892b5e2507c701f132ecbd45d2bad4bbb75157a19427087c16dadb833. -Original BSD-3-Clause notices and source retained. No QUIC/TLS/relay wire change. +Original SPDX MIT OR Apache-2.0 license files from exact upstream tag v1.3.0 +and the additional BSD-3-Clause Tailscale-derived source notice are retained. No QUIC/TLS/relay wire change. The published selector only runs after connection/path topology events. Add one optional default-None selector refresh interval, bounded250ms..60s. The RDS From 948456afb447d27f93c51c73d3a6759f9f1424d5 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 08:15:46 +0500 Subject: [PATCH 4/7] docs(security): review exact Iroh public identity taint findings --- ...iroh-codeql-public-data-review-20261006.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/reports/rds-iroh-codeql-public-data-review-20261006.md diff --git a/docs/reports/rds-iroh-codeql-public-data-review-20261006.md b/docs/reports/rds-iroh-codeql-public-data-review-20261006.md new file mode 100644 index 0000000..feb4a85 --- /dev/null +++ b/docs/reports/rds-iroh-codeql-public-data-review-20261006.md @@ -0,0 +1,26 @@ +# Iroh public-data CodeQL review — 2026-10-06 + +PR110 introduces vendored Iroh1.3 source to the repository scanner. The eleven +new Rust cleartext alerts are exact source/sink cases below, reviewed without +changing query coverage, disabling CodeQL or altering runtime cryptography. + +`iroh-base1.3 SecretKey::public` derives the Ed25519 verifying key through +`SigningKey::verifying_key().to_bytes`; it never returns the signing seed. The +example/test logging sinks identified by alerts61–70 print that public EndpointId. +They do not print SecretKey::to_bytes or a credential. Those upstream demo/test +identifiers are intended connection identities, not deployment inventory. + +Alert71 ends at PkarrRelayClient::publish.put(url). The appended URL segment is +SignedPacket::public_key().to_z32(), read from the first32serialized public-key +bytes. `iroh-dns1.3 SignedPacket` layout is publickey32/signature64/timestamp8/DNS; +its relay payload omits the public-key prefix. Signing uses a secret internally, +but that secret is not serialized or placed in the URL. The RDS adapter uses +Minimal/no public pkarr lookup for explicitly configured private relays. +Public discovery is an explicit different preset, not a secret-key export. + +These exact alerts are false positives for secret-key disclosure because the +analyzer propagates secret-key taint through derived public identity objects. +This assessment does not deem arbitrary DNS/TXT content public, nor sanitize +other logging/transmission sinks. Only the11reviewed alert instances are in scope; +future actual credential flows remain subject to the unchanged security rules. +The exact published crypto/discovery implementation remains unchanged. From 3da9e89cae50e2a38323eafbb931479ed635dbe5 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 08:29:54 +0500 Subject: [PATCH 5/7] feat(observe): record desktop selected path transitions without RTT chatter --- crates/rds-desktop/src/control_observation.rs | 53 +++++++++++++++++++ .../rds-latency-path-preference-20261006.md | 13 +++++ 2 files changed, 66 insertions(+) diff --git a/crates/rds-desktop/src/control_observation.rs b/crates/rds-desktop/src/control_observation.rs index c28def2..90f750e 100644 --- a/crates/rds-desktop/src/control_observation.rs +++ b/crates/rds-desktop/src/control_observation.rs @@ -111,6 +111,18 @@ impl ControlObservation { // diagnostic budget. Truncation must remain explicit. snapshot.paths.sort_by_key(|path| !path.selected); snapshot.paths.truncate(MAX_PATHS); + if selection_changed(&snapshot.paths, &self.previous) { + tracing::info!(target:"rds_desktop::control_timing", control_instance=self.instance, + heartbeat_seq=seq, coverage=?snapshot.coverage, observed_paths=observed, + paths_truncated=observed>MAX_PATHS, + "desktop selected transmit paths changed"); + for path in snapshot.paths.iter().filter(|path| path.selected) { + tracing::info!(target:"rds_desktop::control_timing", control_instance=self.instance, + heartbeat_seq=seq, path_id=path.path_id, via_relay=path.via_relay, + path_rtt_ms=path.rtt.as_millis(), + "desktop selected transmit path"); + } + } if rtt >= SLOW_RESPONSE { tracing::warn!(target:"rds_desktop::control_timing", control_instance=self.instance, heartbeat_seq=seq, rtt_ms=rtt.as_millis(), @@ -140,6 +152,22 @@ impl ControlObservation { } } +fn selection_changed(current: &[PathStats], previous: &[PathStats]) -> bool { + fn same(path: &PathStats, candidates: &[PathStats]) -> bool { + candidates.iter().any(|other| { + other.selected && path.path_id == other.path_id && path.via_relay == other.via_relay + }) + } + current + .iter() + .filter(|path| path.selected) + .any(|path| !same(path, previous)) + || previous + .iter() + .filter(|path| path.selected) + .any(|path| !same(path, current)) +} + struct Deltas { sent: Option, lost: Option, @@ -177,6 +205,31 @@ mod tests { } } + #[test] + fn selection_observation_tracks_route_changes_without_rtt_chatter() { + let direct = path(5, 0); + let mut relay = path(10, 0); + relay.path_id = 2; + relay.via_relay = true; + let mut newer_direct = path(20, 1); + newer_direct.rtt = Duration::from_millis(150); + assert!(selection_changed(std::slice::from_ref(&direct), &[])); + assert!(!selection_changed( + &[newer_direct], + std::slice::from_ref(&direct) + )); + assert!(selection_changed( + std::slice::from_ref(&relay), + std::slice::from_ref(&direct) + )); + assert!(selection_changed(&[], std::slice::from_ref(&direct))); + assert!(!selection_changed(&[], &[])); + assert!(!selection_changed( + &[direct.clone(), relay.clone()], + &[relay, direct] + )); + } + #[test] fn unobserved_or_reset_counters_are_unknown_not_zero_loss() { let current = path(5, 2); diff --git a/docs/reports/rds-latency-path-preference-20261006.md b/docs/reports/rds-latency-path-preference-20261006.md index 930fab4..c4e383d 100644 --- a/docs/reports/rds-latency-path-preference-20261006.md +++ b/docs/reports/rds-latency-path-preference-20261006.md @@ -22,3 +22,16 @@ refresh without topology changes and termination on endpoint close. Existing real UDP-proxy packetization tests now select latency with a strict single-path pin, ensuring that preference cannot escape an impairment route. Qualification is pending; no deployed device configuration changed from this source yet. + +The existing bounded desktop observer records initial selected transmit paths +and actual selection changes at info level, with numeric connection-local path +IDs, kind and sampled RTT. Routine RTT/counter changes do not emit these records. +No address, ticket, input text or credential is logged. The observer retains its +weak transport reference, one worker and newest-only queued sample; no extra +metadata query is added to input/control readers. Tests distinguish initial, +changed, disappeared and reordered selections from ordinary RTT updates. + +Before that observation follow-up, strict desktop/owned-backend Clippy and all +170 network tests passed locally on both supported platforms. Optimized agent +builds also passed. Supply-chain and native packaging CI passed; the full CI +matrix and observation follow-up are still required at the final source head. From 1c4298a5ea88019315e77013bccc204b07d33d81 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 08:31:06 +0500 Subject: [PATCH 6/7] test(observe): use copied path statistics in transition assertions --- crates/rds-desktop/src/control_observation.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/rds-desktop/src/control_observation.rs b/crates/rds-desktop/src/control_observation.rs index 90f750e..bbc8e26 100644 --- a/crates/rds-desktop/src/control_observation.rs +++ b/crates/rds-desktop/src/control_observation.rs @@ -224,10 +224,7 @@ mod tests { )); assert!(selection_changed(&[], std::slice::from_ref(&direct))); assert!(!selection_changed(&[], &[])); - assert!(!selection_changed( - &[direct.clone(), relay.clone()], - &[relay, direct] - )); + assert!(!selection_changed(&[direct, relay], &[relay, direct])); } #[test] From b277c8be5a1eb6255563915b6bb1d42dc2fa8b7b Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 6 Oct 2026 08:36:37 +0500 Subject: [PATCH 7/7] fix(net): skip obsolete path refresh ticks after actor stalls --- crates/rds-net/tests/iroh_selector_refresh.rs | 40 +++++++++++++++++-- .../rds-latency-path-preference-20261006.md | 5 +++ vendor/iroh/RDS-PATCH.md | 3 +- .../src/socket/remote_map/remote_state.rs | 5 ++- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/rds-net/tests/iroh_selector_refresh.rs b/crates/rds-net/tests/iroh_selector_refresh.rs index 2d6757c..f8a2cec 100644 --- a/crates/rds-net/tests/iroh_selector_refresh.rs +++ b/crates/rds-net/tests/iroh_selector_refresh.rs @@ -1,7 +1,7 @@ //! Real actor refresh against isolated loopback endpoints; no deployed devices. use std::sync::{ Arc, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, }; use std::time::Duration; @@ -11,6 +11,8 @@ use iroh::endpoint::transports::{PathSelection, PathSelectionContext, PathSelect struct CountingSelector { calls: Arc, interval: Option, + stall: Arc, + stalled: Arc, } impl PathSelector for CountingSelector { @@ -19,6 +21,11 @@ impl PathSelector for CountingSelector { } fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection { self.calls.fetch_add(1, Ordering::Relaxed); + if self.stall.swap(false, Ordering::Relaxed) { + // Intentionally block only this isolated test actor to miss ticks. + std::thread::sleep(Duration::from_secs(1)); + self.stalled.store(true, Ordering::Release); + } let mut choice = PathSelection::none(); if let Some(path) = ctx.paths().next() { choice.set(&path); @@ -27,8 +34,10 @@ impl PathSelector for CountingSelector { } } -async fn exercise(interval: Option) { +async fn exercise(interval: Option, stall_probe: bool) { let calls = Arc::new(AtomicU64::new(0)); + let stall = Arc::new(AtomicBool::new(false)); + let stalled = Arc::new(AtomicBool::new(false)); let a = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal) .clear_ip_transports() .bind_addr("127.0.0.1:0".parse::().unwrap()) @@ -37,6 +46,8 @@ async fn exercise(interval: Option) { .path_selector(Arc::new(CountingSelector { calls: calls.clone(), interval, + stall: stall.clone(), + stalled: stalled.clone(), })) .alpns(vec![rds_core::ALPN.to_vec()]) .bind() @@ -80,6 +91,22 @@ async fn exercise(interval: Option) { "default selector unexpectedly became periodic" ); } + if stall_probe { + let before = calls.load(Ordering::Relaxed); + stall.store(true, Ordering::Relaxed); + tokio::time::timeout(Duration::from_secs(3), async { + while !stalled.load(Ordering::Acquire) { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("isolated actor did not execute the delayed selector"); + tokio::time::sleep(Duration::from_millis(100)).await; + assert!( + calls.load(Ordering::Relaxed) - before < 4, + "missed refresh ticks burst instead of sampling only current state" + ); + } // Ordinary bytes still flow while refresh runs, without new connections. let (mut send, mut recv) = client.open_bi().await.unwrap(); send.write_all(b"refresh").await.unwrap(); @@ -103,10 +130,15 @@ async fn exercise(interval: Option) { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn custom_refresh_runs_without_topology_change_and_stops_with_endpoint() { - exercise(Some(Duration::from_millis(250))).await; + exercise(Some(Duration::from_millis(250)), false).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn default_custom_selector_remains_topology_only() { - exercise(None).await; + exercise(None, false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn missed_refresh_ticks_do_not_burst_after_actor_stall() { + exercise(Some(Duration::from_millis(250)), true).await; } diff --git a/docs/reports/rds-latency-path-preference-20261006.md b/docs/reports/rds-latency-path-preference-20261006.md index c4e383d..34d56dc 100644 --- a/docs/reports/rds-latency-path-preference-20261006.md +++ b/docs/reports/rds-latency-path-preference-20261006.md @@ -31,6 +31,11 @@ weak transport reference, one worker and newest-only queued sample; no extra metadata query is added to input/control readers. Tests distinguish initial, changed, disappeared and reordered selections from ordinary RTT updates. +Refresh skips missed ticks after suspension/stall; obsolete reselection polls +must not burst and compete with current input/media work. An isolated real actor +test stalls one callback long enough to miss multiple periods, checks that they +are not replayed in a burst, then proves normal traffic and endpoint cleanup. + Before that observation follow-up, strict desktop/owned-backend Clippy and all 170 network tests passed locally on both supported platforms. Optimized agent builds also passed. Supply-chain and native packaging CI passed; the full CI diff --git a/vendor/iroh/RDS-PATCH.md b/vendor/iroh/RDS-PATCH.md index 99f85a3..31b3c5f 100644 --- a/vendor/iroh/RDS-PATCH.md +++ b/vendor/iroh/RDS-PATCH.md @@ -8,7 +8,8 @@ and the additional BSD-3-Clause Tailscale-derived source notice are retained. No The published selector only runs after connection/path topology events. Add one optional default-None selector refresh interval, bounded250ms..60s. The RDS latency selector requests1s; ordinary/pinned selectors preserve original events. -Unchanged selections are not reapplied during refresh. The actor already owns +Unchanged selections are not reapplied during refresh, and missed refresh ticks +are skipped rather than replayed in a burst after suspension/stall. The actor already owns only weak connection references; refresh does not add connection ownership. No default behavior change for other selectors. This temporary source patch keeps the working substrate while the owned Noq backend retains its existing diff --git a/vendor/iroh/src/socket/remote_map/remote_state.rs b/vendor/iroh/src/socket/remote_map/remote_state.rs index 11b0a63..adaac0a 100644 --- a/vendor/iroh/src/socket/remote_map/remote_state.rs +++ b/vendor/iroh/src/socket/remote_map/remote_state.rs @@ -253,7 +253,10 @@ impl RemoteStateActor { // Optional custom selection refresh; no interval runs for default selectors. let refresh_interval = self.state.path_selector.refresh_interval().map(|interval| interval.clamp(Duration::from_millis(250), Duration::from_secs(60))); - let refresh_paths = time::interval(refresh_interval.unwrap_or(Duration::from_secs(3600))); + let mut refresh_paths = time::interval(refresh_interval.unwrap_or(Duration::from_secs(3600))); + // Reselection uses current measurements; missed polls must not burst + // after suspension or a stalled actor and compete with fresh traffic. + refresh_paths.set_missed_tick_behavior(time::MissedTickBehavior::Skip); n0_future::pin!(refresh_paths); loop {