From 7a5ad5252e6aa4933d28b714c9d224079ab3bcfd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 18 Sep 2026 17:14:49 +0800 Subject: [PATCH] socket_client: only the controller's NAT test takes the resolver's address `new_direct_udp_for` is back on `test_target`. The punch reply the controlled side sends with it goes to the address it picks, and that reply lost is a device online and unreachable - so it keeps the handshake that proves the address, as the registration does. The controller's NAT test, once per connection, is what could not afford the proof, a round trip right before the connection it then opens to the same host; it alone takes the resolver's preferred address, as `new_direct_udp_for_unverified`, and a wrong one there costs that connection its UDP punch and nothing more. That lookup is the first on the controller's connection path and the one not under CONNECT_TIMEOUT, so it gets a second: a resolver that hangs would otherwise hold the connection for its whole retry schedule before `connect_tcp` even starts, and what a slower lookup loses is, again, only the NAT test. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --- src/socket_client.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/socket_client.rs b/src/socket_client.rs index e18062521..d40a0fff4 100644 --- a/src/socket_client.rs +++ b/src/socket_client.rs @@ -204,8 +204,8 @@ pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { } /// The resolver's preferred address for `target`: first in getaddrinfo's RFC 6724 order, so NAT64's -/// synthesized IPv6 leads on a v6-only host. Preferred, not proven reachable: the per-connection -/// UDP paths take it as is, and spare themselves the round trip that proving it costs. +/// synthesized IPv6 leads on a v6-only host. Preferred, not proven reachable: what `test_target` +/// falls back to, and what the controller's NAT test takes as is to spare itself the proof. async fn resolve_target(target: &str) -> ResultType { tokio::net::lookup_host(target) .await? @@ -215,7 +215,7 @@ async fn resolve_target(target: &str) -> ResultType { /// The address of `target` a TCP connection reaches, the resolver's candidates tried in order until /// one answers: an AAAA the server refuses is passed over for its A record, which the order alone -/// would not do. For the rendezvous registration, which lives on what this picks. +/// would not do. For the rendezvous registration and the punch reply, which live on what it picks. async fn test_target(target: &str) -> ResultType { if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { if let Ok(addr) = s.peer_addr() { @@ -227,7 +227,20 @@ async fn test_target(target: &str) -> ResultType { #[inline] pub async fn new_direct_udp_for(target: &str) -> ResultType<(Arc, SocketAddr)> { - let peer_addr = resolve_target(target).await?; + let peer_addr = test_target(target).await?; + let local_addr = Config::get_any_listen_addr(peer_addr.is_ipv4()); + let socket = UdpSocket::bind(local_addr).await?; + Ok((Arc::new(socket), peer_addr)) +} + +/// `new_direct_udp_for` on the resolver's preferred address, with no handshake to prove it and a +/// second at most for the lookup: for the controller's NAT test, once per connection, where the +/// proof cost a round trip right before the connection it then opens to the same host. +#[inline] +pub async fn new_direct_udp_for_unverified( + target: &str, +) -> ResultType<(Arc, SocketAddr)> { + let peer_addr = super::timeout(1000, resolve_target(target)).await??; let local_addr = Config::get_any_listen_addr(peer_addr.is_ipv4()); let socket = UdpSocket::bind(local_addr).await?; Ok((Arc::new(socket), peer_addr))