From d4ce5c008f64f0352978ca336ac216ecdceeaf9e Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 01:28:09 +0000 Subject: [PATCH] connflood: refuse connection floods from a single IP --- echoircd.conf.example | 3 +++ src/config.rs | 13 +++++++++++++ src/ircd.rs | 1 + src/server.rs | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/echoircd.conf.example b/echoircd.conf.example index 66ba9d1..13a753e 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -80,3 +80,6 @@ amu_target = both # --- command aliases: /NS ... -> PRIVMSG :... (services shortcuts) --- # alias = NS NickServ # alias = CS ChanServ + +# --- connflood: refuse >max connections per from a single IP --- +# connflood = 5 10 diff --git a/src/config.rs b/src/config.rs index fc82dd8..eca3d4f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -79,6 +79,7 @@ pub struct Config { pub opermotd: Vec, // OPERMOTD text, one line per entry 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 conns, per secs) from one IP before refusing } impl Default for Config { @@ -110,6 +111,7 @@ impl Default for Config { opermotd: Vec::new(), vhosts: Vec::new(), aliases: Vec::new(), + connflood: None, } } } @@ -268,6 +270,17 @@ impl Config { .push((name.to_ascii_uppercase(), target.to_string())); } } + "connflood" => { + // connflood = — refuse >max connections/secs from one IP + let mut it = v.split_whitespace(); + if let (Some(mx), Some(sc)) = (it.next(), it.next()) { + if let (Ok(mx), Ok(sc)) = (mx.parse::(), sc.parse::()) { + if mx > 0 && sc > 0 { + c.connflood = Some((mx, sc)); + } + } + } + } _ => {} } } diff --git a/src/ircd.rs b/src/ircd.rs index 86ddb91..1a9c353 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -375,6 +375,7 @@ impl Ircd { self.server.ping_links(); // keepalive on every server link self.server.purge_xlines(); // drop expired server bans self.server.purge_tbans(); // lift expired timed channel bans (TBAN) + self.server.prune_conn_history(); // connflood bookkeeping let now = crate::server::now(); let (to_ping, to_quit) = self.server.idle_check(now); for uid in to_ping { diff --git a/src/server.rs b/src/server.rs index 00fcd3c..7422184 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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, // 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>, // 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 * :*** ` line. pub(crate) fn notice_star(&self, uid: Uid, msg: &str) { self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));