Phase 8c hotfix: skip probe deadline when no fallback exists
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 2s

Live smoke on tank↔architect (both LAN) failed with 200ms probe:
LAN handshake takes longer than that in the wild (TLS 1.3 with
full cert chain + rustls startup on fresh endpoint). The old
single-addr .connect() had no deadline, so pre-8c callers never
noticed.

Fix: when `tailscale` is `None`, treat LAN as unlimited — the
probe deadline only matters as a fall-through trigger, and
there's nothing to fall through to. Callers with a real fallback
addr still get the fast-path routing behavior unchanged.

+1 test (connect_lan_first_lan_only_ignores_probe_deadline)
using a 1-nanosecond probe budget that a real handshake could
never meet — must succeed anyway because no fallback exists.

376 tests pass (+1).
This commit is contained in:
Omar Sobh
2026-07-14 11:45:29 -07:00
parent 38652c5886
commit f2c056464c
+39
View File
@@ -545,6 +545,17 @@ impl QuicClient {
lan_probe: std::time::Duration,
) -> Result<(quinn::Connection, ConnectRoute)> {
if let Some(lan_addr) = lan {
// No fallback path → don't apply the probe deadline.
// Otherwise a slow-but-fine LAN handshake can spuriously
// fail when the operator never opted into a tailnet
// fallback in the first place.
if tailscale.is_none() {
let conn = self
.connect(lan_addr, expected_server_name)
.await
.with_context(|| format!("dialing LAN addr {lan_addr}"))?;
return Ok((conn, ConnectRoute::Lan(lan_addr)));
}
match tokio::time::timeout(
lan_probe,
self.connect(lan_addr, expected_server_name),
@@ -840,6 +851,34 @@ mod tests {
accept_task.abort();
}
#[tokio::test]
async fn connect_lan_first_lan_only_ignores_probe_deadline() {
// Phase 8c hotfix: when no fallback exists, LAN dial gets
// unlimited time — otherwise a slow-but-fine handshake
// fails a caller that never opted into a fallback path.
let (id_a, id_b) = NodeIdentity::generate_test_pair("a", "b").unwrap();
let server = QuicServer::bind(loopback(0), id_b).unwrap();
let server_addr = server.local_addr().unwrap();
let accept_task = tokio::spawn(async move {
if let Some(res) = server.accept().await {
let conn = res.expect("accept");
let _ = ping_handler_loop(conn).await;
}
server.shutdown().await;
});
let client = QuicClient::new(loopback(0), id_a).unwrap();
// Absurdly short probe. Would fail if the deadline applied.
let (conn, route) = client
.connect_lan_first("b", Some(server_addr), None, Duration::from_nanos(1))
.await
.expect("connect_lan_first with no fallback ignores deadline");
assert!(matches!(route, ConnectRoute::Lan(a) if a == server_addr));
conn.close(VarInt::from_u32(0), b"done");
client.shutdown().await;
tokio::time::sleep(Duration::from_millis(50)).await;
accept_task.abort();
}
#[tokio::test]
async fn connect_lan_first_errors_when_both_addrs_absent() {
let (id_a, _id_b) = NodeIdentity::generate_test_pair("a", "b").unwrap();