1//! The relay: rooms of WebSocket peers, each message passed on to the peer it names, with
2//! limits on everything a stranger can make it hold. A thread reads each connection and
3//! another writes it, from a queue capped in bytes.
4
5use crate::{BROADCAST, GROUP, Notice, SLOT, Verdict, ws};
6use std::{
7 collections::{BTreeMap, HashMap, VecDeque},
8 io::{BufReader, Write},
9 net::{IpAddr, Shutdown, TcpListener, TcpStream},
10 ops::RangeInclusive,
11 sync::{
12 Arc, Condvar, Mutex,
13 atomic::{AtomicU64, AtomicUsize, Ordering},
14 },
15 thread,
16 time::{Duration, Instant},
17};
18
19/// What a relay allows; `snowbound-relay --help` explains each.
20#[derive(Clone, Debug)]
21pub struct Config {
22 /// Counts a peer by the last `X-Forwarded-For` entry, the one its own proxy added.
23 pub trust_forwarded: bool,
24 pub max_connections: usize,
25 pub max_connections_per_address: usize,
26 pub max_rooms: usize,
27 pub max_room_peers: usize,
28 /// The largest message, in bytes.
29 pub max_message: usize,
30 /// The most bytes waiting to go to one peer before the relay hangs up on it.
31 pub queue: usize,
32 /// How long a connection may send nothing.
33 pub idle: Duration,
34 pub room_bytes_per_second: u64,
35 pub joins_per_minute: u32,
36 pub room_joins_per_minute: u32,
37 /// Wrong codes an address may try in a minute before it is locked out, a minute the first
38 /// time and twice as long each time after, up to an hour.
39 pub failures_per_minute: u32,
40 /// Wrong codes after which a code admits no one new.
41 pub burn_after: u32,
42 /// How long a peer joining a code's room has to meet its owner.
43 pub pending: Duration,
44}
45
46impl Default for Config {
47 fn default() -> Self {
48 Self {
49 trust_forwarded: false,
50 max_connections: 256,
51 max_connections_per_address: 128,
52 max_rooms: 128,
53 max_room_peers: 64,
54 max_message: 256 << 10,
55 queue: 1 << 20,
56 idle: Duration::from_secs(600),
57 room_bytes_per_second: 4 << 20,
58 joins_per_minute: 240,
59 room_joins_per_minute: 120,
60 failures_per_minute: 10,
61 burn_after: 5,
62 pending: Duration::from_secs(20),
63 }
64 }
65}
66
67const HANDSHAKE: Duration = Duration::from_secs(10);
68/// How long a polled session's `GET` waits for something to bring, how long one of its
69/// connections may idle between requests, and how long a session may ask nothing.
70const WAIT: Duration = Duration::from_secs(25);
71const KEPT: Duration = Duration::from_secs(60);
72const IDLE_POLL: Duration = Duration::from_secs(60);
73const WRITE: Duration = Duration::from_secs(30);
74const MINUTE: Duration = Duration::from_secs(60);
75const HOUR: Duration = Duration::from_secs(3600);
76/// Addresses remembered at once; past it, new ones wait.
77const ADDRESSES: usize = 1 << 16;
78const NAMEPLATES: RangeInclusive<u32> = 1..=999;
79const STACK: usize = 256 << 10;
80
81/// Serves `listener` until it fails.
82pub fn serve(listener: TcpListener, config: Config) -> std::io::Result<()> {
83 let relay = Arc::new(Relay {
84 config,
85 state: Mutex::default(),
86 connections: AtomicUsize::new(0),
87 relayed: Default::default(),
88 started: Instant::now(),
89 });
90 let sweeping = Arc::clone(&relay);
91 thread::Builder::new().name("sweep".into()).spawn(move || {
92 loop {
93 thread::sleep(Duration::from_secs(1));
94 sweeping.sweep(Instant::now());
95 }
96 })?;
97 for stream in listener.incoming() {
98 let Ok(stream) = stream else {
99 // Out of descriptors, most likely: let some close.
100 thread::sleep(Duration::from_millis(50));
101 continue;
102 };
103 if relay.connections.fetch_add(1, Ordering::AcqRel) >= relay.config.max_connections {
104 relay.connections.fetch_sub(1, Ordering::AcqRel);
105 continue;
106 }
107 let serving = Arc::clone(&relay);
108 let spawned = thread::Builder::new().stack_size(STACK).spawn(move || {
109 serving.connection(stream);
110 serving.connections.fetch_sub(1, Ordering::AcqRel);
111 });
112 if spawned.is_err() {
113 relay.connections.fetch_sub(1, Ordering::AcqRel);
114 }
115 }
116 Ok(())
117}
118
119struct Relay {
120 config: Config,
121 state: Mutex<State>,
122 /// Connections open, joined or not.
123 connections: AtomicUsize,
124 /// Bytes of peers' messages taken in, and given out.
125 relayed: [AtomicU64; 2],
126 started: Instant,
127}
128
129#[derive(Default)]
130struct State {
131 rooms: HashMap<String, Room>,
132 addresses: HashMap<IpAddr, Address>,
133 /// Peers that reach the relay by requests rather than a WebSocket, by session.
134 polls: HashMap<String, Poll>,
135}
136
137/// A peer in a room by requests: where its messages wait for its next `GET`.
138struct Poll {
139 tag: String,
140 slot: u32,
141 outbox: Arc<Outbox>,
142 /// When it last asked anything; one quiet past `IDLE_POLL` has gone.
143 last: Instant,
144}
145
146struct Room {
147 /// The slot of the peer that claimed a code's room, while it is there.
148 owner: Option<u32>,
149 next: u32,
150 members: BTreeMap<u32, Member>,
151 joins: Bucket,
152 bytes: Bucket,
153 failures: u32,
154 burned: bool,
155}
156
157struct Member {
158 outbox: Arc<Outbox>,
159 address: IpAddr,
160 /// When a peer joining a code's room joined, until its owner says it met it.
161 pending: Option<Instant>,
162}
163
164/// What one address (or IPv6 /64) is doing.
165struct Address {
166 connections: usize,
167 pending: usize,
168 joins: Bucket,
169 /// Wrong codes in the last minute.
170 failures: VecDeque<Instant>,
171 /// Lockouts in a row, each twice as long.
172 strikes: u32,
173 struck: Option<Instant>,
174 locked: Option<Instant>,
175}
176
177enum Ask {
178 /// A code's room, numbered by the relay or, coming back, as it was.
179 Claim(Option<u32>),
180 Room(String),
181}
182
183enum Refusal {
184 NotFound,
185 Gone,
186 Wait(Duration),
187 Full,
188}
189
190impl Relay {
191 /// Answers a connection's requests one after another, as a polled session sends them,
192 /// until one takes the connection over as a WebSocket or it ends.
193 fn connection(&self, stream: TcpStream) {
194 let _ = stream.set_nodelay(true);
195 let _ = stream.set_read_timeout(Some(HANDSHAKE));
196 let _ = stream.set_write_timeout(Some(WRITE));
197 let Ok(reading) = stream.try_clone() else {
198 return;
199 };
200 let mut reader = BufReader::new(reading);
201 while let Ok(head) = ws::head(&mut reader) {
202 if !self.request(&stream, &mut reader, &head) {
203 return;
204 }
205 // Between a session's requests, a connection may idle as long as one waits.
206 let _ = stream.set_read_timeout(Some(KEPT));
207 }
208 }
209
210 /// Answers the request `head`: whether the connection serves another.
211 fn request(&self, stream: &TcpStream, reader: &mut BufReader<TcpStream>, head: &str) -> bool {
212 let address = crate::peer(stream, head, self.config.trust_forwarded);
213 let target = head.split(' ').nth(1).unwrap_or_default();
214 let (path, query) = target.split_once('?').unwrap_or((target, ""));
215 if let Some(id) = path.strip_prefix("/v1/poll/") {
216 return self.polled(stream, reader, head, id);
217 }
218 let ask = match path {
219 "/health" => {
220 respond(stream, "200 OK", "application/json", "", &self.health());
221 return false;
222 }
223 "/v1/claim" => Ask::Claim(
224 query
225 .split('&')
226 .find_map(|pair| pair.strip_prefix("nameplate="))
227 .and_then(|number| number.parse().ok()),
228 ),
229 _ => match path.strip_prefix("/v1/room/").filter(|tag| valid(tag)) {
230 Some(tag) => Ask::Room(tag.into()),
231 None => {
232 respond(stream, "404 Not Found", "text/plain", "", "No such page\n");
233 return false;
234 }
235 },
236 };
237 if query.split('&').any(|pair| pair == "poll=1") {
238 let outbox = Arc::new(Outbox::mailbox(self.config.queue));
239 return match self.join(address, ask, &outbox, Instant::now()) {
240 Ok((tag, slot)) => {
241 let mut id = [0; 16];
242 if getrandom::fill(&mut id).is_err() {
243 return false;
244 }
245 let id: String = id.iter().map(|byte| format!("{byte:02x}")).collect();
246 let poll = Poll {
247 tag,
248 slot,
249 outbox,
250 last: Instant::now(),
251 };
252 self.state.lock().unwrap().polls.insert(id.clone(), poll);
253 reply(stream, "200 OK", format!("session {id}").as_bytes())
254 }
255 Err(refusal) => {
256 refuse(stream, refusal);
257 false
258 }
259 };
260 }
261 let upgrade = ws::header(head, "Upgrade")
262 .is_some_and(|value| value.eq_ignore_ascii_case("websocket"));
263 let (true, Some(key)) = (
264 upgrade && head.starts_with("GET "),
265 ws::header(head, "Sec-WebSocket-Key"),
266 ) else {
267 respond(
268 stream,
269 "400 Bad Request",
270 "text/plain",
271 "",
272 "A WebSocket, or ?poll=1\n",
273 );
274 return false;
275 };
276 let Ok(writing) = stream.try_clone() else {
277 return false;
278 };
279 let outbox = Arc::new(Outbox::new(writing, self.config.queue));
280 let (tag, slot) = match self.join(address, ask, &outbox, Instant::now()) {
281 Ok(joined) => joined,
282 Err(refusal) => {
283 refuse(stream, refusal);
284 return false;
285 }
286 };
287 let switching = format!(
288 "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\
289 Sec-WebSocket-Accept: {}\r\n\r\n",
290 ws::accept(key)
291 );
292 let writer = Arc::clone(&outbox);
293 if (&*stream).write_all(switching.as_bytes()).is_ok()
294 && thread::Builder::new()
295 .stack_size(STACK)
296 .spawn(move || writer.drain())
297 .is_ok()
298 {
299 let _ = stream.set_read_timeout(Some(self.config.idle));
300 let mut reader = ws::Reader::new(reader, self.config.max_message, true);
301 while let Ok(message) = reader.read() {
302 if !self.heard(&tag, slot, &outbox, message) {
303 break;
304 }
305 }
306 }
307 let mut state = self.state.lock().unwrap();
308 depart(&mut state, &self.config, &tag, slot, Instant::now());
309 false
310 }
311
312 /// A polled session's request: `GET` waits for what is to go to it, `POST` brings what
313 /// it sends, each a run of WebSocket frames. Whether the connection serves another.
314 fn polled(
315 &self,
316 stream: &TcpStream,
317 reader: &mut BufReader<TcpStream>,
318 head: &str,
319 id: &str,
320 ) -> bool {
321 let length: usize = ws::header(head, "Content-Length")
322 .and_then(|length| length.parse().ok())
323 .unwrap_or(0);
324 if length > self.config.max_message * 4 {
325 return false;
326 }
327 let mut body = vec![0; length];
328 if std::io::Read::read_exact(reader, &mut body).is_err() {
329 return false;
330 }
331 let session = {
332 let mut state = self.state.lock().unwrap();
333 state.polls.get_mut(id).map(|poll| {
334 poll.last = Instant::now();
335 (poll.tag.clone(), poll.slot, Arc::clone(&poll.outbox))
336 })
337 };
338 let Some((tag, slot, outbox)) = session else {
339 return reply(stream, "410 Gone", b"No such session\n");
340 };
341 if head.starts_with("POST ") {
342 let mut frames = ws::Reader::new(&body[..], self.config.max_message, true);
343 while let Ok(message) = frames.read() {
344 if !self.heard(&tag, slot, &outbox, message) {
345 self.end_poll(id);
346 return reply(stream, "410 Gone", b"Closed\n");
347 }
348 }
349 return reply(stream, "200 OK", b"");
350 }
351 let _ = stream.set_write_timeout(Some(WRITE));
352 match outbox.take(WAIT) {
353 Some(bytes) => {
354 if let Some(poll) = self.state.lock().unwrap().polls.get_mut(id) {
355 poll.last = Instant::now();
356 }
357 reply(stream, "200 OK", &bytes)
358 }
359 None => {
360 self.end_poll(id);
361 reply(stream, "410 Gone", b"Closed\n")
362 }
363 }
364 }
365
366 /// Ends the polled session `id`: it leaves its room.
367 fn end_poll(&self, id: &str) {
368 let mut state = self.state.lock().unwrap();
369 if let Some(poll) = state.polls.remove(id) {
370 depart(
371 &mut state,
372 &self.config,
373 &poll.tag,
374 poll.slot,
375 Instant::now(),
376 );
377 }
378 }
379
380 fn health(&self) -> String {
381 let state = self.state.lock().unwrap();
382 let peers: usize = state.rooms.values().map(|room| room.members.len()).sum();
383 let [taken, given] = &self.relayed;
384 format!(
385 "{{\"rooms\":{},\"peers\":{peers},\"connections\":{},\"seconds\":{},\
386 \"bytes_in\":{},\"bytes_out\":{}}}\n",
387 state.rooms.len(),
388 self.connections.load(Ordering::Acquire),
389 self.started.elapsed().as_secs(),
390 taken.load(Ordering::Relaxed),
391 given.load(Ordering::Relaxed),
392 )
393 }
394
395 /// Puts a peer from `address` in the room it asks for, telling it and those it may talk
396 /// to: its room's tag and its slot.
397 fn join(
398 &self,
399 address: IpAddr,
400 ask: Ask,
401 outbox: &Arc<Outbox>,
402 now: Instant,
403 ) -> Result<(String, u32), Refusal> {
404 let config = &self.config;
405 let mut state = self.state.lock().unwrap();
406 let State {
407 rooms, addresses, ..
408 } = &mut *state;
409 if !addresses.contains_key(&address) && addresses.len() >= ADDRESSES {
410 return Err(Refusal::Full);
411 }
412 let client = addresses
413 .entry(address)
414 .or_insert_with(|| Address::new(config, now));
415 client.refresh(now);
416 if let Some(until) = client.locked {
417 return Err(Refusal::Wait(until - now));
418 }
419 if client.connections >= config.max_connections_per_address {
420 return Err(Refusal::Wait(Duration::from_secs(10)));
421 }
422 let per_minute = f64::from(config.joins_per_minute);
423 client
424 .joins
425 .take(per_minute, per_minute / 60.0, now)
426 .map_err(Refusal::Wait)?;
427 let (tag, nameplate) = match ask {
428 Ask::Claim(back) => {
429 let free = |number: u32| {
430 rooms
431 .get(&code(number))
432 .is_none_or(|room| room.owner.is_none() && !room.burned)
433 };
434 let number = back
435 .filter(|number| NAMEPLATES.contains(number) && free(*number))
436 .or_else(|| {
437 (0..32)
438 .filter_map(|_| {
439 let mut bytes = [0; 4];
440 getrandom::fill(&mut bytes).ok()?;
441 Some(1 + u32::from_le_bytes(bytes) % NAMEPLATES.end())
442 })
443 .chain(NAMEPLATES)
444 .find(|number| free(*number))
445 })
446 .ok_or(Refusal::Full)?;
447 (code(number), Some(number))
448 }
449 Ask::Room(tag) if tag.starts_with("code-") => {
450 let room = rooms
451 .get(&tag)
452 .filter(|room| room.owner.is_some())
453 .ok_or(Refusal::NotFound)?;
454 if room.burned {
455 return Err(Refusal::Gone);
456 }
457 if client.failures.len() + client.pending >= config.failures_per_minute as usize {
458 let wait = client.failures.front().map_or(config.pending, |first| {
459 (*first + MINUTE).saturating_duration_since(now)
460 });
461 return Err(Refusal::Wait(wait));
462 }
463 (tag, None)
464 }
465 Ask::Room(tag) => (tag, None),
466 };
467 if !rooms.contains_key(&tag) && rooms.len() >= config.max_rooms {
468 return Err(Refusal::Full);
469 }
470 let room = rooms.entry(tag.clone()).or_insert_with(|| Room {
471 owner: None,
472 next: 1,
473 members: BTreeMap::new(),
474 joins: Bucket::full(f64::from(config.room_joins_per_minute), now),
475 bytes: Bucket::full(config.room_bytes_per_second as f64, now),
476 failures: 0,
477 burned: false,
478 });
479 let per_minute = f64::from(config.room_joins_per_minute);
480 let admitted = room.members.len() < config.max_room_peers;
481 let joined = admitted.then(|| room.joins.take(per_minute, per_minute / 60.0, now));
482 let refusal = match joined {
483 None => Some(Refusal::Full),
484 Some(Err(wait)) => Some(Refusal::Wait(wait)),
485 Some(Ok(())) => None,
486 };
487 if let Some(refusal) = refusal {
488 if room.members.is_empty() {
489 rooms.remove(&tag);
490 }
491 return Err(refusal);
492 }
493 let slot = room.next;
494 room.next += 1;
495 let pending = room.owner.is_some() && nameplate.is_none();
496 let visible: Vec<u32> = match room.owner {
497 Some(owner) if pending => vec![owner],
498 _ => (room.members.iter())
499 .filter(|(_, member)| member.pending.is_none())
500 .map(|(slot, _)| *slot)
501 .collect(),
502 };
503 if let Some(number) = nameplate {
504 room.owner = Some(slot);
505 outbox.push(notice(Notice::Nameplate(number)));
506 }
507 outbox.push(notice(Notice::Welcome {
508 you: slot,
509 members: visible.clone(),
510 }));
511 for other in visible {
512 room.members[&other]
513 .outbox
514 .push(notice(Notice::Joined(slot)));
515 }
516 room.members.insert(
517 slot,
518 Member {
519 outbox: Arc::clone(outbox),
520 address,
521 pending: pending.then_some(now),
522 },
523 );
524 client.connections += 1;
525 client.pending += usize::from(pending);
526 Ok((tag, slot))
527 }
528
529 /// Acts on a message from `slot`: false once it is gone or broke the protocol.
530 fn heard(&self, tag: &str, slot: u32, outbox: &Outbox, message: ws::Message) -> bool {
531 match message {
532 ws::Message::Binary(data) => {
533 let Some((to, rest)) = data.split_first_chunk::<SLOT>() else {
534 return false;
535 };
536 let to = u32::from_be_bytes(*to);
537 // The slots a group message names, then what it carries.
538 let (named, payload) = match to {
539 BROADCAST => (None, rest),
540 to if to & GROUP != 0 => {
541 let Some((named, payload)) =
542 rest.split_at_checked((to & !GROUP) as usize * SLOT)
543 else {
544 return false;
545 };
546 let named = named.chunks_exact(SLOT);
547 (
548 Some(named.map(|slot| u32::from_be_bytes(slot.try_into().unwrap()))),
549 payload,
550 )
551 }
552 _ => (None, rest),
553 };
554 let state = self.state.lock().unwrap();
555 let Some(room) = state.rooms.get(tag) else {
556 return false;
557 };
558 let Some(from) = room.members.get(&slot) else {
559 return false;
560 };
561 // One waiting for the code's owner talks to the owner alone.
562 let allowed = |to: u32| {
563 let target = room.members.get(&to)?;
564 (to != slot
565 && (from.pending.is_none() || room.owner == Some(to))
566 && (target.pending.is_none() || room.owner == Some(slot)))
567 .then_some(&target.outbox)
568 };
569 let (source, targets): (u32, Vec<&Arc<Outbox>>) = match (to, named) {
570 (BROADCAST, _) => (
571 slot | GROUP,
572 room.members.keys().filter_map(|to| allowed(*to)).collect(),
573 ),
574 (_, Some(named)) => (slot | GROUP, named.filter_map(allowed).collect()),
575 (to, None) => (slot, allowed(to).into_iter().collect()),
576 };
577 let message = ws::frame(
578 ws::BINARY,
579 &[&source.to_be_bytes()[..], payload].concat(),
580 None,
581 );
582 let targets: Vec<Arc<Outbox>> = targets.into_iter().cloned().collect();
583 drop(state);
584 // Waiting here slows the sender alone, as its socket fills.
585 let given = (SLOT + payload.len()) * targets.len();
586 let rate = self.config.room_bytes_per_second as f64;
587 let wait = match self.state.lock().unwrap().rooms.get_mut(tag) {
588 Some(room) => room.bytes.spend(given as f64, rate, Instant::now()),
589 None => return false,
590 };
591 thread::sleep(wait);
592 for target in targets {
593 target.push(message.clone());
594 }
595 let [taken, out] = &self.relayed;
596 taken.fetch_add(data.len() as u64, Ordering::Relaxed);
597 out.fetch_add(given as u64, Ordering::Relaxed);
598 true
599 }
600 ws::Message::Text(text) => {
601 if let Ok(verdict) = text.parse() {
602 self.judge(tag, slot, verdict);
603 }
604 true
605 }
606 ws::Message::Ping(payload) => {
607 outbox.push(ws::frame(ws::PONG, &payload, None));
608 true
609 }
610 ws::Message::Pong => true,
611 ws::Message::Close => false,
612 }
613 }
614
615 /// Takes a code's owner's word on the peer in a slot waiting to meet it.
616 fn judge(&self, tag: &str, from: u32, verdict: Verdict) {
617 let mut state = self.state.lock().unwrap();
618 let State {
619 rooms, addresses, ..
620 } = &mut *state;
621 let Some(room) = rooms.get_mut(tag).filter(|room| room.owner == Some(from)) else {
622 return;
623 };
624 match verdict {
625 Verdict::Met(slot) => {
626 let Some(member) = room.members.get_mut(&slot) else {
627 return;
628 };
629 if member.pending.take().is_none() {
630 return;
631 }
632 if let Some(address) = addresses.get_mut(&member.address) {
633 address.pending -= 1;
634 }
635 let newcomer = Arc::clone(&member.outbox);
636 for (other, peer) in &room.members {
637 if *other != slot && *other != from && peer.pending.is_none() {
638 newcomer.push(notice(Notice::Joined(*other)));
639 peer.outbox.push(notice(Notice::Joined(slot)));
640 }
641 }
642 }
643 Verdict::Failed(slot) => {
644 if room
645 .members
646 .get(&slot)
647 .is_some_and(|member| member.pending.is_some())
648 {
649 depart(&mut state, &self.config, tag, slot, Instant::now());
650 }
651 }
652 }
653 }
654
655 /// Fails the peers that waited too long to meet a code's owner, and forgets quiet addresses.
656 fn sweep(&self, now: Instant) {
657 let config = &self.config;
658 let mut state = self.state.lock().unwrap();
659 let late: Vec<(String, u32)> = (state.rooms.iter())
660 .flat_map(|(tag, room)| {
661 (room.members.iter())
662 .filter(|(_, member)| {
663 member.pending.is_some_and(|since| {
664 now.saturating_duration_since(since) >= config.pending
665 })
666 })
667 .map(|(slot, _)| (tag.clone(), *slot))
668 })
669 .collect();
670 for (tag, slot) in late {
671 depart(&mut state, config, &tag, slot, now);
672 }
673 let quiet: Vec<String> = (state.polls.iter())
674 .filter(|(_, poll)| now.saturating_duration_since(poll.last) >= IDLE_POLL)
675 .map(|(id, _)| id.clone())
676 .collect();
677 for id in quiet {
678 if let Some(poll) = state.polls.remove(&id) {
679 depart(&mut state, config, &poll.tag, poll.slot, now);
680 }
681 }
682 state.addresses.retain(|_, address| {
683 address.refresh(now);
684 !address.quiet(config, now)
685 });
686 }
687}
688
689/// Takes `slot` out of room `tag` and hangs up on it. One still waiting to meet a code's
690/// owner tried a wrong code; the owner leaving excuses those waiting for it.
691fn depart(state: &mut State, config: &Config, tag: &str, slot: u32, now: Instant) {
692 let State {
693 rooms, addresses, ..
694 } = state;
695 let Some(room) = rooms.get_mut(tag) else {
696 return;
697 };
698 let Some(member) = room.members.remove(&slot) else {
699 return;
700 };
701 member.outbox.close();
702 if let Some(address) = addresses.get_mut(&member.address) {
703 address.connections -= 1;
704 if member.pending.is_some() {
705 address.pending -= 1;
706 if let Some(lockout) = address.fail(now, config.failures_per_minute) {
707 eprintln!(
708 "{}: locked out for {} s after too many wrong codes",
709 member.address,
710 lockout.as_secs()
711 );
712 }
713 }
714 }
715 for peer in room.members.values() {
716 peer.outbox.push(notice(Notice::Left(slot)));
717 }
718 if member.pending.is_some() {
719 room.failures += 1;
720 if room.failures >= config.burn_after && !room.burned {
721 room.burned = true;
722 eprintln!("{tag}: burned after {} wrong codes", room.failures);
723 if let Some(owner) = room.owner.and_then(|owner| room.members.get(&owner)) {
724 owner.outbox.push(notice(Notice::Burned));
725 }
726 }
727 }
728 if room.owner == Some(slot) {
729 room.owner = None;
730 for member in room.members.values_mut() {
731 if member.pending.take().is_some() {
732 if let Some(address) = addresses.get_mut(&member.address) {
733 address.pending -= 1;
734 }
735 member.outbox.close();
736 }
737 }
738 }
739 if room.members.is_empty() {
740 rooms.remove(tag);
741 }
742}
743
744fn notice(notice: Notice) -> Vec<u8> {
745 ws::frame(ws::TEXT, notice.to_string().as_bytes(), None)
746}
747
748fn code(number: u32) -> String {
749 format!("code-{number}")
750}
751
752/// A room tag as clients make them: a hash in hex, or `code-` and a number.
753fn valid(tag: &str) -> bool {
754 (1..=64).contains(&tag.len())
755 && tag
756 .bytes()
757 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
758}
759
760/// Answers a polled session's request, keeping the connection: whether that worked.
761fn reply(mut stream: &TcpStream, status: &str, body: &[u8]) -> bool {
762 let head = format!(
763 "HTTP/1.1 {status}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\
764 Cache-Control: no-store\r\n\r\n",
765 body.len()
766 );
767 stream.write_all(head.as_bytes()).is_ok() && stream.write_all(body).is_ok()
768}
769
770fn refuse(stream: &TcpStream, refusal: Refusal) {
771 let (status, headers, body) = match refusal {
772 Refusal::NotFound => ("404 Not Found", String::new(), "No such code\n"),
773 Refusal::Gone => ("410 Gone", String::new(), "The code has expired\n"),
774 Refusal::Wait(wait) => (
775 "429 Too Many Requests",
776 // Rounded up, so that a client waiting so long finds it over.
777 format!(
778 "Retry-After: {}\r\n",
779 wait.as_secs() + u64::from(wait.subsec_nanos() > 0)
780 ),
781 "Too many tries\n",
782 ),
783 Refusal::Full => (
784 "503 Service Unavailable",
785 "Retry-After: 30\r\n".into(),
786 "The relay is full\n",
787 ),
788 };
789 respond(stream, status, "text/plain", &headers, body);
790}
791
792fn respond(mut stream: &TcpStream, status: &str, kind: &str, headers: &str, body: &str) {
793 let _ = write!(
794 stream,
795 "HTTP/1.1 {status}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\nConnection: close\r\n\
796 {headers}\r\n{body}",
797 body.len()
798 );
799}
800
801impl Address {
802 fn new(config: &Config, now: Instant) -> Self {
803 Self {
804 connections: 0,
805 pending: 0,
806 joins: Bucket::full(f64::from(config.joins_per_minute), now),
807 failures: VecDeque::new(),
808 strikes: 0,
809 struck: None,
810 locked: None,
811 }
812 }
813
814 /// Forgets failures over a minute old, a lockout that has ended, and strikes after a
815 /// quiet hour.
816 fn refresh(&mut self, now: Instant) {
817 while self
818 .failures
819 .front()
820 .is_some_and(|failed| now.saturating_duration_since(*failed) >= MINUTE)
821 {
822 self.failures.pop_front();
823 }
824 self.locked = self.locked.filter(|until| *until > now);
825 if self
826 .struck
827 .is_some_and(|struck| now.saturating_duration_since(struck) >= HOUR)
828 {
829 self.strikes = 0;
830 self.struck = None;
831 }
832 }
833
834 /// Counts a wrong code: at `limit` in a minute, the lockout it starts.
835 fn fail(&mut self, now: Instant, limit: u32) -> Option<Duration> {
836 self.refresh(now);
837 self.failures.push_back(now);
838 if self.failures.len() < limit as usize {
839 return None;
840 }
841 self.failures.clear();
842 self.strikes += 1;
843 self.struck = Some(now);
844 let lockout = MINUTE
845 .saturating_mul(1 << (self.strikes - 1).min(6))
846 .min(HOUR);
847 self.locked = Some(now + lockout);
848 Some(lockout)
849 }
850
851 /// Whether nothing about this address needs remembering.
852 fn quiet(&self, config: &Config, now: Instant) -> bool {
853 let mut joins = self.joins;
854 let per_minute = f64::from(config.joins_per_minute);
855 self.connections == 0
856 && self.pending == 0
857 && self.failures.is_empty()
858 && self.locked.is_none()
859 && self.strikes == 0
860 && joins.take(per_minute, per_minute / 60.0, now).is_ok()
861 }
862}
863
864/// A token bucket.
865#[derive(Clone, Copy)]
866struct Bucket {
867 tokens: f64,
868 at: Instant,
869}
870
871impl Bucket {
872 fn full(capacity: f64, now: Instant) -> Self {
873 Self {
874 tokens: capacity,
875 at: now,
876 }
877 }
878
879 fn refill(&mut self, capacity: f64, rate: f64, now: Instant) {
880 let elapsed = now.saturating_duration_since(self.at).as_secs_f64();
881 self.tokens = (self.tokens + rate * elapsed).min(capacity);
882 self.at = self.at.max(now);
883 }
884
885 /// Takes one token from a bucket of `capacity` refilling at `rate` a second; else how
886 /// long until one is there.
887 fn take(&mut self, capacity: f64, rate: f64, now: Instant) -> Result<(), Duration> {
888 self.refill(capacity, rate, now);
889 if self.tokens >= 1.0 {
890 self.tokens -= 1.0;
891 Ok(())
892 } else {
893 Err(Duration::from_secs_f64((1.0 - self.tokens) / rate))
894 }
895 }
896
897 /// Spends `amount` from a bucket holding a second's worth at `rate`, into debt if need
898 /// be: how long until the debt is repaid.
899 fn spend(&mut self, amount: f64, rate: f64, now: Instant) -> Duration {
900 self.refill(rate, rate, now);
901 self.tokens -= amount;
902 Duration::from_secs_f64((-self.tokens / rate).max(0.0))
903 }
904}
905
906/// What waits to be written to one peer, capped in bytes.
907struct Outbox {
908 /// Where its frames go; none for a polled session's, which its `GET`s take.
909 stream: Option<TcpStream>,
910 queue: Mutex<Queue>,
911 ready: Condvar,
912 most: usize,
913}
914
915#[derive(Default)]
916struct Queue {
917 frames: VecDeque<Vec<u8>>,
918 bytes: usize,
919 closed: bool,
920}
921
922impl Outbox {
923 fn new(stream: TcpStream, most: usize) -> Self {
924 Self {
925 stream: Some(stream),
926 queue: Mutex::default(),
927 ready: Condvar::new(),
928 most,
929 }
930 }
931
932 fn mailbox(most: usize) -> Self {
933 Self {
934 stream: None,
935 queue: Mutex::default(),
936 ready: Condvar::new(),
937 most,
938 }
939 }
940
941 /// Everything queued, waiting up to `wait` for something; none once closed.
942 fn take(&self, wait: Duration) -> Option<Vec<u8>> {
943 let mut queue = self.queue.lock().unwrap();
944 if queue.frames.is_empty() && !queue.closed {
945 queue = self.ready.wait_timeout(queue, wait).unwrap().0;
946 }
947 if queue.closed {
948 return None;
949 }
950 queue.bytes = 0;
951 Some(queue.frames.drain(..).flatten().collect())
952 }
953
954 /// Queues `frame`, or hangs up on a peer that reads too slowly to take it.
955 fn push(&self, frame: Vec<u8>) {
956 let mut queue = self.queue.lock().unwrap();
957 if queue.closed {
958 return;
959 }
960 if queue.bytes + frame.len() > self.most {
961 drop(queue);
962 self.close();
963 return;
964 }
965 queue.bytes += frame.len();
966 queue.frames.push_back(frame);
967 self.ready.notify_one();
968 }
969
970 fn close(&self) {
971 let mut queue = self.queue.lock().unwrap();
972 *queue = Queue {
973 closed: true,
974 ..Queue::default()
975 };
976 self.ready.notify_one();
977 drop(queue);
978 if let Some(stream) = &self.stream {
979 let _ = stream.shutdown(Shutdown::Both);
980 }
981 }
982
983 /// Writes what is queued until closed.
984 fn drain(&self) {
985 loop {
986 let frame = {
987 let mut queue = self.queue.lock().unwrap();
988 loop {
989 if queue.closed {
990 return;
991 }
992 if let Some(frame) = queue.frames.pop_front() {
993 queue.bytes -= frame.len();
994 break frame;
995 }
996 queue = self.ready.wait(queue).unwrap();
997 }
998 };
999 let Some(mut stream) = self.stream.as_ref() else {
1000 return;
1001 };
1002 if stream.write_all(&frame).is_err() {
1003 self.close();
1004 return;
1005 }
1006 }
1007 }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013
1014 /// Ten wrong codes in a minute lock an address out for a minute, then two, then four,
1015 /// and a quiet hour forgives it.
1016 #[test]
1017 fn wrong_codes_lock_out_for_longer_each_time() {
1018 let config = Config::default();
1019 let start = Instant::now();
1020 let mut address = Address::new(&config, start);
1021 let mut now = start;
1022 for expected in [1, 2, 4] {
1023 for _ in 0..9 {
1024 assert_eq!(address.fail(now, 10), None);
1025 }
1026 assert_eq!(address.fail(now, 10), Some(MINUTE * expected));
1027 now += MINUTE * expected;
1028 }
1029 now += HOUR;
1030 address.refresh(now);
1031 assert_eq!(address.strikes, 0);
1032 assert!(address.quiet(&config, now));
1033 }
1034
1035 /// Failures spread out over more than a minute never add up to a lockout.
1036 #[test]
1037 fn slow_wrong_codes_never_lock_out() {
1038 let start = Instant::now();
1039 let mut address = Address::new(&Config::default(), start);
1040 for minute in 0..30 {
1041 for second in [0, 30] {
1042 let now = start + MINUTE * minute + Duration::from_secs(second);
1043 assert_eq!(address.fail(now, 10), None);
1044 }
1045 }
1046 }
1047
1048 #[test]
1049 fn buckets_refill_and_debts_are_waited_out() {
1050 let now = Instant::now();
1051 let mut joins = Bucket::full(2.0, now);
1052 assert!(joins.take(2.0, 1.0, now).is_ok());
1053 assert!(joins.take(2.0, 1.0, now).is_ok());
1054 assert_eq!(joins.take(2.0, 1.0, now), Err(Duration::from_secs(1)));
1055 assert!(joins.take(2.0, 1.0, now + Duration::from_secs(1)).is_ok());
1056
1057 let mut bytes = Bucket::full(100.0, now);
1058 assert_eq!(bytes.spend(100.0, 100.0, now), Duration::ZERO);
1059 assert_eq!(bytes.spend(50.0, 100.0, now), Duration::from_millis(500));
1060 }
1061
1062 #[test]
1063 fn ipv6_addresses_count_by_their_64() {
1064 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1065 let stream = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1066 let head = "GET / HTTP/1.1\r\nX-Forwarded-For: 1.2.3.4, 2001:db8:1:2:3:4:5:6\r\n\r\n";
1067 assert_eq!(
1068 crate::peer(&stream, head, true),
1069 "2001:db8:1:2::".parse::<IpAddr>().unwrap()
1070 );
1071 assert_eq!(
1072 crate::peer(&stream, head, false),
1073 "127.0.0.1".parse::<IpAddr>().unwrap()
1074 );
1075 }
1076}