diff --git a/.github/workflows/golden.yml b/.github/workflows/golden.yml index a8c9f894..f45c86cc 100644 --- a/.github/workflows/golden.yml +++ b/.github/workflows/golden.yml @@ -1139,0 +1140,6 @@ jobs: + # DELIBERATELY NOT THE CEREMONY'S 900. This budget bounds RENDEZVOUS + # skew (how long a half waits for the other to reach its own step), + # not pairing, so a blocked link costs 300 s ONCE here instead of 900 + # per cell per half below. Set explicitly because from_env's default + # is invisible at the step, and the step beside it says 900. + SPT_TWO_HOST_WAIT_SECS: "300" @@ -1301,0 +1308,6 @@ jobs: + # DELIBERATELY NOT THE CEREMONY'S 900. This budget bounds RENDEZVOUS + # skew (how long a half waits for the other to reach its own step), + # not pairing, so a blocked link costs 300 s ONCE here instead of 900 + # per cell per half below. Set explicitly because from_env's default + # is invisible at the step, and the step beside it says 900. + SPT_TWO_HOST_WAIT_SECS: "300" diff --git a/crates/spt-daemon/tests/twohost_web.rs b/crates/spt-daemon/tests/twohost_web.rs index 7022c2de..da655110 100644 --- a/crates/spt-daemon/tests/twohost_web.rs +++ b/crates/spt-daemon/tests/twohost_web.rs @@ -128,3 +128,4 @@ const PROBE_OFFSET: u16 = 9; -/// How long A listens and B sends. Ten seconds against the ceremony's 900: the -/// point of the probe is that a blocked link is answered in the time it takes -/// to notice, not in the time it takes to give up. +/// B's SEND window, started at the first beacon — not A's, which listens for the +/// whole rig budget. Ten seconds against the ceremony's 900: the point of the +/// probe is that a blocked link is answered in the time it takes to notice, not +/// in the time it takes to give up. @@ -132,0 +134,4 @@ const PROBE_MAGIC: &[u8] = b"SPT-TWOHOST-INBOUND-PROBE-v1"; +/// How long A keeps acking after its verdict is already decided. Long enough to +/// cover several of B's 500 ms probes, so a single dropped ack cannot make the +/// two halves disagree about a link they both just used. +const PROBE_ACK_WINDOW: Duration = Duration::from_secs(2); @@ -458,0 +464,20 @@ fn two_host_inbound_probe_role_a() { + // ONE ACK IS NOT ENOUGH, and the failure it leaves behind is the + // self-contradicting run this cell already carries a fix for — + // arriving by a different road. A single unrepeated UDP datagram + // that drops leaves A printing INBOUND OK while B reds + // INBOUND_BLOCKED and blames a firewall for packet loss. So A + // keeps draining briefly and acks EVERY probe it sees in that + // window (B sends one per 500 ms, so this covers four), while B + // stops on the first ack it gets. A's own verdict is unchanged: + // it was decided by the datagram above. + let ack_until = Instant::now() + PROBE_ACK_WINDOW; + while Instant::now() < ack_until { + match socket.recv_from(&mut buf) { + Ok((n, _)) if buf[..n] == *PROBE_MAGIC => { + seen += 1; + let _ = beacon_socket.send_to(PROBE_ACK, beacon_target); + } + _ => {} + } + } + println!("TWOHOST-WEB probe role A: acked {seen} probe(s) in {PROBE_ACK_WINDOW:?}"); @@ -551,3 +576,3 @@ fn two_host_inbound_probe_role_b() { - // cold-start convention nobody will remember. The ack comes back to this - // ephemeral port because A replies to `from`; that direction is open anyway - // and nothing is inferred from it. + // cold-start convention nobody will remember. This socket only ever SENDS: + // the ack comes back to the fixed socket above, because A addresses it to + // this host's rig port (the only address either host is reachable on cold). @@ -556,3 +580,0 @@ fn two_host_inbound_probe_role_b() { - probe_socket - .set_read_timeout(Some(Duration::from_millis(250))) - .expect("probe sender takes a read timeout");