diff --git a/crates/wzp-client/src/dual_path.rs b/crates/wzp-client/src/dual_path.rs index 061f99b..aa06fc0 100644 --- a/crates/wzp-client/src/dual_path.rs +++ b/crates/wzp-client/src/dual_path.rs @@ -170,7 +170,14 @@ pub async fn race( } None => { let (sc, _cert_der) = wzp_transport::server_config(); - let bind: SocketAddr = "0.0.0.0:0".parse().unwrap(); + // [::]:0 = dual-stack socket — handles both IPv4 (via + // v4-mapped addrs) and IPv6 natively. Pre-Phase-5.5 + // used 0.0.0.0:0 (IPv4-only) which silently made + // all IPv6 host candidates non-functional: dials + // to [2a0d:...] failed or hung, accepts from IPv6 + // peers never arrived, and the JoinSet wasted time + // on dead candidates before the IPv4 one won. + let bind: SocketAddr = "[::]:0".parse().unwrap(); let fresh = wzp_transport::create_endpoint(bind, Some(sc))?; tracing::info!( local_addr = ?fresh.local_addr().ok(), @@ -206,7 +213,14 @@ pub async fn race( ep } None => { - let bind: SocketAddr = "0.0.0.0:0".parse().unwrap(); + // [::]:0 = dual-stack socket — handles both IPv4 (via + // v4-mapped addrs) and IPv6 natively. Pre-Phase-5.5 + // used 0.0.0.0:0 (IPv4-only) which silently made + // all IPv6 host candidates non-functional: dials + // to [2a0d:...] failed or hung, accepts from IPv6 + // peers never arrived, and the JoinSet wasted time + // on dead candidates before the IPv4 one won. + let bind: SocketAddr = "[::]:0".parse().unwrap(); let fresh = wzp_transport::create_endpoint(bind, None)?; tracing::info!( local_addr = ?fresh.local_addr().ok(), @@ -302,7 +316,7 @@ pub async fn race( let relay_ep = match shared_endpoint.clone() { Some(ep) => ep, None => { - let relay_bind: SocketAddr = "0.0.0.0:0".parse().unwrap(); + let relay_bind: SocketAddr = "[::]:0".parse().unwrap(); wzp_transport::create_endpoint(relay_bind, None)? } }; diff --git a/crates/wzp-client/src/reflect.rs b/crates/wzp-client/src/reflect.rs index c22a8c7..38ab046 100644 --- a/crates/wzp-client/src/reflect.rs +++ b/crates/wzp-client/src/reflect.rs @@ -102,7 +102,8 @@ pub async fn probe_reflect_addr( let endpoint = match existing_endpoint { Some(ep) => ep, None => { - let bind: SocketAddr = "0.0.0.0:0".parse().unwrap(); + // [::]:0 = dual-stack socket for both IPv4 + IPv6 + let bind: SocketAddr = "[::]:0".parse().unwrap(); create_endpoint(bind, None).map_err(|e| format!("endpoint: {e}"))? } }; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index e3064b6..1e67165 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -194,7 +194,7 @@ fn get_call_debug_logs() -> bool { async fn ping_relay(relay: String) -> Result { let addr: std::net::SocketAddr = relay.parse().map_err(|e| format!("bad address: {e}"))?; let _ = rustls::crypto::ring::default_provider().install_default(); - let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap(); + let bind: std::net::SocketAddr = "[::]:0".parse().unwrap(); let endpoint = wzp_transport::create_endpoint(bind, None).map_err(|e| format!("{e}"))?; let client_cfg = wzp_transport::client_config(); @@ -914,7 +914,13 @@ fn do_register_signal( // endpoints, which made MikroTik look symmetric and broke direct // P2P because the advertised reflex port was not the listening // port. - let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap(); + // [::]:0 = dual-stack socket — handles IPv4 (via ::ffff:x.x.x.x + // mapped addresses) AND native IPv6 on one socket. Critical for + // Phase 5.5 ICE host candidates: without dual-stack, the IPv6 + // candidates advertised in DirectCallOffer/Answer are dead on + // arrival — the Dialer can't send to them and the Acceptor can't + // receive from them. + let bind: std::net::SocketAddr = "[::]:0".parse().unwrap(); let (server_cfg, _cert_der) = wzp_transport::server_config(); let endpoint = wzp_transport::create_endpoint(bind, Some(server_cfg)) .map_err(|e| format!("{e}"))?;