authorclover caruso <git@paperclover.net> 2026-10-15 07:57:52-07:00
committerclover caruso <git@paperclover.net> 2026-10-15 08:34:53-07:00
logf2e89c094937c86ace1a3078d7d48d6056a5d933
tree449faa43d4ad8adc7486317ba2bacd222c891e38
parent1acd26a738c5504b6ce961d8065e7149b3035241
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

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

2 files changed, 23 insertions(+), 12 deletions(-)

crates/relay/src/server.rs+16-11
......@@ -935,13 +935,13 @@ impl Outbox {
935935 }
936936 }
937937
938 /// Everything queued, waiting up to `wait` for something; none once closed.
938 /// Everything queued, waiting up to `wait` for something; none once closed and empty.
939939 fn take(&self, wait: Duration) -> Option<Vec<u8>> {
940940 let mut queue = self.queue.lock().unwrap();
941941 if queue.frames.is_empty() && !queue.closed {
942942 queue = self.ready.wait_timeout(queue, wait).unwrap().0;
943943 }
944 if queue.closed {
944 if queue.closed && queue.frames.is_empty() {
945945 return None;
946946 }
947947 queue.bytes = 0;
......@@ -956,7 +956,7 @@ impl Outbox {
956956 }
957957 if queue.bytes + frame.len() > self.most {
958958 drop(queue);
959 self.close();
959 self.abort();
960960 return;
961961 }
962962 queue.bytes += frame.len();
......@@ -964,14 +964,19 @@ impl Outbox {
964964 self.ready.notify_one();
965965 }
966966
967 /// Hangs up after writing what is queued, which tells a refused guest its code was wrong.
967968 fn close(&self) {
968 let mut queue = self.queue.lock().unwrap();
969 *queue = Queue {
969 self.queue.lock().unwrap().closed = true;
970 self.ready.notify_one();
971 }
972
973 /// Hangs up now, dropping what is queued.
974 fn abort(&self) {
975 *self.queue.lock().unwrap() = Queue {
970976 closed: true,
971977 ..Queue::default()
972978 };
973979 self.ready.notify_one();
974 drop(queue);
975980 if let Some(stream) = &self.stream {
976981 let _ = stream.shutdown(Shutdown::Both);
977982 }
......@@ -983,13 +988,14 @@ impl Outbox {
983988 let frame = {
984989 let mut queue = self.queue.lock().unwrap();
985990 loop {
986 if queue.closed {
987 return;
988 }
989991 if let Some(frame) = queue.frames.pop_front() {
990992 queue.bytes -= frame.len();
991993 break frame;
992994 }
995 if queue.closed {
996 drop(queue);
997 return self.abort();
998 }
993999 queue = self.ready.wait(queue).unwrap();
9941000 }
9951001 };
......@@ -997,8 +1003,7 @@ impl Outbox {
9971003 return;
9981004 };
9991005 if stream.write_all(&frame).is_err() {
1000 self.close();
1001 return;
1006 return self.abort();
10021007 }
10031008 }
10041009 }
crates/relay/tests/relay.rs+7-1
......@@ -275,7 +275,8 @@ fn an_unknown_code_or_room_is_refused() {
275275 assert_eq!(status(relay.join("/v1/room/")), "HTTP/1.1 404 Not Found");
276276}
277277
278/// Five wrong codes burn a code: it admits no one new, and its owner hears so.
278/// Five wrong codes burn a code: it admits no one new, and its owner hears so. A wrong guess
279/// still hears what the owner said before refusing it.
279280#[test]
280281fn wrong_codes_burn_a_code() {
281282 let relay = Relay::start(&["--failures-per-minute", "100"]);
......@@ -285,7 +286,12 @@ fn wrong_codes_burn_a_code() {
285286 let mut guess = relay.join(&path).unwrap();
286287 assert_eq!(guess.text(), format!("welcome {slot} 1"));
287288 assert_eq!(owner.text(), format!("joined {slot}"));
289 owner.send_to(slot, b"hello");
288290 owner.say(&format!("failed {slot}"));
291 assert_eq!(
292 guess.hear(),
293 Message::Binary([&1u32.to_be_bytes()[..], b"hello"].concat())
294 );
289295 assert!(guess.closed(Duration::from_secs(5)));
290296 assert_eq!(owner.text(), format!("left {slot}"));
291297 }