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