ipv6 punch: ask the kernel for the route without resolving a name first

`test_ipv6` is awaited on the connection path by both sides of a punch, and
before it looked for a public IPv6 address in the background it resolved
the STUN hosts' names inline - racing the four, so that one resolver that
hangs would not decide. It could still: `select_ok` returns the first
success or the last failure, so with no resolver answering the probe waits
for the slowest lookup to give up, as long as the system resolver takes,
once a minute, on the first connection of that minute.

The name was never needed. `connect` on a UDP socket sends nothing; it has
the kernel pick a route and a source address for the destination, and any
global address serves, so the probe now names one - the one libwebrtc's
QueryDefaultLocalAddress asks for - and touches no network at all: a bind,
a connect, a local_addr. A machine without an IPv6 route learns so from the
connect's error, at once, as before.

Two smaller things beside it. The minute's gate read the timestamp under
one lock and set it under another, so two connections arriving together
both found it over and both probed; it is one critical section now. And the
background STUN probe, bounded so far by the STUN client's own ten seconds
and the resolver's, has a deadline of its own, five seconds: a probe that
outlived the minute could write an earlier network's address over a later
probe's.

Test: the route probe completes within a second, an address found or not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
This commit is contained in:
rustdesk
2026-09-18 15:13:29 +08:00
parent 349da72d8c
commit 30c2f07061

View File

@@ -2528,45 +2528,42 @@ async fn stun_ipv6_test(stun_server: String) -> ResultType<(SocketAddr, String)>
})
}
/// A global address to ask the kernel for a route to; libwebrtc's QueryDefaultLocalAddress asks
/// for the same one. Nothing is ever sent to it.
const IPV6_ROUTE_PROBE: std::net::Ipv6Addr =
std::net::Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888);
/// The public IPv6 address the STUN servers report is looked for in the background, and for no
/// longer than this: a probe that outlived the minute could write an earlier network's address
/// over a later probe's.
const STUN_IPV6_TIMEOUT_MS: u64 = 5_000;
async fn test_bind_ipv6() -> ResultType<SocketAddr> {
use hbb_common::futures::future::FutureExt;
let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0
let socket = UdpSocket::bind(local_addr).await?;
// Nothing is sent - `connect` only makes the kernel pick a route and a source address - so any
// resolvable target answers equally and the whole cost is DNS. Race the lookups rather than
// walk them: this is awaited inline on the connection path, not every STUN host publishes a
// AAAA, and one resolver that hangs must not decide whether this host has v6.
let lookups = hbb_common::webrtc::WebRTCStream::default_stun_servers()
.into_iter()
.map(|stun| {
(async move {
let addr = tokio::net::lookup_host(&stun)
.await?
.find(|x| x.is_ipv6())
.ok_or_else(|| {
anyhow!("Failed to resolve STUN ipv6 server address: {}", stun)
})?;
Ok::<SocketAddr, hbb_common::anyhow::Error>(addr)
})
.boxed()
})
.collect::<Vec<_>>();
let (addr, _) = hbb_common::futures::future::select_ok(lookups).await?;
socket.connect(addr).await?;
// Nothing is sent - `connect` only makes the kernel pick a route and a source address - so
// the target can be any global address, and given as a number it is: this is awaited on the
// connection path, and resolving a STUN host's name first was the one thing on it that
// could wait on the network - for as long as the resolver takes, when there is none.
socket
.connect(SocketAddr::from((IPV6_ROUTE_PROBE, 53)))
.await?;
Ok(socket.local_addr()?)
}
pub async fn test_ipv6() -> Option<tokio::task::JoinHandle<()>> {
if PUBLIC_IPV6_ADDR
.lock()
.unwrap()
.1
.map(|x| x.elapsed().as_secs() < 60)
.unwrap_or(false)
{
return None;
// One look and one claim of the minute, under one lock: two connections arriving
// together would otherwise both find it over and both probe.
let mut cached = PUBLIC_IPV6_ADDR.lock().unwrap();
if cached
.1
.map(|x| x.elapsed().as_secs() < 60)
.unwrap_or(false)
{
return None;
}
cached.1 = Some(Instant::now());
}
PUBLIC_IPV6_ADDR.lock().unwrap().1 = Some(Instant::now());
match test_bind_ipv6().await {
Ok(mut addr) => {
@@ -2623,8 +2620,8 @@ pub async fn test_ipv6() -> Option<tokio::task::JoinHandle<()>> {
.map(|stun| stun_ipv6_test(stun).boxed())
.collect::<Vec<_>>();
match select_ok(tests).await {
Ok(res) => {
match hbb_common::timeout(STUN_IPV6_TIMEOUT_MS, select_ok(tests)).await {
Ok(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);
@@ -2634,9 +2631,12 @@ pub async fn test_ipv6() -> Option<tokio::task::JoinHandle<()>> {
addr
);
}
Err(e) => {
Ok(Err(e)) => {
log::error!("Failed to get public IPv6 address: {}", e);
}
Err(_) => {
log::warn!("No STUN server answered for IPv6 within {STUN_IPV6_TIMEOUT_MS}ms");
}
};
}))
}
@@ -3416,4 +3416,11 @@ mod tests {
// non-WebRTC handshakes.
assert_eq!(decode_id_pk(&signed, &pk).unwrap(), (id, their_pk));
}
// The route probe is awaited on the connection path, so whatever it finds - an address, or
// no IPv6 route on this machine - it finds without waiting on the network.
#[tokio::test]
async fn test_ipv6_route_probe_does_not_wait_on_the_network() {
assert!(hbb_common::timeout(1_000, test_bind_ipv6()).await.is_ok());
}
}