connflood: refuse connection floods from a single IP

This commit is contained in:
Jean Chevronnet 2026-08-09 01:28:09 +00:00
parent 1eca745092
commit d4ce5c008f
4 changed files with 49 additions and 1 deletions

View file

@ -7,7 +7,7 @@
use std::cell::RefCell;
use std::collections::{HashMap, HashSet, VecDeque};
use std::net::{SocketAddr, TcpStream};
use std::net::{IpAddr, SocketAddr, TcpStream};
use std::sync::atomic::AtomicU64;
use std::sync::mpsc::Sender;
use std::sync::Arc;
@ -135,6 +135,8 @@ pub struct Server {
pub opermotd: Vec<String>, // OPERMOTD text
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick)
pub connflood: Option<(u32, u64)>, // (max, secs) connection throttle per IP
pub conn_history: HashMap<IpAddr, Vec<u64>>, // recent connection times per IP (connflood)
// labeled-response: while Some((uid, buf)), that client's own responses are
// diverted into `buf` instead of the socket, so `on_line` can wrap them with
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
@ -189,6 +191,8 @@ impl Server {
opermotd: cfg.opermotd,
vhosts: cfg.vhosts,
aliases: cfg.aliases,
connflood: cfg.connflood,
conn_history: HashMap::new(),
label_capture: RefCell::new(None),
event_tx,
conn_counter,
@ -273,6 +277,22 @@ impl Server {
},
);
// connflood — refuse an IP that's opening connections too fast
if let Some((max, secs)) = self.connflood {
let n = now();
let hist = self.conn_history.entry(ip).or_default();
hist.retain(|&t| n.saturating_sub(t) < secs);
hist.push(n);
if hist.len() as u32 > max {
self.send(
uid,
"ERROR :Closing link: (Too many connections from your IP)".to_string(),
);
self.remove_user(uid, "Connection throttled");
return;
}
}
// Pre-registration connection notices, InspIRCd / solanum style. Ident-113
// is archaic and firewalled, so those two are cosmetic; the hostname lookup
// is real (see `resolver`) — its result arrives later as an Event.
@ -317,6 +337,17 @@ impl Server {
}
}
/// Drop stale per-IP connflood bookkeeping (called on the background tick).
pub fn prune_conn_history(&mut self) {
if let Some((_, secs)) = self.connflood {
let n = now();
self.conn_history.retain(|_, times| {
times.retain(|&t| n.saturating_sub(t) < secs);
!times.is_empty()
});
}
}
/// A pre-registration `:server NOTICE * :*** <msg>` line.
pub(crate) fn notice_star(&self, uid: Uid, msg: &str) {
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));