connclass: cidr/parent/port/limit/globalmax + hashed/trusted-cert passwords, per-class recvq/sendq + fakelag, and rfc1413 ident
This commit is contained in:
parent
f5f888dbaa
commit
f371ed0a18
14 changed files with 842 additions and 126 deletions
|
|
@ -85,13 +85,46 @@ amu_target = both
|
||||||
# connflood = 5 10
|
# connflood = 5 10
|
||||||
|
|
||||||
# --- connectclass: per-class connection policy. Each line matches connecting
|
# --- connectclass: per-class connection policy. Each line matches connecting
|
||||||
# clients by IP glob (+ optional TLS); first match wins, else global limits.
|
# clients by IP/host mask (glob OR CIDR) and optional TLS/port; the first match
|
||||||
# Keys: allow=<ip glob>, deny=yes (reject), ssl=yes (TLS only), password=<pw>
|
# wins, else the global limits apply. Masks are tested against the IP at connect
|
||||||
# (client must PASS it), localmax=<n> (max connections per IP in this class),
|
# and re-tested against the resolved host at registration. Keys:
|
||||||
# maxchans=<n>, pingfreq=<secs>, timeout=<secs> (registration), modes=<+modes>.
|
# allow=<mask[,mask]> IP/host globs or CIDR (e.g. 10.0.0.0/8); any match hits
|
||||||
# connectclass = trusted allow=10.0.0.* maxchans=200 pingfreq=120
|
# deny=yes reject clients matching this class
|
||||||
# connectclass = vpn allow=* localmax=2 maxchans=20 modes=+ix
|
# parent=<name> inherit this class's other settings (not allow/deny)
|
||||||
# connectclass = banned allow=1.2.3.* deny=yes
|
# requiressl=yes|trusted require TLS; "trusted" also requires a client cert
|
||||||
|
# password=<pw> client must send it via PASS; may be hashed
|
||||||
|
# hash=<algo> names the hash of a hashed password (md5/sha256/…)
|
||||||
|
# port=<p[,p]> only clients that connected to these listener ports
|
||||||
|
# localmax=<n> max connections per IP in this class (local server)
|
||||||
|
# globalmax=<n> max connections per IP across the whole network
|
||||||
|
# limit=<n> max total local users in this class
|
||||||
|
# maxchans=<n> max channels a member may join
|
||||||
|
# pingfreq=<secs> ping frequency; timeout=<secs> registration timeout
|
||||||
|
# modes=<+modes> usermodes set on connect
|
||||||
|
# recvq=<bytes> receive-queue cap; hardsendq=<bytes> send-queue cap
|
||||||
|
# softsendq=<bytes> send-queue level above which reads pause (backpressure)
|
||||||
|
# fakelag=no disconnect flooders instead of rate-limiting them
|
||||||
|
# penaltythreshold=<n> flood message cap; commandrate=<secs> flood window
|
||||||
|
# useident=yes do an ident (RFC1413) lookup for this class
|
||||||
|
# requireident=yes refuse clients whose ident can't be confirmed
|
||||||
|
# resolvehostnames=no skip reverse-DNS for this class
|
||||||
|
# maxconnwarn=yes snotice opers when a limit refuses a client
|
||||||
|
# (recvq/hardsendq/softsendq apply to plaintext clients; TLS clients and links
|
||||||
|
# use the global max_line/max_sendq below.)
|
||||||
|
# connectclass = trusted allow=10.0.0.0/8 maxchans=200 pingfreq=120 fakelag=no
|
||||||
|
# connectclass = secure allow=* requiressl=yes password=sha256:<hex> hash=sha256
|
||||||
|
# connectclass = vpn allow=* parent=trusted localmax=2 maxchans=20 modes=+ix
|
||||||
|
# connectclass = banned allow=1.2.3.0/24 deny=yes
|
||||||
|
# connectclass_required = yes # refuse clients that match no allow class (default no)
|
||||||
|
|
||||||
|
# --- global connection limits (per-class recvq/hardsendq/softsendq override these) ---
|
||||||
|
# max_line = 16384 # max bytes in one line / receive queue (default 16 KiB)
|
||||||
|
# max_sendq = 1048576 # max queued output before a slow client is dropped (1 MiB)
|
||||||
|
|
||||||
|
# --- ident (RFC1413): off by default; a connection class can also enable it ---
|
||||||
|
# useident = yes # look up every client's ident (adds connect latency)
|
||||||
|
# requireident = yes # refuse clients whose ident can't be confirmed
|
||||||
|
# ident_timeout = 5 # seconds to wait for the ident reply
|
||||||
|
|
||||||
# --- security groups: securitygroup = <name> [criteria...]
|
# --- security groups: securitygroup = <name> [criteria...]
|
||||||
# criteria: public tls insecure account unregistered oper exclude-oper
|
# criteria: public tls insecure account unregistered oper exclude-oper
|
||||||
|
|
|
||||||
|
|
@ -853,7 +853,8 @@ impl Command for Connect {
|
||||||
}
|
}
|
||||||
let addr = format!("{}:{}", b.ip, b.port);
|
let addr = format!("{}:{}", b.ip, b.port);
|
||||||
let (tx, counter) = (s.event_tx.clone(), s.conn_counter.clone());
|
let (tx, counter) = (s.event_tx.clone(), s.conn_counter.clone());
|
||||||
std::thread::spawn(move || crate::socketengine::connect_link(&addr, tx, counter));
|
let max_line = s.conf_num("max_line", crate::socketengine::DEFAULT_MAX_LINE);
|
||||||
|
std::thread::spawn(move || crate::socketengine::connect_link(&addr, tx, counter, max_line));
|
||||||
let by = oper_nick(s, uid);
|
let by = oper_nick(s, uid);
|
||||||
s.snotice(&format!(
|
s.snotice(&format!(
|
||||||
"{by} used CONNECT to {} ({}:{})",
|
"{by} used CONNECT to {} ({}:{})",
|
||||||
|
|
|
||||||
23
src/ircd.rs
23
src/ircd.rs
|
|
@ -25,6 +25,7 @@ pub enum Event {
|
||||||
sock: Option<TcpStream>,
|
sock: Option<TcpStream>,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
certfp: Option<String>, // TLS client-cert fingerprint (clients only)
|
certfp: Option<String>, // TLS client-cert fingerprint (clients only)
|
||||||
|
local_port: u16, // the listener port the client connected to
|
||||||
link: bool, // a server-to-server connection, not a client
|
link: bool, // a server-to-server connection, not a client
|
||||||
outbound: bool, // (link) we dialed them
|
outbound: bool, // (link) we dialed them
|
||||||
websocket: bool, // arrived over the WebSocket transport
|
websocket: bool, // arrived over the WebSocket transport
|
||||||
|
|
@ -43,6 +44,12 @@ pub enum Event {
|
||||||
host: Option<String>,
|
host: Option<String>,
|
||||||
dnsbl: crate::modules::dnsbl::Outcome,
|
dnsbl: crate::modules::dnsbl::Outcome,
|
||||||
},
|
},
|
||||||
|
/// A client's ident (RFC 1413) lookup finished: the confirmed username, or
|
||||||
|
/// `None` if the host gave no valid response (see `crate::modules::ident`).
|
||||||
|
Ident {
|
||||||
|
uid: Uid,
|
||||||
|
ident: Option<String>,
|
||||||
|
},
|
||||||
/// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
|
/// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
|
||||||
/// so the core can route the reply back to the module that issued it (e.g.
|
/// so the core can route the reply back to the module that issued it (e.g.
|
||||||
/// account registration, captcha verification). `status` is 0 on transport
|
/// account registration, captcha verification). `status` is 0 on transport
|
||||||
|
|
@ -114,6 +121,7 @@ impl Ircd {
|
||||||
sock,
|
sock,
|
||||||
secure,
|
secure,
|
||||||
certfp,
|
certfp,
|
||||||
|
local_port,
|
||||||
link,
|
link,
|
||||||
outbound,
|
outbound,
|
||||||
websocket,
|
websocket,
|
||||||
|
|
@ -121,7 +129,8 @@ impl Ircd {
|
||||||
if link {
|
if link {
|
||||||
self.server.add_link(uid, addr, out, sock, outbound);
|
self.server.add_link(uid, addr, out, sock, outbound);
|
||||||
} else {
|
} else {
|
||||||
self.server.add_conn(uid, addr, out, sock, secure, certfp);
|
self.server
|
||||||
|
.add_conn(uid, addr, out, sock, secure, certfp, local_port);
|
||||||
if websocket {
|
if websocket {
|
||||||
if let Some(u) = self.server.users.get_mut(&uid) {
|
if let Some(u) = self.server.users.get_mut(&uid) {
|
||||||
u.flags.via_websocket = true;
|
u.flags.via_websocket = true;
|
||||||
|
|
@ -157,6 +166,10 @@ impl Ircd {
|
||||||
}
|
}
|
||||||
self.try_register(uid); // DNS may have been the last thing we waited on
|
self.try_register(uid); // DNS may have been the last thing we waited on
|
||||||
}
|
}
|
||||||
|
Event::Ident { uid, ident } => {
|
||||||
|
crate::modules::ident::on_result(&mut self.server, uid, ident);
|
||||||
|
self.try_register(uid); // ident may have been the last hold
|
||||||
|
}
|
||||||
Event::HttpResult {
|
Event::HttpResult {
|
||||||
uid,
|
uid,
|
||||||
tag,
|
tag,
|
||||||
|
|
@ -384,6 +397,7 @@ impl Ircd {
|
||||||
&& !u.ident.is_empty()
|
&& !u.ident.is_empty()
|
||||||
&& !u.cap
|
&& !u.cap
|
||||||
&& !u.dns_pending
|
&& !u.dns_pending
|
||||||
|
&& !u.ident_pending
|
||||||
&& u.waitpong.is_none()
|
&& u.waitpong.is_none()
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
@ -414,6 +428,13 @@ impl Ircd {
|
||||||
self.server.remove_user(uid, &reason);
|
self.server.remove_user(uid, &reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ident: apply a confirmed username (dropping `~`) and enforce requireident
|
||||||
|
if let Some(reason) = crate::modules::ident::finalize(&mut self.server, uid) {
|
||||||
|
self.server
|
||||||
|
.send(uid, format!("ERROR :Closing link: ({reason})"));
|
||||||
|
self.server.remove_user(uid, &reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// connectclass: verify the class password and apply its on-connect modes
|
// connectclass: verify the class password and apply its on-connect modes
|
||||||
if let Some(reason) = crate::modules::connclass::on_register(&mut self.server, uid) {
|
if let Some(reason) = crate::modules::connclass::on_register(&mut self.server, uid) {
|
||||||
self.server
|
self.server
|
||||||
|
|
|
||||||
39
src/link.rs
39
src/link.rs
|
|
@ -55,6 +55,7 @@ pub struct RemoteUser {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub realname: String,
|
pub realname: String,
|
||||||
pub account: Option<String>,
|
pub account: Option<String>,
|
||||||
|
pub ip: String, // client IP (for network-wide clone limits); "" if a peer omitted it
|
||||||
pub sid: String, // origin server id
|
pub sid: String, // origin server id
|
||||||
pub via: Uid, // local link uid it is reached through
|
pub via: Uid, // local link uid it is reached through
|
||||||
}
|
}
|
||||||
|
|
@ -313,13 +314,14 @@ impl Server {
|
||||||
fn uid_line(&self, u: &User) -> String {
|
fn uid_line(&self, u: &User) -> String {
|
||||||
let acct = u.account.clone().unwrap_or_else(|| "*".to_string());
|
let acct = u.account.clone().unwrap_or_else(|| "*".to_string());
|
||||||
format!(
|
format!(
|
||||||
":{} UID {} {} {} {} {} :{}",
|
":{} UID {} {} {} {} {} {} :{}",
|
||||||
self.sid,
|
self.sid,
|
||||||
u.uuid,
|
u.uuid,
|
||||||
u.nick,
|
u.nick,
|
||||||
u.ident,
|
u.ident,
|
||||||
u.host_display(),
|
u.host_display(),
|
||||||
acct,
|
acct,
|
||||||
|
u.addr.ip(),
|
||||||
u.realname
|
u.realname
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -674,10 +676,22 @@ impl Server {
|
||||||
// --- inbound S2S records --------------------------------------------------
|
// --- inbound S2S records --------------------------------------------------
|
||||||
|
|
||||||
fn link_uid_recv(&mut self, via: Uid, msg: &Message) {
|
fn link_uid_recv(&mut self, via: Uid, msg: &Message) {
|
||||||
// :<sid> UID <uuid> <nick> <ident> <host> <account> :<realname>
|
// :<sid> UID <uuid> <nick> <ident> <host> <account> <ip> :<realname>
|
||||||
|
// The <ip> field is newer; tolerate the older 6-param form (no IP).
|
||||||
if msg.params.len() < 6 {
|
if msg.params.len() < 6 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let has_ip = msg.params.len() >= 7;
|
||||||
|
let ip = if has_ip {
|
||||||
|
msg.params[5].clone()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let realname = if has_ip {
|
||||||
|
msg.params[6].clone()
|
||||||
|
} else {
|
||||||
|
msg.params[5].clone()
|
||||||
|
};
|
||||||
let sid = msg.source.clone().unwrap_or_default();
|
let sid = msg.source.clone().unwrap_or_default();
|
||||||
let uuid = msg.params[0].clone();
|
let uuid = msg.params[0].clone();
|
||||||
let nick = msg.params[1].clone();
|
let nick = msg.params[1].clone();
|
||||||
|
|
@ -705,13 +719,27 @@ impl Server {
|
||||||
nick,
|
nick,
|
||||||
ident: msg.params[2].clone(),
|
ident: msg.params[2].clone(),
|
||||||
host: msg.params[3].clone(),
|
host: msg.params[3].clone(),
|
||||||
realname: msg.params[5].clone(),
|
realname,
|
||||||
account,
|
account,
|
||||||
|
ip: ip.clone(),
|
||||||
sid: sid.clone(),
|
sid: sid.clone(),
|
||||||
via,
|
via,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let line = format!(
|
// re-propagate to our other peers, carrying the IP when we have one
|
||||||
|
let line = if has_ip {
|
||||||
|
format!(
|
||||||
|
":{sid} UID {} {} {} {} {} {} :{}",
|
||||||
|
msg.params[0],
|
||||||
|
msg.params[1],
|
||||||
|
msg.params[2],
|
||||||
|
msg.params[3],
|
||||||
|
msg.params[4],
|
||||||
|
ip,
|
||||||
|
msg.params[6]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
":{sid} UID {} {} {} {} {} :{}",
|
":{sid} UID {} {} {} {} {} :{}",
|
||||||
msg.params[0],
|
msg.params[0],
|
||||||
msg.params[1],
|
msg.params[1],
|
||||||
|
|
@ -719,7 +747,8 @@ impl Server {
|
||||||
msg.params[3],
|
msg.params[3],
|
||||||
msg.params[4],
|
msg.params[4],
|
||||||
msg.params[5]
|
msg.params[5]
|
||||||
);
|
)
|
||||||
|
};
|
||||||
self.propagate(&line, Some(via));
|
self.propagate(&line, Some(via));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
22
src/main.rs
22
src/main.rs
|
|
@ -44,6 +44,17 @@ fn main() {
|
||||||
cfg.servername
|
cfg.servername
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// global queue limits (per-class overrides layer on top of these in the reactor)
|
||||||
|
let raw_num = |k: &str, d: usize| {
|
||||||
|
cfg.raw
|
||||||
|
.get(k)
|
||||||
|
.and_then(|v| v.first())
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(d)
|
||||||
|
};
|
||||||
|
let max_line = raw_num("max_line", socketengine::DEFAULT_MAX_LINE);
|
||||||
|
let max_sendq = raw_num("max_sendq", socketengine::DEFAULT_MAX_SENDQ);
|
||||||
|
|
||||||
// one uid counter shared by every listener (and by CONNECT) so ids stay unique
|
// one uid counter shared by every listener (and by CONNECT) so ids stay unique
|
||||||
let counter = Arc::new(AtomicU64::new(1));
|
let counter = Arc::new(AtomicU64::new(1));
|
||||||
|
|
||||||
|
|
@ -79,6 +90,7 @@ fn main() {
|
||||||
Some(backend),
|
Some(backend),
|
||||||
tls_counter,
|
tls_counter,
|
||||||
false,
|
false,
|
||||||
|
max_line,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +107,9 @@ fn main() {
|
||||||
eprintln!("echoircd S2S link listener on {bind_srv} (sid {})", cfg.sid);
|
eprintln!("echoircd S2S link listener on {bind_srv} (sid {})", cfg.sid);
|
||||||
let s_tx = tx.clone();
|
let s_tx = tx.clone();
|
||||||
let s_counter = counter.clone();
|
let s_counter = counter.clone();
|
||||||
thread::spawn(move || socketengine::accept_loop(sl, s_tx, None, s_counter, true));
|
thread::spawn(move || {
|
||||||
|
socketengine::accept_loop(sl, s_tx, None, s_counter, true, max_line)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("echoircd: cannot bind server port {bind_srv}: {e}"),
|
Err(e) => eprintln!("echoircd: cannot bind server port {bind_srv}: {e}"),
|
||||||
}
|
}
|
||||||
|
|
@ -114,11 +128,13 @@ fn main() {
|
||||||
let u_counter = counter.clone();
|
let u_counter = counter.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
thread::sleep(std::time::Duration::from_secs(2));
|
thread::sleep(std::time::Duration::from_secs(2));
|
||||||
socketengine::connect_link(&addr, u_tx, u_counter);
|
socketengine::connect_link(&addr, u_tx, u_counter, max_line);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// client plaintext connections: one mio reactor thread drives them all
|
// client plaintext connections: one mio reactor thread drives them all
|
||||||
thread::spawn(move || socketengine::run_reactor(client_listener, tx, counter));
|
thread::spawn(move || {
|
||||||
|
socketengine::run_reactor(client_listener, tx, counter, max_line, max_sendq)
|
||||||
|
});
|
||||||
let _ = core.join();
|
let _ = core.join();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,123 +1,390 @@
|
||||||
//! connclass — connection classes. Each `connectclass` config line matches
|
//! connclass — connection classes. Each `connectclass` config line matches
|
||||||
//! connecting clients by IP glob (and optionally TLS), then applies per-class
|
//! connecting clients by IP/host mask (CIDR or glob) and optional TLS/port, then
|
||||||
//! policy: reject (deny), a per-IP connection cap, a password, usermodes on
|
//! applies per-class policy: reject (deny), per-IP and per-class connection caps, a
|
||||||
//! connect, and overrides for max channels / ping frequency / registration
|
//! password, on-connect usermodes, queue/flood limits, and overrides for max
|
||||||
//! timeout. Config, one line per class (first token = name, rest key=value):
|
//! channels / ping frequency / registration timeout. One line per class — the first
|
||||||
|
//! token is the name, the rest are `key=value`:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! connectclass = <name> allow=<ip glob> [deny=yes] [ssl=yes] [password=<pw>]
|
//! connectclass = <name> allow=<mask[,mask]> [parent=<name>] [deny=yes]
|
||||||
//! [localmax=<n>] [maxchans=<n>] [pingfreq=<secs>] [timeout=<secs>] [modes=<+modes>]
|
//! [requiressl=yes|trusted] [password=<pw>] [hash=<algo>] [port=<p[,p]>]
|
||||||
|
//! [localmax=<n>] [globalmax=<n>] [limit=<n>] [maxchans=<n>] [pingfreq=<secs>]
|
||||||
|
//! [timeout=<secs>] [modes=<+modes>] [recvq=<bytes>] [hardsendq=<bytes>]
|
||||||
|
//! [softsendq=<bytes>] [fakelag=yes|no] [penaltythreshold=<n>] [commandrate=<secs>]
|
||||||
|
//! [useident=yes] [requireident=yes] [resolvehostnames=no] [maxconnwarn=yes]
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The first class whose `allow` glob (and `ssl` if given) matches a client is
|
//! The first class whose masks (and TLS/port conditions) match a client is assigned.
|
||||||
//! assigned at connect. With no class, the global limits apply. Matching is against
|
//! Masks are tested against the IP at connect and re-tested against the resolved
|
||||||
//! the IP (the host isn't resolved yet at connect).
|
//! host at registration, so host masks work once rDNS returns. With no class the
|
||||||
|
//! global limits apply; set `connectclass_required = yes` to refuse clients that
|
||||||
|
//! match no allow class.
|
||||||
|
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone)]
|
||||||
pub struct ConnClass {
|
pub struct ConnClass {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub allow: String,
|
pub allow: Vec<String>, // IP/host masks (glob or CIDR); any match = match
|
||||||
pub deny: bool,
|
pub deny: bool, // deny class: matching clients are refused
|
||||||
pub ssl: bool,
|
pub ssl: bool, // require TLS
|
||||||
pub password: Option<String>,
|
pub ssl_trusted: bool, // require a TLS client certificate (requiressl=trusted)
|
||||||
pub localmax: Option<usize>,
|
pub password: Option<String>, // PASS credential (plain or hashed; verify auto-detects)
|
||||||
|
pub ports: Vec<u16>, // restrict to these listener ports (empty = any)
|
||||||
|
pub localmax: Option<usize>, // max local connections per IP in this class
|
||||||
|
pub globalmax: Option<usize>, // max network-wide connections per IP
|
||||||
|
pub limit: Option<usize>, // max total local users in this class
|
||||||
pub maxchans: Option<usize>,
|
pub maxchans: Option<usize>,
|
||||||
pub pingfreq: Option<u64>,
|
pub pingfreq: Option<u64>,
|
||||||
pub timeout: Option<u64>,
|
pub timeout: Option<u64>, // registration timeout
|
||||||
pub modes: Option<String>,
|
pub modes: Option<String>, // usermodes set on connect
|
||||||
|
pub recvq: Option<usize>, // per-conn receive-queue byte cap
|
||||||
|
pub hardsendq: Option<usize>, // send-queue byte cap → disconnect
|
||||||
|
pub softsendq: Option<usize>, // send-queue byte cap → pause reading (backpressure)
|
||||||
|
pub penaltythreshold: Option<usize>, // flood message cap override (see modules::flood)
|
||||||
|
pub commandrate: Option<u64>, // flood window override, seconds
|
||||||
|
pub fakelag: bool, // apply flood limiting (default); false = kill on flood
|
||||||
|
pub useident: bool, // do an ident (RFC1413) lookup for this class
|
||||||
|
pub requireident: bool, // refuse if the ident lookup fails
|
||||||
|
pub resolvehostnames: bool, // resolve rDNS for this class (default yes)
|
||||||
|
pub maxconnwarn: bool, // snotice opers when a limit refuses a client
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(line: &str) -> Option<ConnClass> {
|
/// Split a `key=value` value on commas into non-empty pieces.
|
||||||
let mut it = line.split_whitespace();
|
fn list(v: &str) -> impl Iterator<Item = &str> {
|
||||||
let mut c = ConnClass {
|
v.split(',').map(str::trim).filter(|s| !s.is_empty())
|
||||||
name: it.next()?.to_string(),
|
}
|
||||||
allow: "*".to_string(),
|
|
||||||
..Default::default()
|
/// Apply one `key=value` token to `c`.
|
||||||
};
|
fn apply(c: &mut ConnClass, k: &str, v: &str) {
|
||||||
for tok in it {
|
|
||||||
let Some((k, v)) = tok.split_once('=') else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
match k {
|
match k {
|
||||||
"allow" => c.allow = v.to_string(),
|
"allow" => c.allow.extend(list(v).map(str::to_string)),
|
||||||
"deny" => c.deny = v.eq_ignore_ascii_case("yes"),
|
"deny" => c.deny = v.eq_ignore_ascii_case("yes"),
|
||||||
"ssl" | "requiressl" => c.ssl = v.eq_ignore_ascii_case("yes"),
|
"ssl" | "requiressl" => {
|
||||||
|
c.ssl = !v.eq_ignore_ascii_case("no") && !v.is_empty();
|
||||||
|
c.ssl_trusted = v.eq_ignore_ascii_case("trusted");
|
||||||
|
}
|
||||||
"password" | "pass" => c.password = Some(v.to_string()),
|
"password" | "pass" => c.password = Some(v.to_string()),
|
||||||
|
"hash" => {
|
||||||
|
// name the algorithm of a hashed password: fold it into the stored
|
||||||
|
// credential (`<algo>:<digest>`) that verify() auto-detects, unless the
|
||||||
|
// password value already carries its own prefix.
|
||||||
|
if let Some(pw) = c.password.take() {
|
||||||
|
c.password = Some(if pw.contains(':') {
|
||||||
|
pw
|
||||||
|
} else {
|
||||||
|
format!("{v}:{pw}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"port" => c.ports.extend(list(v).filter_map(|p| p.parse::<u16>().ok())),
|
||||||
"localmax" => c.localmax = v.parse().ok(),
|
"localmax" => c.localmax = v.parse().ok(),
|
||||||
|
"globalmax" => c.globalmax = v.parse().ok(),
|
||||||
|
"limit" => c.limit = v.parse().ok(),
|
||||||
"maxchans" => c.maxchans = v.parse().ok(),
|
"maxchans" => c.maxchans = v.parse().ok(),
|
||||||
"pingfreq" => c.pingfreq = v.parse().ok(),
|
"pingfreq" => c.pingfreq = v.parse().ok(),
|
||||||
"timeout" => c.timeout = v.parse().ok(),
|
"timeout" => c.timeout = v.parse().ok(),
|
||||||
"modes" => c.modes = Some(v.to_string()),
|
"modes" => c.modes = Some(v.to_string()),
|
||||||
|
"recvq" => c.recvq = v.parse().ok(),
|
||||||
|
"hardsendq" => c.hardsendq = v.parse().ok(),
|
||||||
|
"softsendq" => c.softsendq = v.parse().ok(),
|
||||||
|
"penaltythreshold" => c.penaltythreshold = v.parse().ok(),
|
||||||
|
"commandrate" => c.commandrate = v.parse().ok(),
|
||||||
|
"fakelag" => c.fakelag = !v.eq_ignore_ascii_case("no"),
|
||||||
|
"useident" => c.useident = v.eq_ignore_ascii_case("yes"),
|
||||||
|
"requireident" => c.requireident = v.eq_ignore_ascii_case("yes"),
|
||||||
|
"resolvehostnames" => c.resolvehostnames = !v.eq_ignore_ascii_case("no"),
|
||||||
|
"maxconnwarn" => c.maxconnwarn = v.eq_ignore_ascii_case("yes"),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw `connectclass` line whose first token is `name`.
|
||||||
|
fn raw_line(s: &Server, name: &str) -> Option<String> {
|
||||||
|
s.conf_all("connectclass")
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.split_whitespace().next() == Some(name))
|
||||||
|
.map(|l| l.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The effective token list for `name` with `parent=` inheritance applied: a
|
||||||
|
/// parent's tokens come first (so the child overrides), minus the block-defining
|
||||||
|
/// `allow`/`deny`/`parent` keys, which stay class-local. Bounded against cycles.
|
||||||
|
fn tokens_for(s: &Server, name: &str, depth: u8) -> Option<Vec<String>> {
|
||||||
|
let line = raw_line(s, name)?;
|
||||||
|
let own: Vec<String> = line.split_whitespace().skip(1).map(str::to_string).collect();
|
||||||
|
let parent = own
|
||||||
|
.iter()
|
||||||
|
.find_map(|t| t.strip_prefix("parent="))
|
||||||
|
.map(str::to_string);
|
||||||
|
let mut merged = Vec::new();
|
||||||
|
if let Some(p) = parent {
|
||||||
|
if depth < 8 {
|
||||||
|
if let Some(pt) = tokens_for(s, &p, depth + 1) {
|
||||||
|
merged.extend(pt.into_iter().filter(|t| {
|
||||||
|
!t.starts_with("allow=")
|
||||||
|
&& !t.starts_with("deny=")
|
||||||
|
&& !t.starts_with("parent=")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.extend(own);
|
||||||
|
Some(merged)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a resolved class (parent inheritance applied) from its config line.
|
||||||
|
fn build(s: &Server, name: &str) -> Option<ConnClass> {
|
||||||
|
let toks = tokens_for(s, name, 0)?;
|
||||||
|
let mut c = ConnClass {
|
||||||
|
name: name.to_string(),
|
||||||
|
fakelag: true,
|
||||||
|
resolvehostnames: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for tok in toks {
|
||||||
|
if let Some((k, v)) = tok.split_once('=') {
|
||||||
|
apply(&mut c, k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.allow.is_empty() {
|
||||||
|
c.allow.push("*".to_string()); // an unqualified class matches everyone
|
||||||
}
|
}
|
||||||
Some(c)
|
Some(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every configured class, resolved.
|
||||||
pub fn all(s: &Server) -> Vec<ConnClass> {
|
pub fn all(s: &Server) -> Vec<ConnClass> {
|
||||||
s.conf_all("connectclass")
|
s.conf_all("connectclass")
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|l| parse(l))
|
.filter_map(|l| l.split_whitespace().next())
|
||||||
|
.filter_map(|name| build(s, name))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single resolved class by name.
|
||||||
pub fn named(s: &Server, name: &str) -> Option<ConnClass> {
|
pub fn named(s: &Server, name: &str) -> Option<ConnClass> {
|
||||||
all(s).into_iter().find(|c| c.name == name)
|
build(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- mask matching -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// Whether the first `bits` bits of `a` and `b` are equal.
|
||||||
|
fn prefix_eq(a: &[u8], b: &[u8], bits: u8) -> bool {
|
||||||
|
let full = (bits / 8) as usize;
|
||||||
|
if a[..full] != b[..full] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let rem = bits % 8;
|
||||||
|
if rem == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mask = 0xffu8 << (8 - rem);
|
||||||
|
(a[full] & mask) == (b[full] & mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `target` falls inside the CIDR `base`/`bits` (same family required).
|
||||||
|
fn cidr_contains(base: IpAddr, bits: u8, target: IpAddr) -> bool {
|
||||||
|
match (base, target) {
|
||||||
|
(IpAddr::V4(b), IpAddr::V4(t)) => prefix_eq(&b.octets(), &t.octets(), bits.min(32)),
|
||||||
|
(IpAddr::V6(b), IpAddr::V6(t)) => prefix_eq(&b.octets(), &t.octets(), bits.min(128)),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match one mask against a client's IP and (once known) resolved host. A mask with
|
||||||
|
/// a `/` is a CIDR range tested against the IP; otherwise it's a glob tested against
|
||||||
|
/// both the IP text and the host.
|
||||||
|
fn mask_match(mask: &str, ip: &str, host: &str) -> bool {
|
||||||
|
if let Some((net, bits)) = mask.split_once('/') {
|
||||||
|
if let (Ok(base), Ok(bits), Ok(target)) =
|
||||||
|
(net.parse::<IpAddr>(), bits.parse::<u8>(), ip.parse::<IpAddr>())
|
||||||
|
{
|
||||||
|
return cidr_contains(base, bits, target);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
glob_match(mask, ip) || (!host.is_empty() && glob_match(mask, host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- class selection ---------------------------------------------------------
|
||||||
|
|
||||||
|
enum Pick {
|
||||||
|
Class(ConnClass),
|
||||||
|
Deny(String),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Choose the first suitable class for a client. Suitability = a matching mask plus
|
||||||
|
/// any TLS/port/limit conditions; an unsuitable class is skipped, a matching deny
|
||||||
|
/// class rejects. `host` is empty at connect (pre-rDNS) and the resolved name later.
|
||||||
|
fn pick(
|
||||||
|
s: &Server,
|
||||||
|
uid: Uid,
|
||||||
|
ip: &str,
|
||||||
|
host: &str,
|
||||||
|
secure: bool,
|
||||||
|
has_cert: bool,
|
||||||
|
port: u16,
|
||||||
|
) -> Pick {
|
||||||
|
for c in all(s) {
|
||||||
|
if !c.allow.iter().any(|m| mask_match(m, ip, host)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c.ssl && !secure {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c.ssl_trusted && !has_cert {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !c.ports.is_empty() && !c.ports.contains(&port) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c.deny {
|
||||||
|
return Pick::Deny(c.name);
|
||||||
|
}
|
||||||
|
if let Some(max) = c.limit {
|
||||||
|
if class_count(s, &c.name, uid) >= max {
|
||||||
|
if c.maxconnwarn {
|
||||||
|
s.snotice(&format!("connect class {} is full ({max})", c.name));
|
||||||
|
}
|
||||||
|
continue; // full — try the next class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pick::Class(c);
|
||||||
|
}
|
||||||
|
Pick::None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local users currently in class `name` (excluding `uid`).
|
||||||
|
fn class_count(s: &Server, name: &str, uid: Uid) -> usize {
|
||||||
|
s.users
|
||||||
|
.iter()
|
||||||
|
.filter(|(&k, u)| k != uid && u.class.as_deref() == Some(name))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local connections from `ip` in class `name` (excluding `uid`).
|
||||||
|
fn local_clones(s: &Server, ip: &str, name: &str, uid: Uid) -> usize {
|
||||||
|
s.users
|
||||||
|
.iter()
|
||||||
|
.filter(|(&k, u)| {
|
||||||
|
k != uid && u.addr.ip().to_string() == ip && u.class.as_deref() == Some(name)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connections from `ip` across the whole network (local + remote), excluding `uid`.
|
||||||
|
fn global_clones(s: &Server, ip: &str, uid: Uid) -> usize {
|
||||||
|
let local = s
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.filter(|(&k, u)| k != uid && u.addr.ip().to_string() == ip)
|
||||||
|
.count();
|
||||||
|
let remote = s.remote_users.values().filter(|ru| ru.ip == ip).count();
|
||||||
|
local + remote
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assign the connecting client to the first matching class. Returns `Some(reason)`
|
/// Assign the connecting client to the first matching class. Returns `Some(reason)`
|
||||||
/// if the connection must be rejected (a deny class or a per-IP cap); otherwise sets
|
/// if the connection must be rejected (a deny class or a per-IP/per-class cap);
|
||||||
/// the class name on the user and returns `None`. Called from `add_conn`.
|
/// otherwise sets the class on the user and returns `None`. Called from `add_conn`.
|
||||||
pub fn assign(s: &mut Server, uid: Uid) -> Option<String> {
|
pub fn assign(s: &mut Server, uid: Uid) -> Option<String> {
|
||||||
let (ip, secure) = {
|
let (ip, secure, has_cert, port) = {
|
||||||
let u = s.users.get(&uid)?;
|
let u = s.users.get(&uid)?;
|
||||||
(u.addr.ip().to_string(), u.secure)
|
(
|
||||||
|
u.addr.ip().to_string(),
|
||||||
|
u.secure,
|
||||||
|
u.certfp.is_some(),
|
||||||
|
u.port,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let class = all(s)
|
let class = match pick(s, uid, &ip, "", secure, has_cert, port) {
|
||||||
.into_iter()
|
Pick::Deny(name) => {
|
||||||
.find(|c| glob_match(&c.allow, &ip) && (!c.ssl || secure))?;
|
return Some(format!("Connection class {name} denies your address"));
|
||||||
if class.deny {
|
|
||||||
return Some(format!("Connection class {} denies your address", class.name));
|
|
||||||
}
|
}
|
||||||
|
Pick::None => {
|
||||||
|
if !all(s).iter().any(|c| !c.deny) || !s.conf_bool("connectclass_required", false) {
|
||||||
|
return None; // no allow classes, or strict mode off: allow, no class
|
||||||
|
}
|
||||||
|
return Some("You are not allowed to connect to this server".to_string());
|
||||||
|
}
|
||||||
|
Pick::Class(c) => c,
|
||||||
|
};
|
||||||
|
let warn = |s: &Server, why: &str| {
|
||||||
|
if class.maxconnwarn {
|
||||||
|
s.snotice(&format!("connect class {} refused {ip}: {why}", class.name));
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Some(max) = class.localmax {
|
if let Some(max) = class.localmax {
|
||||||
let n = s
|
if local_clones(s, &ip, &class.name, uid) >= max {
|
||||||
.users
|
warn(s, "local clone limit");
|
||||||
.values()
|
|
||||||
.filter(|u| {
|
|
||||||
u.addr.ip().to_string() == ip && u.class.as_deref() == Some(class.name.as_str())
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
if n >= max {
|
|
||||||
return Some("Too many connections from your address".to_string());
|
return Some("Too many connections from your address".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(max) = class.globalmax {
|
||||||
|
if global_clones(s, &ip, uid) >= max {
|
||||||
|
warn(s, "global clone limit");
|
||||||
|
return Some("Too many global connections from your address".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(u) = s.users.get_mut(&uid) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
u.class = Some(class.name);
|
u.class = Some(class.name);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// At registration: verify the class password (if any) and apply the class's
|
/// At registration: re-pick the class now the host is resolved (host masks), verify
|
||||||
/// on-connect usermodes. Returns `Some(reason)` to reject.
|
/// the class password, enforce a required client cert, and apply on-connect modes.
|
||||||
|
/// Returns `Some(reason)` to reject.
|
||||||
pub fn on_register(s: &mut Server, uid: Uid) -> Option<String> {
|
pub fn on_register(s: &mut Server, uid: Uid) -> Option<String> {
|
||||||
|
let (ip, host, secure, has_cert, port, sent) = {
|
||||||
|
let u = s.users.get(&uid)?;
|
||||||
|
(
|
||||||
|
u.addr.ip().to_string(),
|
||||||
|
u.host.clone(),
|
||||||
|
u.secure,
|
||||||
|
u.certfp.is_some(),
|
||||||
|
u.port,
|
||||||
|
u.pass.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
match pick(s, uid, &ip, &host, secure, has_cert, port) {
|
||||||
|
Pick::Deny(name) => {
|
||||||
|
return Some(format!("Connection class {name} denies your address"));
|
||||||
|
}
|
||||||
|
Pick::Class(c) => {
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.class = Some(c.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pick::None => {} // keep whatever was assigned at connect
|
||||||
|
}
|
||||||
let name = s.users.get(&uid)?.class.clone()?;
|
let name = s.users.get(&uid)?.class.clone()?;
|
||||||
let class = named(s, &name)?;
|
let class = named(s, &name)?;
|
||||||
if let Some(pw) = &class.password {
|
if let Some(pw) = &class.password {
|
||||||
let ok = s.users.get(&uid).and_then(|u| u.pass.clone());
|
let ok = sent
|
||||||
if ok.as_deref() != Some(pw.as_str()) {
|
.as_deref()
|
||||||
|
.map(|p| crate::modules::password_hash::verify(pw, p))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !ok {
|
||||||
return Some("Password mismatch for your connection class".to_string());
|
return Some("Password mismatch for your connection class".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if class.ssl_trusted && !has_cert {
|
||||||
|
return Some("Your connection class requires a client certificate".to_string());
|
||||||
|
}
|
||||||
if let Some(m) = class.modes {
|
if let Some(m) = class.modes {
|
||||||
crate::coremods::core_mode::svs_set_user_modes(s, uid, &m);
|
crate::coremods::core_mode::svs_set_user_modes(s, uid, &m);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- per-class getters consulted by the core / other modules -----------------
|
||||||
|
|
||||||
fn class_of(s: &Server, uid: Uid) -> Option<ConnClass> {
|
fn class_of(s: &Server, uid: Uid) -> Option<ConnClass> {
|
||||||
let name = s.users.get(&uid).and_then(|u| u.class.clone())?;
|
let name = s.users.get(&uid).and_then(|u| u.class.clone())?;
|
||||||
named(s, &name)
|
named(s, &name)
|
||||||
|
|
@ -132,3 +399,57 @@ pub fn reg_timeout(s: &Server, uid: Uid) -> Option<u64> {
|
||||||
pub fn max_chans(s: &Server, uid: Uid) -> Option<usize> {
|
pub fn max_chans(s: &Server, uid: Uid) -> Option<usize> {
|
||||||
class_of(s, uid)?.maxchans
|
class_of(s, uid)?.maxchans
|
||||||
}
|
}
|
||||||
|
pub fn recvq(s: &Server, uid: Uid) -> Option<usize> {
|
||||||
|
class_of(s, uid)?.recvq
|
||||||
|
}
|
||||||
|
pub fn hardsendq(s: &Server, uid: Uid) -> Option<usize> {
|
||||||
|
class_of(s, uid)?.hardsendq
|
||||||
|
}
|
||||||
|
pub fn softsendq(s: &Server, uid: Uid) -> Option<usize> {
|
||||||
|
class_of(s, uid)?.softsendq
|
||||||
|
}
|
||||||
|
/// Per-class flood override: `(message cap, window secs, fakelag)`. `fakelag=false`
|
||||||
|
/// means flooders are killed rather than rate-limited.
|
||||||
|
pub fn flood_over(s: &Server, uid: Uid) -> Option<(Option<usize>, Option<u64>, bool)> {
|
||||||
|
let c = class_of(s, uid)?;
|
||||||
|
Some((c.penaltythreshold, c.commandrate, c.fakelag))
|
||||||
|
}
|
||||||
|
/// Whether reverse-DNS should be resolved for this client's class (default yes).
|
||||||
|
pub fn resolve_hostnames(s: &Server, uid: Uid) -> bool {
|
||||||
|
class_of(s, uid).map(|c| c.resolvehostnames).unwrap_or(true)
|
||||||
|
}
|
||||||
|
/// `(useident, requireident)` for this client's class.
|
||||||
|
pub fn ident_policy(s: &Server, uid: Uid) -> (bool, bool) {
|
||||||
|
class_of(s, uid)
|
||||||
|
.map(|c| (c.useident, c.requireident))
|
||||||
|
.unwrap_or((false, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cidr_v4_ranges() {
|
||||||
|
let base = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0));
|
||||||
|
assert!(cidr_contains(base, 8, "10.9.9.9".parse().unwrap()));
|
||||||
|
assert!(!cidr_contains(base, 8, "11.0.0.1".parse().unwrap()));
|
||||||
|
let net = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0));
|
||||||
|
assert!(cidr_contains(net, 24, "192.168.1.200".parse().unwrap()));
|
||||||
|
assert!(!cidr_contains(net, 24, "192.168.2.1".parse().unwrap()));
|
||||||
|
// a /32 is an exact host
|
||||||
|
let host = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5));
|
||||||
|
assert!(cidr_contains(host, 32, "203.0.113.5".parse().unwrap()));
|
||||||
|
assert!(!cidr_contains(host, 32, "203.0.113.6".parse().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mask_glob_and_cidr() {
|
||||||
|
assert!(mask_match("10.0.0.0/8", "10.1.2.3", ""));
|
||||||
|
assert!(!mask_match("10.0.0.0/8", "192.0.2.1", ""));
|
||||||
|
assert!(mask_match("*.example.com", "192.0.2.1", "host.example.com"));
|
||||||
|
assert!(mask_match("192.0.2.*", "192.0.2.7", ""));
|
||||||
|
assert!(!mask_match("nomatch/33", "1.2.3.4", "")); // unparseable → no match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
//! Per-user message-rate limit (`flood_messages` within `flood_seconds`); opers
|
//! Per-user message-rate limit (`flood_messages` within `flood_seconds`); opers
|
||||||
//! exempt. Recent message times live in the user's typed
|
//! exempt. A connection class can override the limit (`penaltythreshold` /
|
||||||
|
//! `commandrate`) and, with `fakelag=no`, have flooders disconnected instead of
|
||||||
|
//! rate-limited. Recent message times live in the user's typed
|
||||||
//! [`crate::extensible::Extensible`] slot, so the state is freed when the user quits.
|
//! [`crate::extensible::Extensible`] slot, so the state is freed when the user quits.
|
||||||
|
|
||||||
|
use crate::modules::connclass;
|
||||||
use crate::module::{ModResult, Module};
|
use crate::module::{ModResult, Module};
|
||||||
use crate::server::{now, Server};
|
use crate::server::{now, Server};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
@ -32,8 +35,11 @@ impl Module for Flood {
|
||||||
_text: &str,
|
_text: &str,
|
||||||
) -> ModResult {
|
) -> ModResult {
|
||||||
let now = now();
|
let now = now();
|
||||||
let max = srv.conf_num("flood_messages", FLOOD_MAX);
|
// a connection class may raise the limit and/or opt out of fake lag
|
||||||
let window = srv.conf_num("flood_seconds", FLOOD_WINDOW);
|
let (cls_max, cls_window, fakelag) =
|
||||||
|
connclass::flood_over(srv, uid).unwrap_or((None, None, true));
|
||||||
|
let max = cls_max.unwrap_or_else(|| srv.conf_num("flood_messages", FLOOD_MAX));
|
||||||
|
let window = cls_window.unwrap_or_else(|| srv.conf_num("flood_seconds", FLOOD_WINDOW));
|
||||||
let (over, warn) = {
|
let (over, warn) = {
|
||||||
let Some(u) = srv.users.get_mut(&uid) else {
|
let Some(u) = srv.users.get_mut(&uid) else {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
|
|
@ -50,6 +56,12 @@ impl Module for Flood {
|
||||||
(over, warn)
|
(over, warn)
|
||||||
};
|
};
|
||||||
if over {
|
if over {
|
||||||
|
if !fakelag {
|
||||||
|
// fakelag disabled: disconnect the flooder instead of throttling
|
||||||
|
srv.send(uid, "ERROR :Closing link: (Excess flood)".to_string());
|
||||||
|
srv.mark_quit(uid, "Excess flood".to_string());
|
||||||
|
return ModResult::Deny;
|
||||||
|
}
|
||||||
if warn {
|
if warn {
|
||||||
let nick = srv
|
let nick = srv
|
||||||
.users
|
.users
|
||||||
|
|
|
||||||
161
src/modules/ident.rs
Normal file
161
src/modules/ident.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
//! ident — optional RFC 1413 (Ident) lookups. When a connection class (or the
|
||||||
|
//! global `useident = yes`) asks for one, we ask the client's host (port 113) who
|
||||||
|
//! owns the connection; a confirmed reply becomes the visible username without the
|
||||||
|
//! `~` that marks an unverified ident. `requireident` refuses clients whose ident
|
||||||
|
//! can't be confirmed. The lookup runs on a short-lived worker thread (like the
|
||||||
|
//! resolver) and reports back as `Event::Ident`, so the core never blocks.
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::{IpAddr, SocketAddr, TcpStream};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::ircd::Event;
|
||||||
|
use crate::modules::connclass;
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::users::ident_of;
|
||||||
|
use crate::Uid;
|
||||||
|
|
||||||
|
/// Default per-lookup timeout; overridable with `ident_timeout` (seconds).
|
||||||
|
const IDENT_TIMEOUT: u64 = 5;
|
||||||
|
/// Cap on concurrent lookups so a connection flood can't spawn unbounded threads.
|
||||||
|
const MAX_ACTIVE: usize = 256;
|
||||||
|
static ACTIVE: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
/// Per-user lookup result stored on `User.ext`: `Some(name)` confirmed, `None` not.
|
||||||
|
struct IdentResult(Option<String>);
|
||||||
|
|
||||||
|
/// `(useident, requireident)` combining the client's class with the global default.
|
||||||
|
fn policy(s: &Server, uid: Uid) -> (bool, bool) {
|
||||||
|
let (cu, cr) = connclass::ident_policy(s, uid);
|
||||||
|
(
|
||||||
|
cu || s.conf_bool("useident", false),
|
||||||
|
cr || s.conf_bool("requireident", false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start an ident lookup for a freshly-connected client if its class (or the global
|
||||||
|
/// config) wants one. Sets `ident_pending` to hold registration until the reply
|
||||||
|
/// arrives. Returns whether a lookup was started.
|
||||||
|
pub fn dispatch(s: &mut Server, uid: Uid) -> bool {
|
||||||
|
let (useident, requireident) = policy(s, uid);
|
||||||
|
if !useident && !requireident {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let (ip, their_port, our_port) = match s.users.get(&uid) {
|
||||||
|
Some(u) => (u.addr.ip(), u.addr.port(), u.port),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
if ACTIVE.fetch_add(1, Ordering::Relaxed) >= MAX_ACTIVE {
|
||||||
|
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
return false; // too many in flight: skip (treated as no ident)
|
||||||
|
}
|
||||||
|
let timeout = Duration::from_secs(s.conf_num("ident_timeout", IDENT_TIMEOUT));
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.ident_pending = true;
|
||||||
|
}
|
||||||
|
s.notice_star(uid, "Checking Ident");
|
||||||
|
let tx = s.event_tx.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let ident = lookup(ip, their_port, our_port, timeout);
|
||||||
|
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
let _ = tx.send(Event::Ident { uid, ident });
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The blocking RFC 1413 exchange: connect to `<ip>:113`, ask about the connection
|
||||||
|
/// pair, and return the confirmed username (unsanitised) or `None`.
|
||||||
|
fn lookup(ip: IpAddr, their_port: u16, our_port: u16, timeout: Duration) -> Option<String> {
|
||||||
|
let mut stream = TcpStream::connect_timeout(&SocketAddr::new(ip, 113), timeout).ok()?;
|
||||||
|
stream.set_read_timeout(Some(timeout)).ok()?;
|
||||||
|
stream.set_write_timeout(Some(timeout)).ok()?;
|
||||||
|
// the query is "<port on their side>, <port on our side>"
|
||||||
|
let query = format!("{their_port}, {our_port}\r\n");
|
||||||
|
stream.write_all(query.as_bytes()).ok()?;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut chunk = [0u8; 256];
|
||||||
|
loop {
|
||||||
|
match stream.read(&mut chunk) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
buf.extend_from_slice(&chunk[..n]);
|
||||||
|
if buf.len() > 512 || buf.contains(&b'\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parse_reply(&String::from_utf8_lossy(&buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an ident reply: `<port>,<port> : USERID : <opsys> : <username>`.
|
||||||
|
fn parse_reply(reply: &str) -> Option<String> {
|
||||||
|
let fields: Vec<&str> = reply.split(':').collect();
|
||||||
|
if fields.len() < 4 || !fields[1].trim().eq_ignore_ascii_case("USERID") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = fields[3].trim();
|
||||||
|
(!name.is_empty()).then(|| name.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A lookup finished: stash the result and clear the registration hold. The result
|
||||||
|
/// is applied to the visible ident at registration (so a late USER can't clobber it).
|
||||||
|
pub fn on_result(s: &mut Server, uid: Uid, ident: Option<String>) {
|
||||||
|
match &ident {
|
||||||
|
Some(name) => s.notice_star(uid, &format!("Received Ident response: {name}")),
|
||||||
|
None => s.notice_star(uid, "No Ident response"),
|
||||||
|
}
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
*u.ext.get_or_insert_with(|| IdentResult(None)) = IdentResult(ident);
|
||||||
|
u.ident_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// At registration, apply a confirmed ident (dropping the leading `~`) and enforce
|
||||||
|
/// `requireident`. Returns `Some(reason)` to reject.
|
||||||
|
pub fn finalize(s: &mut Server, uid: Uid) -> Option<String> {
|
||||||
|
let confirmed = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.and_then(|u| u.ext.get::<IdentResult>().map(|r| r.0.clone()));
|
||||||
|
if let Some(Some(name)) = confirmed {
|
||||||
|
let clean = ident_of(&name); // sanitised, no `~`
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.ident = clean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (_, requireident) = policy(s, uid);
|
||||||
|
if requireident {
|
||||||
|
let unverified = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.ident.is_empty() || u.ident.starts_with('~'))
|
||||||
|
.unwrap_or(true);
|
||||||
|
if unverified {
|
||||||
|
return Some("Your connection class requires a valid ident response".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_userid_and_error() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_reply("6193, 6667 : USERID : UNIX : reverse\r\n").as_deref(),
|
||||||
|
Some("reverse")
|
||||||
|
);
|
||||||
|
assert_eq!(parse_reply("6193, 6667 : ERROR : NO-USER"), None);
|
||||||
|
assert_eq!(parse_reply("garbage"), None);
|
||||||
|
// case-insensitive reply type, trailing charset field tolerated
|
||||||
|
assert_eq!(
|
||||||
|
parse_reply("1,2 : userid : UNIX,US-ASCII : bob").as_deref(),
|
||||||
|
Some("bob")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ pub mod hashident;
|
||||||
pub mod hidelist;
|
pub mod hidelist;
|
||||||
pub mod hidemode;
|
pub mod hidemode;
|
||||||
pub mod hidewhois;
|
pub mod hidewhois;
|
||||||
|
pub mod ident;
|
||||||
pub mod irccloudtags;
|
pub mod irccloudtags;
|
||||||
pub mod jsonlog;
|
pub mod jsonlog;
|
||||||
pub mod jwt;
|
pub mod jwt;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,10 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
|
||||||
}
|
}
|
||||||
let addr = format!("{}:{}", b.ip, b.port);
|
let addr = format!("{}:{}", b.ip, b.port);
|
||||||
let (tx, counter) = (s.event_tx.clone(), s.conn_counter.clone());
|
let (tx, counter) = (s.event_tx.clone(), s.conn_counter.clone());
|
||||||
std::thread::spawn(move || crate::socketengine::connect_link(&addr, tx, counter));
|
let max_line = s.conf_num("max_line", crate::socketengine::DEFAULT_MAX_LINE);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
crate::socketengine::connect_link(&addr, tx, counter, max_line)
|
||||||
|
});
|
||||||
s.snotice(&format!("RPC initiated a link to {}", b.name));
|
s.snotice(&format!("RPC initiated a link to {}", b.name));
|
||||||
Ok(obj(&[("result", "true".into())]))
|
Ok(obj(&[("result", "true".into())]))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ impl Server {
|
||||||
sock: Option<TcpStream>,
|
sock: Option<TcpStream>,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
certfp: Option<String>,
|
certfp: Option<String>,
|
||||||
|
local_port: u16,
|
||||||
) {
|
) {
|
||||||
let uuid = self.next_uuid();
|
let uuid = self.next_uuid();
|
||||||
self.uuid_local.insert(uuid.clone(), uid);
|
self.uuid_local.insert(uuid.clone(), uid);
|
||||||
|
|
@ -314,8 +315,10 @@ impl Server {
|
||||||
account: None,
|
account: None,
|
||||||
signon: now(),
|
signon: now(),
|
||||||
addr,
|
addr,
|
||||||
|
port: local_port,
|
||||||
registered: false,
|
registered: false,
|
||||||
dns_pending: false,
|
dns_pending: false,
|
||||||
|
ident_pending: false,
|
||||||
waitpong: None,
|
waitpong: None,
|
||||||
class: None,
|
class: None,
|
||||||
pass: None,
|
pass: None,
|
||||||
|
|
@ -358,13 +361,24 @@ impl Server {
|
||||||
self.remove_user(uid, &reason);
|
self.remove_user(uid, &reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// push any per-class queue caps (recvq/hardsendq/softsendq) to the reactor
|
||||||
|
let caps = (
|
||||||
|
crate::modules::connclass::recvq(self, uid),
|
||||||
|
crate::modules::connclass::hardsendq(self, uid),
|
||||||
|
crate::modules::connclass::softsendq(self, uid),
|
||||||
|
);
|
||||||
|
if caps.0.is_some() || caps.1.is_some() || caps.2.is_some() {
|
||||||
|
if let Some(u) = self.users.get(&uid) {
|
||||||
|
u.out.set_limits(caps.0, caps.1, caps.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-registration connection notices. The ident-113 notices are cosmetic
|
// ident: optionally ask the client's host who owns the connection (only when
|
||||||
// (ident is archaic and firewalled); the hostname lookup is real (see
|
// the class or global config wants it — see modules::ident). Holds
|
||||||
// `resolver`) and its result arrives later as an Event.
|
// registration via ident_pending until the reply arrives.
|
||||||
self.notice_star(uid, "Checking Ident");
|
crate::modules::ident::dispatch(self, uid);
|
||||||
self.notice_star(uid, "No Ident response");
|
// a connection class may opt out of reverse-DNS (resolvehostnames=no)
|
||||||
let do_rdns = self.resolve_hosts;
|
let do_rdns = self.resolve_hosts && crate::modules::connclass::resolve_hostnames(self, uid);
|
||||||
let zones = self.dnsbl_zones.clone(); // DNSBL runs if any zones are configured
|
let zones = self.dnsbl_zones.clone(); // DNSBL runs if any zones are configured
|
||||||
if do_rdns {
|
if do_rdns {
|
||||||
self.notice_star(uid, "Looking up your hostname...");
|
self.notice_star(uid, "Looking up your hostname...");
|
||||||
|
|
@ -1057,8 +1071,10 @@ mod tests {
|
||||||
account: None,
|
account: None,
|
||||||
signon: 0,
|
signon: 0,
|
||||||
addr: "127.0.0.1:1".parse().unwrap(),
|
addr: "127.0.0.1:1".parse().unwrap(),
|
||||||
|
port: 6667,
|
||||||
registered: true,
|
registered: true,
|
||||||
dns_pending: false,
|
dns_pending: false,
|
||||||
|
ident_pending: false,
|
||||||
waitpong: None,
|
waitpong: None,
|
||||||
class: None,
|
class: None,
|
||||||
pass: None,
|
pass: None,
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,28 @@ use crate::ircd::Event;
|
||||||
use crate::tls::TlsBackend;
|
use crate::tls::TlsBackend;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Longest single line we'll buffer before dropping it (crude flood guard).
|
/// Default recvq: longest single line we'll buffer before dropping it. Overridable
|
||||||
const MAX_LINE: usize = 16 * 1024;
|
/// globally (`max_line`) and per connection class (`recvq`).
|
||||||
/// Most bytes we'll queue to a slow client before dropping them (backpressure).
|
pub const DEFAULT_MAX_LINE: usize = 16 * 1024;
|
||||||
const MAX_WBUF: usize = 1 << 20; // 1 MiB
|
/// Default hardsendq: most bytes we'll queue to a slow client before dropping them
|
||||||
|
/// and closing. Overridable globally (`max_sendq`) and per class (`hardsendq`).
|
||||||
|
pub const DEFAULT_MAX_SENDQ: usize = 1 << 20; // 1 MiB
|
||||||
/// How long a TLS thread blocks on a read before draining its write queue.
|
/// How long a TLS thread blocks on a read before draining its write queue.
|
||||||
const TLS_POLL: Duration = Duration::from_millis(100);
|
const TLS_POLL: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
/// A queued output action the core hands the reactor: a line to write to a
|
/// A queued output action the core hands the reactor: a line to write to a
|
||||||
/// connection, or a request to flush-then-close it (sent when the core drops the
|
/// connection, a request to flush-then-close it (sent when the core drops the
|
||||||
/// [`OutSink`], e.g. on quit).
|
/// [`OutSink`], e.g. on quit), or a per-connection queue-limit override (from the
|
||||||
|
/// assigned connection class).
|
||||||
pub enum Out {
|
pub enum Out {
|
||||||
Line(usize, String),
|
Line(usize, String),
|
||||||
Close(usize),
|
Close(usize),
|
||||||
|
Limits {
|
||||||
|
token: usize,
|
||||||
|
recvq: Option<usize>,
|
||||||
|
hardsendq: Option<usize>,
|
||||||
|
softsendq: Option<usize>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The core's handle to one connection's output. Thread-model connections (TLS,
|
/// The core's handle to one connection's output. Thread-model connections (TLS,
|
||||||
|
|
@ -68,6 +77,30 @@ impl OutSink {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override this connection's queue limits (from its connection class). Only the
|
||||||
|
/// reactor (plaintext client) model honours these; thread-model connections (TLS,
|
||||||
|
/// links) use the global defaults.
|
||||||
|
pub fn set_limits(
|
||||||
|
&self,
|
||||||
|
recvq: Option<usize>,
|
||||||
|
hardsendq: Option<usize>,
|
||||||
|
softsendq: Option<usize>,
|
||||||
|
) {
|
||||||
|
if let OutSink::Reactor { token, tx, waker } = self {
|
||||||
|
if tx
|
||||||
|
.send(Out::Limits {
|
||||||
|
token: *token,
|
||||||
|
recvq,
|
||||||
|
hardsendq,
|
||||||
|
softsendq,
|
||||||
|
})
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let _ = waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for OutSink {
|
impl Drop for OutSink {
|
||||||
|
|
@ -94,8 +127,13 @@ struct Conn {
|
||||||
rbuf: Vec<u8>, // bytes read, awaiting a newline
|
rbuf: Vec<u8>, // bytes read, awaiting a newline
|
||||||
wbuf: Vec<u8>, // bytes queued to write
|
wbuf: Vec<u8>, // bytes queued to write
|
||||||
wpos: usize, // how far into wbuf we've written
|
wpos: usize, // how far into wbuf we've written
|
||||||
|
want_read: bool,
|
||||||
want_write: bool,
|
want_write: bool,
|
||||||
closing: bool, // flush wbuf, then close
|
closing: bool, // flush wbuf, then close
|
||||||
|
paused: bool, // reads paused (softsendq backpressure); ⟺ pending > softsendq
|
||||||
|
recvq: usize, // max buffered unterminated-line bytes before dropping
|
||||||
|
hardsendq: usize, // max queued output bytes before dropping + closing
|
||||||
|
softsendq: usize, // queued output above this pauses reads until it drains
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Conn {
|
impl Conn {
|
||||||
|
|
@ -104,9 +142,35 @@ impl Conn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reregister `t`'s epoll interest to match its current read/write intent, but only
|
||||||
|
/// if it changed. A paused connection drops READABLE (so the client stops being
|
||||||
|
/// serviced) while keeping WRITABLE to drain the backlog that paused it.
|
||||||
|
fn set_interest(poll: &mut Poll, c: &mut Conn, t: usize) {
|
||||||
|
let want_read = !c.paused;
|
||||||
|
let want_write = !c.wbuf.is_empty() || c.paused;
|
||||||
|
if want_read == c.want_read && want_write == c.want_write {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.want_read = want_read;
|
||||||
|
c.want_write = want_write;
|
||||||
|
let interest = match (want_read, want_write) {
|
||||||
|
(true, true) => Interest::READABLE | Interest::WRITABLE,
|
||||||
|
(false, true) => Interest::WRITABLE,
|
||||||
|
// never both-false (paused ⟹ backlog ⟹ want_write); READABLE is a safe floor
|
||||||
|
_ => Interest::READABLE,
|
||||||
|
};
|
||||||
|
let _ = poll.registry().reregister(&mut c.stream, Token(t), interest);
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the client plaintext reactor on this thread. `listener` is an already-bound
|
/// Run the client plaintext reactor on this thread. `listener` is an already-bound
|
||||||
/// mio listener (bound in `main` so a bind failure is fatal and fails fast).
|
/// mio listener (bound in `main` so a bind failure is fatal and fails fast).
|
||||||
pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<AtomicU64>) {
|
pub fn run_reactor(
|
||||||
|
mut listener: MioListener,
|
||||||
|
core: Sender<Event>,
|
||||||
|
counter: Arc<AtomicU64>,
|
||||||
|
max_line: usize,
|
||||||
|
max_sendq: usize,
|
||||||
|
) {
|
||||||
let mut poll = match Poll::new() {
|
let mut poll = match Poll::new() {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -158,6 +222,7 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
||||||
let addr = stream
|
let addr = stream
|
||||||
.peer_addr()
|
.peer_addr()
|
||||||
.unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap());
|
.unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap());
|
||||||
|
let local_port = stream.local_addr().map(|a| a.port()).unwrap_or(0);
|
||||||
conns.insert(
|
conns.insert(
|
||||||
token,
|
token,
|
||||||
Conn {
|
Conn {
|
||||||
|
|
@ -166,8 +231,13 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
||||||
rbuf: Vec::new(),
|
rbuf: Vec::new(),
|
||||||
wbuf: Vec::new(),
|
wbuf: Vec::new(),
|
||||||
wpos: 0,
|
wpos: 0,
|
||||||
|
want_read: true,
|
||||||
want_write: false,
|
want_write: false,
|
||||||
closing: false,
|
closing: false,
|
||||||
|
paused: false,
|
||||||
|
recvq: max_line,
|
||||||
|
hardsendq: max_sendq,
|
||||||
|
softsendq: max_sendq,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let out = OutSink::Reactor {
|
let out = OutSink::Reactor {
|
||||||
|
|
@ -183,6 +253,7 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
||||||
sock: None,
|
sock: None,
|
||||||
secure: false,
|
secure: false,
|
||||||
certfp: None,
|
certfp: None,
|
||||||
|
local_port,
|
||||||
link: false,
|
link: false,
|
||||||
outbound: false,
|
outbound: false,
|
||||||
websocket: false,
|
websocket: false,
|
||||||
|
|
@ -203,8 +274,8 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
||||||
match msg {
|
match msg {
|
||||||
Out::Line(t, line) => {
|
Out::Line(t, line) => {
|
||||||
if let Some(c) = conns.get_mut(&t) {
|
if let Some(c) = conns.get_mut(&t) {
|
||||||
if c.pending() + line.len() + 2 > MAX_WBUF {
|
if c.pending() + line.len() + 2 > c.hardsendq {
|
||||||
// slow client: drop queued data and close
|
// hardsendq: drop queued data and close
|
||||||
c.wbuf.clear();
|
c.wbuf.clear();
|
||||||
c.wpos = 0;
|
c.wpos = 0;
|
||||||
c.closing = true;
|
c.closing = true;
|
||||||
|
|
@ -215,6 +286,11 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
||||||
}
|
}
|
||||||
c.wbuf.extend_from_slice(line.as_bytes());
|
c.wbuf.extend_from_slice(line.as_bytes());
|
||||||
c.wbuf.extend_from_slice(b"\r\n");
|
c.wbuf.extend_from_slice(b"\r\n");
|
||||||
|
// softsendq: over the soft cap, stop reading
|
||||||
|
// their commands until the backlog drains
|
||||||
|
if c.pending() > c.softsendq {
|
||||||
|
c.paused = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
touched.insert(t);
|
touched.insert(t);
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +301,24 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
||||||
touched.insert(t);
|
touched.insert(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Out::Limits {
|
||||||
|
token,
|
||||||
|
recvq,
|
||||||
|
hardsendq,
|
||||||
|
softsendq,
|
||||||
|
} => {
|
||||||
|
if let Some(c) = conns.get_mut(&token) {
|
||||||
|
if let Some(v) = recvq {
|
||||||
|
c.recvq = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = hardsendq {
|
||||||
|
c.hardsendq = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = softsendq {
|
||||||
|
c.softsendq = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for t in touched {
|
for t in touched {
|
||||||
|
|
@ -267,7 +361,7 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
lines.push((c.uid, l.to_string()));
|
lines.push((c.uid, l.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.rbuf.len() > MAX_LINE {
|
if c.rbuf.len() > c.recvq {
|
||||||
c.rbuf.clear(); // overlong line with no newline: drop it
|
c.rbuf.clear(); // overlong line with no newline: drop it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,10 +384,13 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write as much of `t`'s queued output as the socket accepts, adjust WRITABLE
|
/// Write as much of `t`'s queued output as the socket accepts, adjust epoll
|
||||||
/// interest, and close once a `closing` connection's buffer is drained.
|
/// interest, and close once a `closing` connection's buffer is drained. If the
|
||||||
|
/// backlog dropped back under softsendq, un-pause reads and catch up (edge-triggered:
|
||||||
|
/// data that arrived while paused won't re-fire, so read it here).
|
||||||
fn flush_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core: &Sender<Event>) {
|
fn flush_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core: &Sender<Event>) {
|
||||||
let mut close = false;
|
let mut close = false;
|
||||||
|
let mut unpaused = false;
|
||||||
if let Some(c) = conns.get_mut(&t) {
|
if let Some(c) = conns.get_mut(&t) {
|
||||||
while c.wpos < c.wbuf.len() {
|
while c.wpos < c.wbuf.len() {
|
||||||
match c.stream.write(&c.wbuf[c.wpos..]) {
|
match c.stream.write(&c.wbuf[c.wpos..]) {
|
||||||
|
|
@ -311,25 +408,19 @@ fn flush_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
c.wbuf.clear();
|
c.wbuf.clear();
|
||||||
c.wpos = 0;
|
c.wpos = 0;
|
||||||
}
|
}
|
||||||
// re-arm WRITABLE only while there's a backlog (edge-triggered)
|
if c.paused && c.pending() <= c.softsendq {
|
||||||
let want = !c.wbuf.is_empty();
|
c.paused = false;
|
||||||
if want != c.want_write {
|
unpaused = true;
|
||||||
c.want_write = want;
|
|
||||||
let interest = if want {
|
|
||||||
Interest::READABLE | Interest::WRITABLE
|
|
||||||
} else {
|
|
||||||
Interest::READABLE
|
|
||||||
};
|
|
||||||
let _ = poll
|
|
||||||
.registry()
|
|
||||||
.reregister(&mut c.stream, Token(t), interest);
|
|
||||||
}
|
}
|
||||||
|
set_interest(poll, c, t);
|
||||||
if c.closing && c.wbuf.is_empty() {
|
if c.closing && c.wbuf.is_empty() {
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if close {
|
if close {
|
||||||
close_conn(poll, conns, t, core);
|
close_conn(poll, conns, t, core);
|
||||||
|
} else if unpaused {
|
||||||
|
read_conn(poll, conns, t, core); // catch reads missed while paused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,6 +445,7 @@ pub fn accept_loop(
|
||||||
tls: Option<Arc<dyn TlsBackend>>,
|
tls: Option<Arc<dyn TlsBackend>>,
|
||||||
counter: Arc<AtomicU64>,
|
counter: Arc<AtomicU64>,
|
||||||
link: bool,
|
link: bool,
|
||||||
|
max_line: usize,
|
||||||
) {
|
) {
|
||||||
for conn in listener.incoming() {
|
for conn in listener.incoming() {
|
||||||
let Ok(stream) = conn else { continue };
|
let Ok(stream) = conn else { continue };
|
||||||
|
|
@ -361,6 +453,7 @@ pub fn accept_loop(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let _ = stream.set_nodelay(true);
|
let _ = stream.set_nodelay(true);
|
||||||
|
let local_port = stream.local_addr().map(|a| a.port()).unwrap_or(0);
|
||||||
let uid = counter.fetch_add(1, Ordering::Relaxed);
|
let uid = counter.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
match &tls {
|
match &tls {
|
||||||
|
|
@ -381,6 +474,7 @@ pub fn accept_loop(
|
||||||
sock: Some(shutdown),
|
sock: Some(shutdown),
|
||||||
secure: false,
|
secure: false,
|
||||||
certfp: None,
|
certfp: None,
|
||||||
|
local_port,
|
||||||
link,
|
link,
|
||||||
outbound: false,
|
outbound: false,
|
||||||
websocket: false,
|
websocket: false,
|
||||||
|
|
@ -390,12 +484,12 @@ pub fn accept_loop(
|
||||||
break; // core gone
|
break; // core gone
|
||||||
}
|
}
|
||||||
let core_tx = core.clone();
|
let core_tx = core.clone();
|
||||||
thread::spawn(move || reader_loop(reader, uid, core_tx));
|
thread::spawn(move || reader_loop(reader, uid, core_tx, max_line));
|
||||||
}
|
}
|
||||||
Some(backend) => {
|
Some(backend) => {
|
||||||
let backend = backend.clone();
|
let backend = backend.clone();
|
||||||
let core_tx = core.clone();
|
let core_tx = core.clone();
|
||||||
thread::spawn(move || tls_conn(backend, stream, uid, addr, core_tx, link));
|
thread::spawn(move || tls_conn(backend, stream, uid, addr, core_tx, link, max_line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +497,7 @@ pub fn accept_loop(
|
||||||
|
|
||||||
/// Dial an outbound server link and wire it to the core (an `outbound` link that
|
/// Dial an outbound server link and wire it to the core (an `outbound` link that
|
||||||
/// introduces itself first). Used for auto-connecting to a configured uplink.
|
/// introduces itself first). Used for auto-connecting to a configured uplink.
|
||||||
pub fn connect_link(addr: &str, core: Sender<Event>, counter: Arc<AtomicU64>) {
|
pub fn connect_link(addr: &str, core: Sender<Event>, counter: Arc<AtomicU64>, max_line: usize) {
|
||||||
let stream = match TcpStream::connect(addr) {
|
let stream = match TcpStream::connect(addr) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -430,6 +524,7 @@ pub fn connect_link(addr: &str, core: Sender<Event>, counter: Arc<AtomicU64>) {
|
||||||
sock: Some(shutdown),
|
sock: Some(shutdown),
|
||||||
secure: false,
|
secure: false,
|
||||||
certfp: None,
|
certfp: None,
|
||||||
|
local_port: 0,
|
||||||
link: true,
|
link: true,
|
||||||
outbound: true,
|
outbound: true,
|
||||||
websocket: false,
|
websocket: false,
|
||||||
|
|
@ -438,12 +533,12 @@ pub fn connect_link(addr: &str, core: Sender<Event>, counter: Arc<AtomicU64>) {
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
thread::spawn(move || reader_loop(reader, uid, core));
|
thread::spawn(move || reader_loop(reader, uid, core, max_line));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- plaintext link: two blocking threads -----------------------------------
|
// --- plaintext link: two blocking threads -----------------------------------
|
||||||
|
|
||||||
fn reader_loop(stream: TcpStream, uid: Uid, core: Sender<Event>) {
|
fn reader_loop(stream: TcpStream, uid: Uid, core: Sender<Event>, max_line: usize) {
|
||||||
let mut buf = BufReader::new(stream);
|
let mut buf = BufReader::new(stream);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -451,7 +546,7 @@ fn reader_loop(stream: TcpStream, uid: Uid, core: Sender<Event>) {
|
||||||
match buf.read_line(&mut line) {
|
match buf.read_line(&mut line) {
|
||||||
Ok(0) => break, // EOF
|
Ok(0) => break, // EOF
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if line.len() > MAX_LINE {
|
if line.len() > max_line {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let l = line.trim_end_matches(['\r', '\n']);
|
let l = line.trim_end_matches(['\r', '\n']);
|
||||||
|
|
@ -493,11 +588,13 @@ fn tls_conn(
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
core: Sender<Event>,
|
core: Sender<Event>,
|
||||||
link: bool,
|
link: bool,
|
||||||
|
max_line: usize,
|
||||||
) {
|
) {
|
||||||
// Keep a raw handle so the core can force the socket shut later.
|
// Keep a raw handle so the core can force the socket shut later.
|
||||||
let Ok(shutdown) = stream.try_clone() else {
|
let Ok(shutdown) = stream.try_clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let local_port = stream.local_addr().map(|a| a.port()).unwrap_or(0);
|
||||||
let mut conn = match backend.accept(stream) {
|
let mut conn = match backend.accept(stream) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
@ -515,6 +612,7 @@ fn tls_conn(
|
||||||
sock: Some(shutdown),
|
sock: Some(shutdown),
|
||||||
secure: true,
|
secure: true,
|
||||||
certfp,
|
certfp,
|
||||||
|
local_port,
|
||||||
link,
|
link,
|
||||||
outbound: false,
|
outbound: false,
|
||||||
websocket: false,
|
websocket: false,
|
||||||
|
|
@ -548,7 +646,7 @@ fn tls_conn(
|
||||||
break 'io;
|
break 'io;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if acc.len() > MAX_LINE {
|
if acc.len() > max_line {
|
||||||
acc.clear(); // overlong line with no newline: drop it
|
acc.clear(); // overlong line with no newline: drop it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,8 +290,10 @@ pub struct User {
|
||||||
pub account: Option<String>, // logged-in account name (set by services)
|
pub account: Option<String>, // logged-in account name (set by services)
|
||||||
pub signon: u64, // unix secs at registration (WHOIS 317)
|
pub signon: u64, // unix secs at registration (WHOIS 317)
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
|
pub port: u16, // listener port the client connected to (connectclass port=, ident)
|
||||||
pub registered: bool,
|
pub registered: bool,
|
||||||
pub dns_pending: bool, // holding registration for a reverse-DNS lookup
|
pub dns_pending: bool, // holding registration for a reverse-DNS lookup
|
||||||
|
pub ident_pending: bool, // holding registration for an ident (RFC1413) lookup
|
||||||
pub waitpong: Option<String>, // conn_waitpong: cookie the client must PONG before registering
|
pub waitpong: Option<String>, // conn_waitpong: cookie the client must PONG before registering
|
||||||
pub class: Option<String>, // connectclass: assigned connection class name
|
pub class: Option<String>, // connectclass: assigned connection class name
|
||||||
pub pass: Option<String>, // password sent via PASS (for connectclass passwords)
|
pub pass: Option<String>, // password sent via PASS (for connectclass passwords)
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ fn ws_session<S: WsStream>(
|
||||||
.unwrap_or(addr);
|
.unwrap_or(addr);
|
||||||
let secure = tls_secure || hs.secure;
|
let secure = tls_secure || hs.secure;
|
||||||
let send_opcode = if hs.binary { OP_BIN } else { OP_TEXT };
|
let send_opcode = if hs.binary { OP_BIN } else { OP_TEXT };
|
||||||
|
let local_port = shutdown.local_addr().map(|a| a.port()).unwrap_or(0);
|
||||||
|
|
||||||
let (out_tx, out_rx) = std::sync::mpsc::channel::<String>();
|
let (out_tx, out_rx) = std::sync::mpsc::channel::<String>();
|
||||||
if core
|
if core
|
||||||
|
|
@ -251,6 +252,7 @@ fn ws_session<S: WsStream>(
|
||||||
sock: Some(shutdown),
|
sock: Some(shutdown),
|
||||||
secure,
|
secure,
|
||||||
certfp: None,
|
certfp: None,
|
||||||
|
local_port,
|
||||||
link: false,
|
link: false,
|
||||||
outbound: false,
|
outbound: false,
|
||||||
websocket: true,
|
websocket: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue