diff --git a/src/client.rs b/src/client.rs index 8ea70898f..a87c01fb3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -303,9 +303,6 @@ impl Client { } }; - if crate::get_ipv6_punch_enabled() { - crate::test_ipv6().await; - } let (stop_udp_tx, stop_udp_rx) = oneshot::channel::<()>(); let udp = diff --git a/src/common.rs b/src/common.rs index 3e23770c6..8d4938ade 100644 --- a/src/common.rs +++ b/src/common.rs @@ -96,7 +96,7 @@ lazy_static::lazy_static! { pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); pub static ref DEVICE_ID: Arc> = Default::default(); pub static ref DEVICE_NAME: Arc> = Default::default(); - static ref PUBLIC_IPV6_ADDR: Arc, Option)>> = Default::default(); + } lazy_static::lazy_static! { @@ -581,7 +581,6 @@ impl Drop for CheckTestNatType { } pub fn test_nat_type() { - test_ipv6_sync(); use std::sync::atomic::{AtomicBool, Ordering}; std::thread::spawn(move || { static IS_RUNNING: AtomicBool = AtomicBool::new(false); @@ -2099,108 +2098,6 @@ pub async fn test_nat_ipv4() -> ResultType<(SocketAddr, String)> { }; } -async fn test_bind_ipv6() -> ResultType { - let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 - let socket = UdpSocket::bind(local_addr).await?; - let addr = STUNS_V6[0] - .to_socket_addrs()? - .filter(|x| x.is_ipv6()) - .next() - .ok_or_else(|| { - anyhow!( - "Failed to resolve STUN ipv6 server address: {}", - STUNS_V6[0] - ) - })?; - socket.connect(addr).await?; - Ok(socket.local_addr()?) -} - -pub async fn test_ipv6() -> Option> { - if PUBLIC_IPV6_ADDR - .lock() - .unwrap() - .1 - .map(|x| x.elapsed().as_secs() < 60) - .unwrap_or(false) - { - return None; - } - PUBLIC_IPV6_ADDR.lock().unwrap().1 = Some(Instant::now()); - - match test_bind_ipv6().await { - Ok(mut addr) => { - if let std::net::IpAddr::V6(ip) = addr.ip() { - if !ip.is_loopback() - && !ip.is_unspecified() - && !ip.is_multicast() - && (ip.segments()[0] & 0xe000) == 0x2000 - { - addr.set_port(0); - PUBLIC_IPV6_ADDR.lock().unwrap().0 = Some(addr); - log::debug!("Found public IPv6 address locally: {}", addr); - } - } - } - Err(e) => { - log::warn!("Failed to bind IPv6 socket: {}", e); - } - } - // Interestingly, on my macOS, sometimes my ipv6 works, sometimes not (test with ping6 or https://test-ipv6.com/). - // I checked ifconfig, could not see any difference. Both secure ipv6 and temporary ipv6 are there. - // So we can not rely on the local ipv6 address queries with if_addrs. - // above test_bind_ipv6 is safer, because it can fail in this case. - /* - std::thread::spawn(|| { - if let Ok(ifaces) = if_addrs::get_if_addrs() { - for iface in ifaces { - if let if_addrs::IfAddr::V6(v6) = iface.addr { - let ip = v6.ip; - if !ip.is_loopback() - && !ip.is_unspecified() - && !ip.is_multicast() - && !ip.is_unique_local() - && !ip.is_unicast_link_local() - && (ip.segments()[0] & 0xe000) == 0x2000 - { - // only use the first one, on mac, the first one is the stable - // one, the last one is the temporary one. The middle ones are deperecated. - *PUBLIC_IPV6_ADDR.lock().unwrap() = - Some((SocketAddr::from((ip, 0)), Instant::now())); - log::debug!("Found public IPv6 address locally: {}", ip); - break; - } - } - } - } - }); - */ - - Some(tokio::spawn(async { - use hbb_common::futures::future::{select_ok, FutureExt}; - let tests = STUNS_V6 - .iter() - .map(|&stun| stun_ipv6_test(stun).boxed()) - .collect::>(); - - match select_ok(tests).await { - Ok(res) => { - let mut addr = res.0 .0; - addr.set_port(0); // Set port to 0 to avoid conflicts - PUBLIC_IPV6_ADDR.lock().unwrap().0 = Some(addr); - log::debug!( - "Found public IPv6 address via STUN server {}: {}", - res.0 .1, - addr - ); - } - Err(e) => { - log::error!("Failed to get public IPv6 address: {}", e); - } - }; - })) -} - pub async fn punch_udp( socket: Arc, listen: bool, @@ -2252,35 +2149,131 @@ pub async fn punch_udp( } } -fn test_ipv6_sync() { - #[tokio::main(flavor = "current_thread")] - async fn func() { - if let Some(job) = test_ipv6().await { - job.await.ok(); - } - } - std::thread::spawn(func); -} +/// STUN query on the actual hole-punching socket. +/// Tries up to 2 STUN servers sequentially within a global deadline. +/// Returns the mapped (external) addresses from each successful response. +/// All queries use the SAME socket . +async fn stun_probe_on_socketv6(socket: &UdpSocket) -> Vec { + use std::net::ToSocketAddrs; + use stunclient::StunClient; -pub async fn get_ipv6_socket() -> Option<(Arc, bytes::Bytes)> { - let Some(addr) = PUBLIC_IPV6_ADDR.lock().unwrap().0 else { - return None; - }; + let mut mapped_addrs: Vec = Vec::new(); + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(3); - match UdpSocket::bind(addr).await { - Err(err) => { - log::warn!("Failed to create UDP socket for IPv6: {err}"); + for stun_server in STUNS_V6.iter() { + if mapped_addrs.len() >= 1 { + //enough for decision, no need to wait for more + //If we needed to determine the NAT type, we would set this value to 2 so that the result could be compared later. + //Relay connections and hole‑punching are performed in parallel. + //so,We don’t need to do that. + break; } - Ok(socket) => { - if let Ok(local_addr_v6) = socket.local_addr() { - return Some(( - Arc::new(socket), - hbb_common::AddrMangle::encode(local_addr_v6).into(), - )); + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); + if remaining.is_zero() { + break; + } + let per_server = std::cmp::min(remaining, std::time::Duration::from_millis(1500)); + + let stun_addr = match tokio::net::lookup_host(stun_server) + .await + .ok() + .and_then(|mut it| it.find(|x| x.is_ipv6())) + { + Some(a) => a, + None => continue, + }; + + let client = StunClient::new(stun_addr); + match tokio::time::timeout(per_server, client.query_external_address_async(socket)).await { + Ok(Ok(addr)) => { + log::debug!("IPv6 STUN {} → mapped {}", stun_server, addr); + mapped_addrs.push(addr); + } + + Ok(Err(e)) => { + log::debug!("IPv6 STUN {} failed: {}", stun_server, e); + } + Err(_) => { + log::debug!("IPv6 STUN {} timed out", stun_server); } } } - None + mapped_addrs +} + +/// Returns `(socket, encoded_mapped_addr)` where: +/// - `socket` is bound to a local GUA address (used for sending/receiving), +/// - `encoded_mapped_addr` is the STUN-discovered external address (reported to hbbs). +/// Under NAT, the local and external addresses/ports may differ. +pub async fn get_ipv6_socket() -> Option<(Arc, bytes::Bytes)> { + fn is_usable_global_ipv6(addr: std::net::IpAddr) -> bool { + if let std::net::IpAddr::V6(ip) = addr { + !ip.is_loopback() + && !ip.is_unspecified() + && !ip.is_multicast() + && (ip.segments()[0] & 0xe000) == 0x2000 + } else { + false + } + } + fn route_probe_ipv6() -> Option { + // We’re not establishing a real connection, only performing a routing probe. + // so we use a hard‑coded IP address instead of a domain name to avoid unnecessary DNS resolution. This address is Cloudflare’s STUN server. + let addr: SocketAddr = "[2606:4700:49::]:3478".parse().ok()?; + let socket = std::net::UdpSocket::bind(SocketAddr::from(([0u16; 8], 0))).ok()?; + socket + .connect(addr) + .map_err(|_| log::debug!("route probe fail")) + .ok()?; + socket.local_addr().ok().map(|a| a.ip()) + } + + let local_addr_ip = route_probe_ipv6()?; + + let socket = match UdpSocket::bind(SocketAddr::from((local_addr_ip, 0))).await { + Ok(s) => s, + Err(err) => { + log::warn!("Failed to create UDP socket for IPv6: {err}"); + return None; + } + }; + // We only need to obtain an IPv6 GUA (global unicast address). If we have it, we skip the STUN test, + // which reduces unnecessary connection latency and lets the connection be established more quickly. + // In this case, the external port is the same as the local port. + if is_usable_global_ipv6(local_addr_ip) { + let socket_port = socket.local_addr().ok()?.port(); + let external_addr = SocketAddr::new(local_addr_ip, socket_port); + log::debug!( + "IPv6 socket: local={} (usable global IPv6, skipping STUN), external={}", + local_addr_ip, + external_addr + ); + return Some(( + Arc::new(socket), + hbb_common::AddrMangle::encode(external_addr).into(), + )); + } + + log::debug!("IPv6 socket: local={} (not gua)", local_addr_ip); + // Single STUN flow on the actual hole-punching socket. + // Same source port for STUN and hole punching — correct under any NAT type. + // Pass local_addr so STUN can short-circuit when no NAT is detected. + let mapped_addrs = stun_probe_on_socketv6(&socket).await; + + if mapped_addrs.is_empty() { + log::warn!("Failed to get IPv6 mapping from STUN"); + return None; + } + let external_addr = mapped_addrs[0]; + log::debug!( + "IPv6 socket: local={}, external={} (reported to hbbs)", + local_addr_ip, + external_addr + ); + Some(( + Arc::new(socket), + hbb_common::AddrMangle::encode(external_addr).into(), + )) } // The color is the same to `str2color()` in flutter. diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 3ef280a2a..01c66bddf 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -32,10 +32,15 @@ use crate::{ }; type Message = RendezvousMessage; +const DEDUP_WINDOW_MS: u128 = 100; lazy_static::lazy_static! { static ref SOLVING_PK_MISMATCH: Mutex = Default::default(); - static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); + static ref LAST_MSG: Mutex<(SocketAddr, SocketAddr, Instant)> = Mutex::new(( + SocketAddr::new([0; 4].into(), 0), + SocketAddr::new(std::net::Ipv6Addr::UNSPECIFIED.into(), 0), + Instant::now() + )); static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); } static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); @@ -416,7 +421,7 @@ impl RendezvousMediator { let last = *LAST_RELAY_MSG.lock().await; *LAST_RELAY_MSG.lock().await = (addr, Instant::now()); // skip duplicate relay request messages - if last.0 == addr && last.1.elapsed().as_millis() < 100 { + if last.0 == addr && last.1.elapsed().as_millis() < DEDUP_WINDOW_MS { return Ok(()); } @@ -484,13 +489,20 @@ impl RendezvousMediator { async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { let addr = AddrMangle::decode(&fla.socket_addr); + let peer_addr_v6 = hbb_common::AddrMangle::decode(&fla.socket_addr_v6); let last = *LAST_MSG.lock().await; - *LAST_MSG.lock().await = (addr, Instant::now()); - // skip duplicate punch hole messages - if last.0 == addr && last.1.elapsed().as_millis() < 100 { + *LAST_MSG.lock().await = (addr, peer_addr_v6, Instant::now()); + //IPv4: Continue using the existing deduplication mechanism. + //IPv6: Because IPv6 hole‑punching can be triggered in parallel from both the pure‑TCP branch and the UDP branch. + // refer to Client::_start_inner + //two attempts may occur simultaneously. Deduplicate solely by the IP address (i.e., keep only one entry per IPv6 address). This prevents duplicate IPv6 hole‑punching attempts. + if last.2.elapsed().as_millis() < DEDUP_WINDOW_MS + && last.0 == addr + && ((last.1.port() == 0 && peer_addr_v6.port() == 0) + || last.1.ip() == peer_addr_v6.ip()) + { return Ok(()); } - let peer_addr_v6 = hbb_common::AddrMangle::decode(&fla.socket_addr_v6); let relay_server = self.get_relay_server(fla.relay_server.clone()); let relay = use_ws() || Config::is_proxy(); let mut socket_addr_v6 = Default::default(); @@ -571,13 +583,18 @@ impl RendezvousMediator { async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> { let mut peer_addr = AddrMangle::decode(&ph.socket_addr); + let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); let last = *LAST_MSG.lock().await; - *LAST_MSG.lock().await = (peer_addr, Instant::now()); - // skip duplicate punch hole messages - if last.0 == peer_addr && last.1.elapsed().as_millis() < 100 { + *LAST_MSG.lock().await = (peer_addr, peer_addr_v6, Instant::now()); + // skip duplicate punch hole messages (match by IP pair, ignore short-lived port jitter) + if last.2.elapsed().as_millis() < DEDUP_WINDOW_MS + && last.0 == peer_addr + && ((last.1.port() == 0 && peer_addr_v6.port() == 0) + || last.1.ip() == peer_addr_v6.ip()) + { return Ok(()); } - let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); + let relay = use_ws() || Config::is_proxy() || ph.force_relay; let mut socket_addr_v6 = Default::default(); let control_permissions = ph.control_permissions.into_option(); @@ -590,10 +607,11 @@ impl RendezvousMediator { ) .await; } + let has_ipv6_punch = !socket_addr_v6.is_empty(); let relay_server = self.get_relay_server(ph.relay_server); // for ensure, websocket go relay directly if ph.nat_type.enum_value() == Ok(NatType::SYMMETRIC) - || Config::get_nat_type() == NatType::SYMMETRIC as i32 + || (Config::get_nat_type() == NatType::SYMMETRIC as i32 && !has_ipv6_punch) || relay || (config::is_disable_tcp_listen() && ph.udp_port <= 0) { @@ -849,8 +867,8 @@ async fn start_ipv6( server: ServerPtr, control_permissions: Option, ) -> bytes::Bytes { - crate::test_ipv6().await; - if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await { + // get v6 socket and mapped v6 addr. + if let Some((socket, mapped_addr_v6)) = crate::get_ipv6_socket().await { let server = server.clone(); tokio::spawn(async move { allow_err!( @@ -864,7 +882,7 @@ async fn start_ipv6( .await ); }); - return local_addr_v6; + return mapped_addr_v6; } Default::default() }