From b68643e5a693d4214bf439349ccb4c5ae453daeb Mon Sep 17 00:00:00 2001 From: William <65813441+William-Herly@users.noreply.github.com> Date: Fri, 11 Sep 2026 06:03:51 +0800 Subject: [PATCH 1/3] fix(iroh): handle IPv6 literal relay URLs in QAD probes --- iroh/src/net_report.rs | 110 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 10 deletions(-) diff --git a/iroh/src/net_report.rs b/iroh/src/net_report.rs index 63cd500ab70..99e8b53148e 100644 --- a/iroh/src/net_report.rs +++ b/iroh/src/net_report.rs @@ -836,12 +836,9 @@ async fn run_probe_v4( .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 host = relay_tls_server_name(&relay).ok_or_else(|| e!(QadProbeError::MissingHost))?; let conn = quic_client - .create_conn(relay_addr.into(), host) + .create_conn(relay_addr.into(), &host) .await .map_err(|source| e!(QadProbeError::Quic { source }))?; @@ -905,12 +902,9 @@ async fn run_probe_v6( .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 host = relay_tls_server_name(&relay).ok_or_else(|| e!(QadProbeError::MissingHost))?; let conn = quic_client - .create_conn(relay_addr.into(), host) + .create_conn(relay_addr.into(), &host) .await .map_err(|source| e!(QadProbeError::Quic { source }))?; @@ -960,6 +954,16 @@ async fn run_probe_v6( )) } +#[cfg(not(wasm_browser))] +fn relay_tls_server_name(relay: &RelayConfig) -> Option { + // host_str() includes brackets around IPv6 literals. + match relay.url.host()? { + url::Host::Domain(name) => Some(name.to_owned()), + url::Host::Ipv4(ip) => Some(ip.to_string()), + url::Host::Ipv6(ip) => Some(ip.to_string()), + } +} + #[cfg(test)] mod test_utils { //! Creates a relay server against which to perform tests @@ -1009,6 +1013,92 @@ mod tests { use super::*; use crate::net_report::probes::Probe; + #[test] + fn relay_tls_server_names() { + for (url, expected) in [ + ("https://relay.example.test", "relay.example.test"), + ("https://192.0.2.10", "192.0.2.10"), + ("https://[2001:db8::10]", "2001:db8::10"), + ] { + let relay = RelayConfig::from(url.parse::().unwrap()); + assert_eq!(relay_tls_server_name(&relay).as_deref(), Some(expected)); + } + } + + #[tokio::test] + async fn qad_probe_accepts_ipv6_literal_relay_url() { + use std::net::Ipv6Addr; + + use iroh_relay::{RelayQuicConfig, server}; + + let (certs, server_crypto) = server::testing::self_signed_tls_certs_and_config(); + let mut roots = rustls::RootCertStore::empty(); + for cert in certs { + roots.add(cert).unwrap(); + } + let client_crypto = rustls::ClientConfig::builder_with_provider(default_provider()) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(roots) + .with_no_client_auth(); + let mut config = server::testing::server_config(); + config.relay = None; + let quic = config.quic.as_mut().unwrap(); + quic.bind_addr = (Ipv6Addr::LOCALHOST, 0).into(); + quic.server_config = Some(server_crypto); + let server = server::Server::spawn(config).await.unwrap(); + let server_addr = server.quic_addr().unwrap(); + let ep = noq::Endpoint::client((Ipv6Addr::LOCALHOST, 0).into()).unwrap(); + let client_addr = ep.local_addr().unwrap(); + let client = QuicClient::new(ep.clone(), client_crypto); + let shutdown = CancellationToken::new(); + let https_port = if server_addr.port() == 443 { 8443 } else { 443 }; + let relay = RelayConfig::new( + format!("https://[::1]:{https_port}").parse().unwrap(), + Some(RelayQuicConfig::new(server_addr.port())), + ); + + let result = time::timeout(Duration::from_secs(5), async { + // Verify the same server accepts the bare IPv6 address. + let control = client + .create_conn(server_addr, "::1") + .await + .map_err(|err| format!("control connection failed: {err:#}"))?; + let observed = control + .observed_external_addr() + .next() + .await + .ok_or_else(|| "control connection returned no address".to_owned())?; + control.close(0u32.into(), b"control complete"); + + let (report, conn) = run_probe_v6( + Arc::new(relay), + client, + DnsResolver::new(), + shutdown.clone(), + ) + .await + .map_err(|err| format!("IPv6 literal QAD probe failed: {err:#}"))?; + Ok::<_, String>((observed, report, conn)) + }) + .await; + + shutdown.cancel(); + ep.close(0u32.into(), b"test complete"); + let (_, server_result) = time::timeout(Duration::from_secs(5), async { + tokio::join!(ep.wait_idle(), server.shutdown()) + }) + .await + .expect("QAD test cleanup timed out"); + server_result.unwrap(); + + let (observed, report, _conn) = result + .expect("IPv6 QAD probe timed out") + .unwrap_or_else(|err| panic!("{err}")); + assert_eq!(observed, client_addr); + assert_eq!(report.addr, client_addr); + } + #[tokio::test(flavor = "multi_thread")] #[traced_test] async fn test_basic() -> Result<()> { From e490b1c5ecb7343baa444f852a675317794224c5 Mon Sep 17 00:00:00 2001 From: William <65813441+William-Herly@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:47:03 +0800 Subject: [PATCH 2/3] fix(iroh): make IPv6 QAD regression test portable --- iroh/src/net_report.rs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/iroh/src/net_report.rs b/iroh/src/net_report.rs index 99e8b53148e..2646f14819a 100644 --- a/iroh/src/net_report.rs +++ b/iroh/src/net_report.rs @@ -955,12 +955,12 @@ async fn run_probe_v6( } #[cfg(not(wasm_browser))] -fn relay_tls_server_name(relay: &RelayConfig) -> Option { +fn relay_tls_server_name(relay: &RelayConfig) -> Option> { // host_str() includes brackets around IPv6 literals. match relay.url.host()? { - url::Host::Domain(name) => Some(name.to_owned()), - url::Host::Ipv4(ip) => Some(ip.to_string()), - url::Host::Ipv6(ip) => Some(ip.to_string()), + url::Host::Domain(name) => Some(name.into()), + url::Host::Ipv4(ip) => Some(ip.to_string().into()), + url::Host::Ipv6(ip) => Some(ip.to_string().into()), } } @@ -1031,6 +1031,15 @@ mod tests { use iroh_relay::{RelayQuicConfig, server}; + match std::net::UdpSocket::bind((Ipv6Addr::LOCALHOST, 0)) { + Ok(_) => {} + Err(err) if err.kind() == std::io::ErrorKind::AddrNotAvailable => { + eprintln!("skipping IPv6 QAD probe: IPv6 loopback is unavailable ({err})"); + return; + } + Err(err) => panic!("failed to bind IPv6 loopback: {err}"), + } + let (certs, server_crypto) = server::testing::self_signed_tls_certs_and_config(); let mut roots = rustls::RootCertStore::empty(); for cert in certs { @@ -1044,12 +1053,14 @@ mod tests { let mut config = server::testing::server_config(); config.relay = None; let quic = config.quic.as_mut().unwrap(); - quic.bind_addr = (Ipv6Addr::LOCALHOST, 0).into(); + // Dual-stack sockets cannot bind to ::1 on Windows. + quic.bind_addr = (Ipv6Addr::UNSPECIFIED, 0).into(); quic.server_config = Some(server_crypto); let server = server::Server::spawn(config).await.unwrap(); - let server_addr = server.quic_addr().unwrap(); - let ep = noq::Endpoint::client((Ipv6Addr::LOCALHOST, 0).into()).unwrap(); - let client_addr = ep.local_addr().unwrap(); + let server_addr = + SocketAddr::from((Ipv6Addr::LOCALHOST, server.quic_addr().unwrap().port())); + let ep = noq::Endpoint::client((Ipv6Addr::UNSPECIFIED, 0).into()).unwrap(); + let client_addr = SocketAddr::from((Ipv6Addr::LOCALHOST, ep.local_addr().unwrap().port())); let client = QuicClient::new(ep.clone(), client_crypto); let shutdown = CancellationToken::new(); let https_port = if server_addr.port() == 443 { 8443 } else { 443 }; From eebfde2090b2ea119c159bbf2ece885e8520a5ac Mon Sep 17 00:00:00 2001 From: William <65813441+William-Herly@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:47:03 +0800 Subject: [PATCH 3/3] build(deps): replace yanked yoke-derive version --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 902cde5f153..238710b4b0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 3.0.6", + "syn 2.0.119", ] [[package]] @@ -6141,14 +6141,14 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", - "synstructure 0.14.0", + "syn 2.0.119", + "synstructure 0.13.2", ] [[package]]