fix: sliding-window rate limits and TCP connection cap

This commit is contained in:
randogoth 2026-10-06 22:13:59 +03:00
parent 0c7311b068
commit fd847d035f
7 changed files with 198 additions and 48 deletions

View file

@ -1,7 +1,7 @@
//! TCP accept loop, per-connection handling, and the background purge loop.
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::bind::TransportBindValues;
@ -28,6 +28,7 @@ pub struct ServeArgs {
pub rate_connections: u32,
pub rate_sends: u32,
pub rate_tokens: u32,
pub max_connections: usize,
}
/// The single-domain CLI path: one anonymous domain.
@ -48,6 +49,7 @@ pub fn run(args: ServeArgs) -> anyhow::Result<()> {
rate_connections: args.rate_connections,
rate_sends: args.rate_sends,
rate_tokens: args.rate_tokens,
max_connections: args.max_connections,
}])
}
@ -94,8 +96,9 @@ pub fn serve_domains(domains: Vec<DomainConfig>) -> anyhow::Result<()> {
purge_loop(purge_db_path, main_retention_secs, requests_retention_secs)
});
let gate = Arc::new(ConnGate::new(domain.max_connections));
let db_path = domain.db_path.clone();
std::thread::spawn(move || accept_loop(listener, config, static_key, db_path));
std::thread::spawn(move || accept_loop(listener, config, static_key, db_path, gate));
}
// Each domain's accept loop runs in its own thread; nothing fails here.
@ -120,6 +123,7 @@ fn accept_loop(
config: Arc<ServerConfig>,
static_key: [u8; KEY_LEN],
db_path: String,
gate: Arc<ConnGate>,
) {
for incoming in listener.incoming() {
let stream = match incoming {
@ -139,9 +143,19 @@ fn accept_loop(
continue;
}
let permit = match gate.try_acquire() {
Some(p) => p,
None => {
log::warn!("connection limit reached, refusing {peer_ip}");
continue;
}
};
let config = Arc::clone(&config);
let db_path = db_path.clone();
std::thread::spawn(move || {
// The permit is dropped with the connection, freeing its slot.
let _permit = permit;
if let Err(e) = handle_connection(stream, &config, &static_key, &db_path, &peer_ip) {
log::info!("connection error from {peer_ip}: {e}");
}
@ -149,6 +163,55 @@ fn accept_loop(
}
}
/// Cap on concurrently served TCP connections, the mirror of the RNS
/// carrier's link cap (SPEC.md sec 13.8): without one, a connection flood
/// would exhaust threads one unbounded spawn at a time. `max == 0` disables
/// the cap; past it, connections are refused, never queued.
struct ConnGate {
max: usize,
active: Mutex<usize>,
}
impl ConnGate {
fn new(max: usize) -> Self {
ConnGate {
max,
active: Mutex::new(0),
}
}
fn try_acquire(self: &Arc<Self>) -> Option<ConnPermit> {
if self.max == 0 {
return Some(ConnPermit {
gate: Arc::clone(self),
});
}
let mut active = self.active.lock().unwrap();
if *active >= self.max {
return None;
}
*active += 1;
Some(ConnPermit {
gate: Arc::clone(self),
})
}
}
/// Dropping releases the slot, so the count stays accurate whatever return
/// path or panic closes the connection.
struct ConnPermit {
gate: Arc<ConnGate>,
}
impl Drop for ConnPermit {
fn drop(&mut self) {
if self.gate.max == 0 {
return;
}
*self.gate.active.lock().unwrap() -= 1;
}
}
/// Also the hostile harness's entry point: it drives real connections
/// through the same accept/handshake/session path the TCP carrier serves.
pub(crate) fn handle_connection(
@ -265,3 +328,27 @@ fn purge_loop(db_path: String, main_retention_secs: i64, requests_retention_secs
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gate_refuses_past_cap_and_frees_on_drop() {
let gate = Arc::new(ConnGate::new(2));
let a = gate.try_acquire().unwrap();
let b = gate.try_acquire().unwrap();
assert!(gate.try_acquire().is_none());
drop(b);
assert!(gate.try_acquire().is_some());
drop(a);
}
#[test]
fn zero_cap_means_unlimited() {
let gate = Arc::new(ConnGate::new(0));
for _ in 0..100 {
assert!(gate.try_acquire().is_some());
}
}
}