From 2b9ee3575618662cc73643536723552b84520e8d Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Wed, 17 Dec 2025 17:37:53 +0100 Subject: [PATCH 1/2] feat: implement rtt hints for nat traversal --- Cargo.lock | 28 ++++----- Cargo.toml | 18 +++--- iroh-relay/Cargo.toml | 4 +- iroh/Cargo.toml | 8 +-- iroh/bench/Cargo.toml | 2 +- iroh/src/magicsock.rs | 2 + iroh/src/magicsock/remote_map.rs | 12 ++++ iroh/src/magicsock/remote_map/remote_state.rs | 62 ++++++++++++++----- iroh/src/net_report/report.rs | 2 +- 9 files changed, 91 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28ae2ea16a4..673e80f77ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,7 +1092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1848,7 +1848,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2276,7 +2276,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main#4dcb3a7f05d1b3eb5441334ed301fed316fd79aa" +source = "git+https://github.com/n0-computer/quinn?branch=feat-rtt-hints#46c9205c39c03d359895d8d2eb2fd8daada674ae" dependencies = [ "bytes", "cfg_aliases", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main#4dcb3a7f05d1b3eb5441334ed301fed316fd79aa" +source = "git+https://github.com/n0-computer/quinn?branch=feat-rtt-hints#46c9205c39c03d359895d8d2eb2fd8daada674ae" dependencies = [ "bytes", "derive_more 2.1.0", @@ -2322,13 +2322,13 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.6.0" -source = "git+https://github.com/n0-computer/quinn?branch=main#4dcb3a7f05d1b3eb5441334ed301fed316fd79aa" +source = "git+https://github.com/n0-computer/quinn?branch=feat-rtt-hints#46c9205c39c03d359895d8d2eb2fd8daada674ae" dependencies = [ "cfg_aliases", "libc", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2855,7 +2855,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3315,7 +3315,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3352,9 +3352,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3623,7 +3623,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3745,7 +3745,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4320,7 +4320,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5096,7 +5096,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6fa3582abbb..283437e73be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,17 +43,17 @@ unused-async = "warn" [patch.crates-io] -iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "main" } -iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "main" } -iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "main" } +iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } +iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } +iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "main" } -# iroh-quinn = { path = "../quinn/quinn" } -# iroh-quinn-proto = { path = "../quinn/quinn-proto" } -# iroh-quinn-udp = { path = "../quinn/quinn-udp" } +# iroh-quinn = { path = "../iroh-quinn/quinn" } +# iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } +# iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } # [patch."https://github.com/n0-computer/quinn"] -# iroh-quinn = { path = "../quinn/quinn" } -# iroh-quinn-proto = { path = "../quinn/quinn-proto" } -# iroh-quinn-udp = { path = "../quinn/quinn-udp" } +# iroh-quinn = { path = "../iroh-quinn/quinn" } +# iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } +# iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 42388bc92f9..1ca06f3f7d6 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -42,8 +42,8 @@ postcard = { version = "1", default-features = false, features = [ "use-std", "experimental-derive", ] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 1c88b795e5f..2f61820fecf 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -35,9 +35,9 @@ n0-watcher = "0.6" netwatch = { version = "0.12" } pin-project = "1" pkarr = { version = "5", default-features = false, features = ["relays"] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main" } -quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "main" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } +quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", @@ -82,7 +82,7 @@ hickory-resolver = "0.25.1" igd-next = { version = "0.16", features = ["aio_tokio"] } netdev = { version = "0.39.0" } portmapper = { version = "0.12", default-features = false } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main", default-features = false, features = ["runtime-tokio", "rustls-ring"] } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints", default-features = false, features = ["runtime-tokio", "rustls-ring"] } tokio = { version = "1", features = [ "io-util", "macros", diff --git a/iroh/bench/Cargo.toml b/iroh/bench/Cargo.toml index d6af2d0dea9..17996047618 100644 --- a/iroh/bench/Cargo.toml +++ b/iroh/bench/Cargo.toml @@ -12,7 +12,7 @@ iroh = { path = "..", default-features = false } iroh-metrics = { version = "0.37", optional = true } n0-future = "0.3.0" n0-error = "0.1.0" -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "feat-rtt-hints" } rand = "0.9.2" rcgen = "0.14" rustls = { version = "0.23.33", default-features = false, features = ["ring"] } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index e544f446627..cfcc32b3327 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1415,6 +1415,8 @@ impl Actor { // Notify all transports self.network_change_sender.on_network_change(r); + // Notify connections + self.msock.remote_map.on_network_change(r); } #[cfg(not(wasm_browser))] diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index b08ea47cc80..402b1aae853 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -21,6 +21,7 @@ use self::remote_state::{RemoteStateActor, RemoteStateHandle}; use super::{ DirectAddr, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, + net_report::Report, }; use crate::discovery::ConcurrentDiscovery; @@ -98,6 +99,17 @@ impl RemoteMap { handles.retain(|_eid, handle| !handle.sender.is_closed()) } + pub(super) fn on_network_change(&self, report: &Report) { + let handles = self.actor_handles.lock().expect("poisoned"); + for (_, handle) in &*handles { + if let Some(sender) = handle.sender.get() { + sender + .try_send(RemoteStateMessage::NetworkChange(report.clone())) + .ok(); + } + } + } + /// Returns the sender for the [`RemoteStateActor`]. /// /// If needed a new actor is started on demand. diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index b8d8548cac4..20d740b043f 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -40,6 +40,7 @@ use crate::{ remote_map::Private, transports::{self, OwnedTransmit, TransportsSender}, }, + net_report::Report, util::MaybeFuture, }; @@ -190,6 +191,9 @@ pub(super) struct RemoteStateActor { // /// Stream of discovery results, or always pending if discovery is not running. discovery_stream: DiscoveryStream, + + /// Last Net Report + last_report: Option, } impl RemoteStateActor { @@ -220,6 +224,7 @@ impl RemoteStateActor { scheduled_open_path: None, pending_open_paths: VecDeque::new(), discovery_stream: Either::Left(n0_future::stream::pending()), + last_report: None, } } @@ -368,6 +373,9 @@ impl RemoteStateActor { }; tx.send(info).ok(); } + RemoteStateMessage::NetworkChange(report) => { + self.last_report.replace(report); + } } } @@ -707,7 +715,26 @@ impl RemoteStateActor { .map(|daddr| daddr.addr) .collect::>(); - match conn.initiate_nat_traversal_round() { + let all_rtts = self.calculate_path_rtts(); + let relay_mapped_addrs = self.relay_mapped_addrs.clone(); + let relay_latencies = self.last_report.as_ref().map(|r| r.relay_latency.clone()); + let rtt_hints = |addr: &SocketAddr| { + let addr = relay_mapped_addrs.to_transport_addr(*addr)?; + match addr { + transports::Addr::Relay(url, _id) => { + // Relay case, grab the latency from net_report + relay_latencies.as_ref().and_then(|l| l.get(&url)) + } + addr @ transports::Addr::Ip(_) => { + // IP case, grab the worst latency we currently have + all_rtts + .get(&addr) + .and_then(|rtts| rtts.iter().max().copied()) + } + } + }; + + match conn.initiate_nat_traversal_round(rtt_hints) { Ok(remote_candidates) => { let remote_candidates = remote_candidates .iter() @@ -776,7 +803,8 @@ impl RemoteStateActor { if conn.side().is_server() { continue; } - let fut = conn.open_path_ensure(quic_addr, path_status); + let rtt_hint = None; // TODO + let fut = conn.open_path_ensure(quic_addr, path_status, rtt_hint); match fut.path_id() { Some(path_id) => { trace!(?conn_id, ?path_id, "opening new path"); @@ -905,6 +933,20 @@ impl RemoteStateActor { } } + fn calculate_path_rtts(&self) -> FxHashMap> { + let mut rtts: FxHashMap> = FxHashMap::default(); + for conn_state in self.connections.values() { + let Some(conn) = conn_state.handle.upgrade() else { + continue; + }; + for (path_id, addr) in conn_state.open_paths.iter() { + if let Some(stats) = conn.path_stats(*path_id) { + rtts.entry(addr.clone()).or_default().push(stats.rtt); + } + } + } + rtts + } /// Selects the path with the lowest RTT, prefers direct paths. /// /// If there are direct paths, this selects the direct path with the lowest RTT. If @@ -914,22 +956,9 @@ impl RemoteStateActor { /// direct paths are closed for all connections. #[instrument(skip_all)] fn select_path(&mut self) { + let all_path_rtts = self.calculate_path_rtts(); // Find the lowest RTT across all connections for each open path. The long way, so // we get to log *all* RTTs. - let mut all_path_rtts: FxHashMap> = FxHashMap::default(); - for conn_state in self.connections.values() { - let Some(conn) = conn_state.handle.upgrade() else { - continue; - }; - for (path_id, addr) in conn_state.open_paths.iter() { - if let Some(stats) = conn.path_stats(*path_id) { - all_path_rtts - .entry(addr.clone()) - .or_default() - .push(stats.rtt); - } - } - } trace!(?all_path_rtts, "dumping all path RTTs"); let path_rtts: FxHashMap = all_path_rtts .into_iter() @@ -1175,6 +1204,7 @@ pub(crate) enum RemoteStateMessage { /// /// This currently only includes a list of all known transport addresses for the remote. RemoteInfo(oneshot::Sender), + NetworkChange(Report), } /// A handle to a [`RemoteStateActor`]. diff --git a/iroh/src/net_report/report.rs b/iroh/src/net_report/report.rs index 004a140dc66..99c5f40ce0e 100644 --- a/iroh/src/net_report/report.rs +++ b/iroh/src/net_report/report.rs @@ -197,7 +197,7 @@ impl RelayLatencies { } /// Returns the lowest latency across records. - pub(super) fn get(&self, url: &RelayUrl) -> Option { + pub(crate) fn get(&self, url: &RelayUrl) -> Option { let mut list = Vec::with_capacity(3); if let Some(val) = self.https.get(url) { list.push(*val); From b926b912b68cc97447e946d2f1ce68ed585ec7b0 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Wed, 17 Dec 2025 17:44:23 +0100 Subject: [PATCH 2/2] impl todo --- iroh/src/magicsock/remote_map.rs | 2 +- iroh/src/magicsock/remote_map/remote_state.rs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index 402b1aae853..040561a35b0 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -101,7 +101,7 @@ impl RemoteMap { pub(super) fn on_network_change(&self, report: &Report) { let handles = self.actor_handles.lock().expect("poisoned"); - for (_, handle) in &*handles { + for handle in handles.values() { if let Some(sender) = handle.sender.get() { sender .try_send(RemoteStateMessage::NetworkChange(report.clone())) diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index 20d740b043f..121b98d6206 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -793,6 +793,21 @@ impl RemoteStateActor { .private_socket_addr(), }; + let rtt_hint = match &open_addr { + transports::Addr::Relay(url, _) => { + // Relay case, grab the latency from net_report + self.last_report + .as_ref() + .and_then(|r| r.relay_latency.get(url)) + } + addr @ transports::Addr::Ip(_) => { + let all_rtts = self.calculate_path_rtts(); + all_rtts + .get(addr) + .and_then(|rtts| rtts.iter().max().copied()) + } + }; + for (conn_id, conn_state) in self.connections.iter_mut() { if conn_state.path_ids.contains_key(open_addr) { continue; @@ -803,7 +818,7 @@ impl RemoteStateActor { if conn.side().is_server() { continue; } - let rtt_hint = None; // TODO + let fut = conn.open_path_ensure(quic_addr, path_status, rtt_hint); match fut.path_id() { Some(path_id) => {