fix: sliding-window rate limits and TCP connection cap
This commit is contained in:
parent
0c7311b068
commit
fd847d035f
7 changed files with 198 additions and 48 deletions
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue