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 {...@@ -935,13 +935,13 @@ impl Outbox {
935 }935 }
936 }936 }
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.
939 fn take(&self, wait: Duration) -> Option<Vec<u8>> {939 fn take(&self, wait: Duration) -> Option<Vec<u8>> {
940 let mut queue = self.queue.lock().unwrap();940 let mut queue = self.queue.lock().unwrap();
941 if queue.frames.is_empty() && !queue.closed {941 if queue.frames.is_empty() && !queue.closed {
942 queue = self.ready.wait_timeout(queue, wait).unwrap().0;942 queue = self.ready.wait_timeout(queue, wait).unwrap().0;
943 }943 }
944 if queue.closed {944 if queue.closed && queue.frames.is_empty() {
945 return None;945 return None;
946 }946 }
947 queue.bytes = 0;947 queue.bytes = 0;
...@@ -956,7 +956,7 @@ impl Outbox {...@@ -956,7 +956,7 @@ impl Outbox {
956 }956 }
957 if queue.bytes + frame.len() > self.most {957 if queue.bytes + frame.len() > self.most {
958 drop(queue);958 drop(queue);
959 self.close();959 self.abort();
960 return;960 return;
961 }961 }
962 queue.bytes += frame.len();962 queue.bytes += frame.len();
...@@ -964,14 +964,19 @@ impl Outbox {...@@ -964,14 +964,19 @@ impl Outbox {
964 self.ready.notify_one();964 self.ready.notify_one();
965 }965 }
966966
967 /// Hangs up after writing what is queued, which tells a refused guest its code was wrong.
967 fn close(&self) {968 fn close(&self) {
968 let mut queue = self.queue.lock().unwrap();969 self.queue.lock().unwrap().closed = true;
969 *queue = Queue {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 {
970 closed: true,976 closed: true,
971 ..Queue::default()977 ..Queue::default()
972 };978 };
973 self.ready.notify_one();979 self.ready.notify_one();
974 drop(queue);
975 if let Some(stream) = &self.stream {980 if let Some(stream) = &self.stream {
976 let _ = stream.shutdown(Shutdown::Both);981 let _ = stream.shutdown(Shutdown::Both);
977 }982 }
...@@ -983,13 +988,14 @@ impl Outbox {...@@ -983,13 +988,14 @@ impl Outbox {
983 let frame = {988 let frame = {
984 let mut queue = self.queue.lock().unwrap();989 let mut queue = self.queue.lock().unwrap();
985 loop {990 loop {
986 if queue.closed {
987 return;
988 }
989 if let Some(frame) = queue.frames.pop_front() {991 if let Some(frame) = queue.frames.pop_front() {
990 queue.bytes -= frame.len();992 queue.bytes -= frame.len();
991 break frame;993 break frame;
992 }994 }
995 if queue.closed {
996 drop(queue);
997 return self.abort();
998 }
993 queue = self.ready.wait(queue).unwrap();999 queue = self.ready.wait(queue).unwrap();
994 }1000 }
995 };1001 };
...@@ -997,8 +1003,7 @@ impl Outbox {...@@ -997,8 +1003,7 @@ impl Outbox {
997 return;1003 return;
998 };1004 };
999 if stream.write_all(&frame).is_err() {1005 if stream.write_all(&frame).is_err() {
1000 self.close();1006 return self.abort();
1001 return;
1002 }1007 }
1003 }1008 }
1004 }1009 }
crates/relay/tests/relay.rs+7-1
...@@ -275,7 +275,8 @@ fn an_unknown_code_or_room_is_refused() {...@@ -275,7 +275,8 @@ fn an_unknown_code_or_room_is_refused() {
275 assert_eq!(status(relay.join("/v1/room/")), "HTTP/1.1 404 Not Found");275 assert_eq!(status(relay.join("/v1/room/")), "HTTP/1.1 404 Not Found");
276}276}
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.
279#[test]280#[test]
280fn wrong_codes_burn_a_code() {281fn wrong_codes_burn_a_code() {
281 let relay = Relay::start(&["--failures-per-minute", "100"]);282 let relay = Relay::start(&["--failures-per-minute", "100"]);
...@@ -285,7 +286,12 @@ fn wrong_codes_burn_a_code() {...@@ -285,7 +286,12 @@ fn wrong_codes_burn_a_code() {
285 let mut guess = relay.join(&path).unwrap();286 let mut guess = relay.join(&path).unwrap();
286 assert_eq!(guess.text(), format!("welcome {slot} 1"));287 assert_eq!(guess.text(), format!("welcome {slot} 1"));
287 assert_eq!(owner.text(), format!("joined {slot}"));288 assert_eq!(owner.text(), format!("joined {slot}"));
289 owner.send_to(slot, b"hello");
288 owner.say(&format!("failed {slot}"));290 owner.say(&format!("failed {slot}"));
291 assert_eq!(
292 guess.hear(),
293 Message::Binary([&1u32.to_be_bytes()[..], b"hello"].concat())
294 );
289 assert!(guess.closed(Duration::from_secs(5)));295 assert!(guess.closed(Duration::from_secs(5)));
290 assert_eq!(owner.text(), format!("left {slot}"));296 assert_eq!(owner.text(), format!("left {slot}"));
291 }297 }