fix(client): more resilient UDP handshake on lossy mobile links

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.
This commit is contained in:
ospab 2026-08-20 00:26:43 +03:00
parent 0bb7db4f01
commit ff8598e512
1 changed files with 15 additions and 3 deletions

View File

@ -1165,7 +1165,10 @@ impl Bridge {
let mut success = false; let mut success = false;
let is_uot = matches!(socket, crate::transport::Transport::Uot { .. }); 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 // 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 // on-path DPI box but expire before the server, so the box classifies
@ -1224,9 +1227,18 @@ impl Bridge {
if attempt > 0 { if attempt > 0 {
tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok();
} }
// 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() { 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); 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 { match timeout(Duration::from_millis(attempt_timeout_ms), socket.recv(&mut buf)).await {
Ok(Ok(n)) => { Ok(Ok(n)) => {