mirror of https://github.com/ospab/ostp.git
fix(client): stop mobile connects from stalling for minutes on dead IPv6
Connecting over a mobile network took ~90s, and under worse conditions did
not complete at all. Three compounding causes, all in the address loop of
perform_handshake_with_id, which walks candidates strictly in order and burns
each one's full retry budget before touching the next:
- IPv6 was tried FIRST. Carriers routinely hand out IPv6 with no working
route and blackhole it rather than rejecting, so each AAAA record cost the
entire 4x1.2s budget with nothing to show; with several of them the
working IPv4 address was not reached for tens of seconds. The identical
ordering bug was already fixed on the server's outbound path and in the
UoT connect - the client's handshake was simply missed.
- The NAT64 prefix discovery lookup had no timeout. It only ever runs on
networks that are already misbehaving, exactly where a resolver can hang
for tens of seconds. Now bounded at 2s, falling back to the well-known
RFC 6052 prefix, which beats waiting.
- NAT64 was retried per failing IPv4 address, each time re-running that
lookup plus another four handshake attempts - for a path that either works
for the whole network or for none of it. Now attempted once.
Ordering alone is the dominant fix; the other two bound the tail.
This commit is contained in:
parent
f789167a22
commit
4a3fb8b944
|
|
@ -971,7 +971,21 @@ impl Bridge {
|
|||
Ok(addrs) => addrs.collect(),
|
||||
Err(e) => return Err(anyhow::anyhow!("failed to resolve server address {}: {}", self.server_addr, e)),
|
||||
};
|
||||
resolved_addrs.sort_by_key(|addr| if addr.is_ipv6() { 0 } else { 1 });
|
||||
// IPv4 first. Addresses are tried strictly in order, each burning its
|
||||
// full retry budget before the next is touched, so this ordering decides
|
||||
// how long a bad family stalls the whole connect. Mobile carriers
|
||||
// routinely hand out IPv6 with no working route and BLACKHOLE it rather
|
||||
// than rejecting, so every IPv6 candidate costs the full timeout budget
|
||||
// — with several AAAA records the working IPv4 address was not reached
|
||||
// for tens of seconds. (The same ordering bug was already fixed on the
|
||||
// server's outbound path and in the UoT connect.)
|
||||
resolved_addrs.sort_by_key(|addr| if addr.is_ipv6() { 1 } else { 0 });
|
||||
|
||||
// NAT64 is a fallback for IPv6-only networks. Retrying it per failing
|
||||
// address multiplied an already-long connect: each attempt re-runs a DNS
|
||||
// lookup and another full round of handshake retries, for a path that
|
||||
// either works for the whole network or for none of it.
|
||||
let mut nat64_attempted = false;
|
||||
|
||||
let mut last_err = anyhow::anyhow!("no IP addresses resolved for {}", self.server_addr);
|
||||
|
||||
|
|
@ -984,7 +998,8 @@ impl Bridge {
|
|||
let socket = match self.try_connect_transport(target_ip, port).await {
|
||||
Ok(sock) => sock,
|
||||
Err(e) => {
|
||||
if let std::net::IpAddr::V4(ipv4) = target_ip {
|
||||
if let (std::net::IpAddr::V4(ipv4), false) = (target_ip, nat64_attempted) {
|
||||
nat64_attempted = true;
|
||||
tx.send(UiEvent::Log(format!("Direct IPv4 connection failed: {}. Trying NAT64 fallback...", e))).await.ok();
|
||||
let nat64_ipv6 = synthesize_nat64(ipv4).await;
|
||||
match self.try_connect_transport(std::net::IpAddr::V6(nat64_ipv6), port).await {
|
||||
|
|
@ -1065,7 +1080,8 @@ impl Bridge {
|
|||
let (final_socket, size) = if success {
|
||||
(socket, size)
|
||||
} else {
|
||||
if let std::net::IpAddr::V4(ipv4) = target_ip {
|
||||
if let (std::net::IpAddr::V4(ipv4), false) = (target_ip, nat64_attempted) {
|
||||
nat64_attempted = true;
|
||||
tx.send(UiEvent::Log("Direct IPv4 handshake timed out. Trying NAT64 fallback...".to_string())).await.ok();
|
||||
let nat64_ipv6 = synthesize_nat64(ipv4).await;
|
||||
match self.try_connect_transport(std::net::IpAddr::V6(nat64_ipv6), port).await {
|
||||
|
|
@ -1309,8 +1325,19 @@ fn next_profile(current: TrafficProfile) -> TrafficProfile {
|
|||
}
|
||||
|
||||
async fn synthesize_nat64(ip: std::net::Ipv4Addr) -> std::net::Ipv6Addr {
|
||||
// Well-known prefix (RFC 6052), used if discovery doesn't answer in time.
|
||||
let mut prefix = [0x00, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
if let Ok(addrs) = tokio::net::lookup_host("ipv4only.arpa:80").await {
|
||||
// Bound the discovery lookup. This runs on exactly the networks that are
|
||||
// already misbehaving, where the resolver can hang for tens of seconds
|
||||
// before giving up — unbounded, it was a large part of why connecting over
|
||||
// a broken mobile network took minutes. Falling back to the well-known
|
||||
// prefix is strictly better than waiting.
|
||||
let discovery = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
tokio::net::lookup_host("ipv4only.arpa:80"),
|
||||
)
|
||||
.await;
|
||||
if let Ok(Ok(addrs)) = discovery {
|
||||
for addr in addrs {
|
||||
if let std::net::SocketAddr::V6(v6) = addr {
|
||||
let octets = v6.ip().octets();
|
||||
|
|
|
|||
Loading…
Reference in New Issue