socketengine: per-IP accept-rate limiter (token bucket, accept_rate/accept_burst, off by default) — drops connection-churn floods at the accept edge before any state is allocated; exempts trusted proxies and server links

This commit is contained in:
Jean Chevronnet 2026-08-12 17:05:24 +00:00
parent 2aaa5ac091
commit 41826c8e1a
4 changed files with 136 additions and 7 deletions

View file

@ -76,6 +76,10 @@ impl Drop for Server {
impl Server {
fn start(io_threads: usize, tls: bool, hs_timeout: u32) -> Server {
Server::start_full(io_threads, tls, hs_timeout, 0)
}
fn start_full(io_threads: usize, tls: bool, hs_timeout: u32, accept_rate: usize) -> Server {
let (plain, tlsp, s2s) = (free_port(), free_port(), free_port());
let dir = std::env::temp_dir().join(format!("echoircd-it-{}-{plain}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
@ -83,6 +87,9 @@ impl Server {
"servername = it.test\nnetwork = itNet\nbind = 127.0.0.1:{plain}\n\
bind_server = 127.0.0.1:{s2s}\nsid = 1AA\nmotd = hi\nio_threads = {io_threads}\n"
);
if accept_rate > 0 {
conf.push_str(&format!("accept_rate = {accept_rate}\naccept_burst = {accept_rate}\n"));
}
if tls {
let (cert, key) = gen_cert();
let (cp, kp) = (dir.join("cert.pem"), dir.join("key.pem"));
@ -265,6 +272,35 @@ fn tls_in_reactor_handshake_and_cross_transport() {
);
}
#[test]
fn accept_rate_limit_drops_connection_churn() {
// rate/burst = 5: a rapid burst of 20 connections from one IP must be partly dropped
// at the accept edge — some register, but not all 20.
let srv = Server::start_full(2, false, 5, 5);
let mut socks = Vec::new();
for i in 0..20 {
if let Ok(mut s) = TcpStream::connect(("127.0.0.1", srv.plain)) {
s.set_read_timeout(Some(Duration::from_millis(600))).unwrap();
let _ = s.write_all(format!("NICK n{i}\r\nUSER n{i} 0 * :n\r\n").as_bytes());
socks.push(s);
}
}
let mut registered = 0;
for s in socks.iter_mut() {
if read_until(s, " 001 ", Duration::from_millis(800)) {
registered += 1;
}
}
assert!(
registered < 20,
"rate limit didn't drop any of a 20-connection burst ({registered} registered)"
);
assert!(
registered >= 3,
"rate limit dropped too much — burst of 5 should let at least a few through ({registered})"
);
}
#[test]
fn tls_stalled_handshake_is_reaped() {
// 2s handshake timeout: a raw TCP connection to the TLS port that never negotiates