From ff8598e51243464fc9a29e599a5aead5be79c2b3 Mon Sep 17 00:00:00 2001 From: ospab Date: Thu, 20 Aug 2026 00:26:43 +0300 Subject: [PATCH] fix(client): more resilient UDP handshake on lossy mobile links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A mobile-network connect showed the handshake completing with rtt=3321ms — it took several attempts because handshake datagrams were being lost, and each loss cost a full 1200ms retransmit window. Sometimes all four attempts were lost and the connect failed outright. Two changes, no increase in the worst-case budget: - UDP now retransmits over 6 windows of 800ms instead of 4 of 1200ms (≈ the same 4.8s total), so a lost handshake recovers faster and there are more chances before giving up / falling back to NAT64. - Each UDP attempt sends the handshake twice. A single dropped datagram no longer costs a whole window; whichever copy lands first is processed and the server's anti-replay drops the duplicate. UoT rides reliable TCP, so it still sends one. This targets loss-driven delay and failure, which the log shows. It does not cure a carrier that deterministically holds the flow for seconds regardless of retries — that is the throttling case, which needs the mimicry work, not more retransmits. --- ostp-client/src/bridge.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index daa8f06..651d015 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -1165,7 +1165,10 @@ impl Bridge { let mut success = false; let is_uot = matches!(socket, crate::transport::Transport::Uot { .. }); - let (attempt_limit, attempt_timeout_ms) = if is_uot { (1, 8000) } else { (4, 1200) }; + // UDP: more, shorter retransmit windows over the SAME total budget + // (6×800ms ≈ the old 4×1200ms), so a lost handshake on a mobile link + // recovers faster and there are more chances before giving up. + let (attempt_limit, attempt_timeout_ms) = if is_uot { (1, 8000) } else { (6, 800) }; // TTL-desync (UDP only, opt-in): fire decoy datagrams that reach an // on-path DPI box but expire before the server, so the box classifies @@ -1224,8 +1227,17 @@ impl Bridge { if attempt > 0 { tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); } - if send_datagram(&socket, &handshake_frame, self.transport_mode == "udp").await.is_ok() { - self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed); + // Send the handshake twice on UDP: a single dropped datagram + // otherwise costs a whole retransmit window, which on a lossy + // mobile link is the gap between a sub-second connect and a + // multi-second one. The duplicate is harmless — whichever copy + // arrives first is processed, and the server's anti-replay drops + // the other. UoT rides reliable TCP, so one send there. + let sends = if is_uot { 1 } else { 2 }; + for _ in 0..sends { + if send_datagram(&socket, &handshake_frame, self.transport_mode == "udp").await.is_ok() { + self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed); + } } match timeout(Duration::from_millis(attempt_timeout_ms), socket.recv(&mut buf)).await {