connflood: refuse connection floods from a single IP
This commit is contained in:
parent
1eca745092
commit
d4ce5c008f
4 changed files with 49 additions and 1 deletions
|
|
@ -80,3 +80,6 @@ amu_target = both
|
||||||
# --- command aliases: /NS ... -> PRIVMSG <target> :... (services shortcuts) ---
|
# --- command aliases: /NS ... -> PRIVMSG <target> :... (services shortcuts) ---
|
||||||
# alias = NS NickServ
|
# alias = NS NickServ
|
||||||
# alias = CS ChanServ
|
# alias = CS ChanServ
|
||||||
|
|
||||||
|
# --- connflood: refuse >max connections per <secs> from a single IP ---
|
||||||
|
# connflood = 5 10
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ pub struct Config {
|
||||||
pub opermotd: Vec<String>, // OPERMOTD text, one line per entry
|
pub opermotd: Vec<String>, // OPERMOTD text, one line per entry
|
||||||
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
|
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
|
||||||
pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick)
|
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 {
|
impl Default for Config {
|
||||||
|
|
@ -110,6 +111,7 @@ impl Default for Config {
|
||||||
opermotd: Vec::new(),
|
opermotd: Vec::new(),
|
||||||
vhosts: Vec::new(),
|
vhosts: Vec::new(),
|
||||||
aliases: Vec::new(),
|
aliases: Vec::new(),
|
||||||
|
connflood: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +270,17 @@ impl Config {
|
||||||
.push((name.to_ascii_uppercase(), target.to_string()));
|
.push((name.to_ascii_uppercase(), target.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"connflood" => {
|
||||||
|
// connflood = <max> <secs> — 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::<u32>(), sc.parse::<u64>()) {
|
||||||
|
if mx > 0 && sc > 0 {
|
||||||
|
c.connflood = Some((mx, sc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,7 @@ impl Ircd {
|
||||||
self.server.ping_links(); // keepalive on every server link
|
self.server.ping_links(); // keepalive on every server link
|
||||||
self.server.purge_xlines(); // drop expired server bans
|
self.server.purge_xlines(); // drop expired server bans
|
||||||
self.server.purge_tbans(); // lift expired timed channel bans (TBAN)
|
self.server.purge_tbans(); // lift expired timed channel bans (TBAN)
|
||||||
|
self.server.prune_conn_history(); // connflood bookkeeping
|
||||||
let now = crate::server::now();
|
let now = crate::server::now();
|
||||||
let (to_ping, to_quit) = self.server.idle_check(now);
|
let (to_ping, to_quit) = self.server.idle_check(now);
|
||||||
for uid in to_ping {
|
for uid in to_ping {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
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::atomic::AtomicU64;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -135,6 +135,8 @@ pub struct Server {
|
||||||
pub opermotd: Vec<String>, // OPERMOTD text
|
pub opermotd: Vec<String>, // OPERMOTD text
|
||||||
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
|
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
|
||||||
pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick)
|
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
|
// 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
|
// 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
|
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
|
||||||
|
|
@ -189,6 +191,8 @@ impl Server {
|
||||||
opermotd: cfg.opermotd,
|
opermotd: cfg.opermotd,
|
||||||
vhosts: cfg.vhosts,
|
vhosts: cfg.vhosts,
|
||||||
aliases: cfg.aliases,
|
aliases: cfg.aliases,
|
||||||
|
connflood: cfg.connflood,
|
||||||
|
conn_history: HashMap::new(),
|
||||||
label_capture: RefCell::new(None),
|
label_capture: RefCell::new(None),
|
||||||
event_tx,
|
event_tx,
|
||||||
conn_counter,
|
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
|
// Pre-registration connection notices, InspIRCd / solanum style. Ident-113
|
||||||
// is archaic and firewalled, so those two are cosmetic; the hostname lookup
|
// is archaic and firewalled, so those two are cosmetic; the hostname lookup
|
||||||
// is real (see `resolver`) — its result arrives later as an Event.
|
// 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.
|
/// A pre-registration `:server NOTICE * :*** <msg>` line.
|
||||||
pub(crate) fn notice_star(&self, uid: Uid, msg: &str) {
|
pub(crate) fn notice_star(&self, uid: Uid, msg: &str) {
|
||||||
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));
|
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue