From f2e89c094937c86ace1a3078d7d48d6056a5d933 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Thu, 15 Oct 2026 07:57:52 -0700 Subject: [PATCH] fix(live): a refused guest hears its code was wrong before the relay hangs up When a code's owner judged a wrong guess before the guest did, its "failed" verdict made the relay close the guest's outbox, which threw away the owner's hello still queued for it. The guest saw its line break instead of a wrong secret, dialled the room again and guessed a second time, so one mistyped code spent two of the tries before a burn and two of the address's failures a minute. A guest could then read the next wrong code as expired, or be locked out early. The relay now writes what is queued before hanging up, and drops it only for a peer too slow to take it or a write that failed. Fixes #150 Assisted-by: claude-opus-5.5 --- crates/relay/src/server.rs | 27 ++++++++++++++++----------- crates/relay/tests/relay.rs | 8 +++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/crates/relay/src/server.rs b/crates/relay/src/server.rs index 2ce8cb466d80aeca68e791d61f6ea4e495bd59c7..388312dc25add7d0260a74253b7eb6ae6444617a 100644 --- a/crates/relay/src/server.rs +++ b/crates/relay/src/server.rs @@ -935,13 +935,13 @@ impl Outbox { } } - /// Everything queued, waiting up to `wait` for something; none once closed. + /// Everything queued, waiting up to `wait` for something; none once closed and empty. fn take(&self, wait: Duration) -> Option> { let mut queue = self.queue.lock().unwrap(); if queue.frames.is_empty() && !queue.closed { queue = self.ready.wait_timeout(queue, wait).unwrap().0; } - if queue.closed { + if queue.closed && queue.frames.is_empty() { return None; } queue.bytes = 0; @@ -956,7 +956,7 @@ impl Outbox { } if queue.bytes + frame.len() > self.most { drop(queue); - self.close(); + self.abort(); return; } queue.bytes += frame.len(); @@ -964,14 +964,19 @@ impl Outbox { self.ready.notify_one(); } + /// Hangs up after writing what is queued, which tells a refused guest its code was wrong. fn close(&self) { - let mut queue = self.queue.lock().unwrap(); - *queue = Queue { + self.queue.lock().unwrap().closed = true; + self.ready.notify_one(); + } + + /// Hangs up now, dropping what is queued. + fn abort(&self) { + *self.queue.lock().unwrap() = Queue { closed: true, ..Queue::default() }; self.ready.notify_one(); - drop(queue); if let Some(stream) = &self.stream { let _ = stream.shutdown(Shutdown::Both); } @@ -983,13 +988,14 @@ impl Outbox { let frame = { let mut queue = self.queue.lock().unwrap(); loop { - if queue.closed { - return; - } if let Some(frame) = queue.frames.pop_front() { queue.bytes -= frame.len(); break frame; } + if queue.closed { + drop(queue); + return self.abort(); + } queue = self.ready.wait(queue).unwrap(); } }; @@ -997,8 +1003,7 @@ impl Outbox { return; }; if stream.write_all(&frame).is_err() { - self.close(); - return; + return self.abort(); } } } diff --git a/crates/relay/tests/relay.rs b/crates/relay/tests/relay.rs index 34d1b1f64e3d43a9de9bd9c158ba129130ae8049..240d765708d243dcb436ca141c674435da6ccbcb 100644 --- a/crates/relay/tests/relay.rs +++ b/crates/relay/tests/relay.rs @@ -275,7 +275,8 @@ fn an_unknown_code_or_room_is_refused() { assert_eq!(status(relay.join("/v1/room/")), "HTTP/1.1 404 Not Found"); } -/// Five wrong codes burn a code: it admits no one new, and its owner hears so. +/// Five wrong codes burn a code: it admits no one new, and its owner hears so. A wrong guess +/// still hears what the owner said before refusing it. #[test] fn wrong_codes_burn_a_code() { let relay = Relay::start(&["--failures-per-minute", "100"]); @@ -285,7 +286,12 @@ fn wrong_codes_burn_a_code() { let mut guess = relay.join(&path).unwrap(); assert_eq!(guess.text(), format!("welcome {slot} 1")); assert_eq!(owner.text(), format!("joined {slot}")); + owner.send_to(slot, b"hello"); owner.say(&format!("failed {slot}")); + assert_eq!( + guess.hear(), + Message::Binary([&1u32.to_be_bytes()[..], b"hello"].concat()) + ); assert!(guess.closed(Duration::from_secs(5))); assert_eq!(owner.text(), format!("left {slot}")); } -- 2.54.0