config: make flood/dnsbl/multiline/chathistory/extjwt limits + nick/chan/watch/monitor/silence/whowas/timeouts configurable (no hardcoded options)

This commit is contained in:
Jean Chevronnet 2026-08-09 22:52:28 +00:00
parent 1969218d3f
commit 61584a545b
13 changed files with 141 additions and 66 deletions

View file

@ -12,9 +12,15 @@ use crate::command::{CmdResult, Command};
use crate::server::{iso_time, now, parse_iso, Server};
use crate::Uid;
/// Recent messages CHATHISTORY keeps per conversation.
/// Default number of messages CHATHISTORY keeps per conversation, if
/// `chathistory_limit` is unset. Also the ceiling a client can request.
pub const HISTORY_CAP: usize = 256;
/// The configured per-conversation history size (overridable via `chathistory_limit`).
pub fn limit(s: &Server) -> usize {
s.conf_num("chathistory_limit", HISTORY_CAP).clamp(1, 100_000)
}
/// One stored message, replayed by CHATHISTORY / the `+H` backlog.
pub struct HistMsg {
pub ts: u64,
@ -40,6 +46,7 @@ pub fn record(
text: &str,
msgid: &str,
) {
let cap = limit(s);
let buf = s
.ext
.get_or_insert_with::<History>(History::default)
@ -54,7 +61,7 @@ pub fn record(
target: target.to_string(),
text: text.to_string(),
});
while buf.len() > HISTORY_CAP {
while buf.len() > cap {
buf.pop_front();
}
}
@ -105,7 +112,7 @@ impl Command for ChatHistory {
.get(3)
.and_then(|l| l.parse::<usize>().ok())
.unwrap_or(50)
.clamp(1, HISTORY_CAP);
.clamp(1, limit(s));
let me = s
.users
.get(&uid)
@ -186,7 +193,7 @@ impl Command for ChatHistory {
let limit = limit_s
.and_then(|l| l.parse::<usize>().ok())
.unwrap_or(50)
.clamp(1, HISTORY_CAP);
.clamp(1, limit(s));
let bref = s.next_msgid().replace('-', "");
let mut lines: Vec<String> = Vec::new();

View file

@ -20,7 +20,7 @@ use crate::xline::XKind;
use crate::Uid;
/// Ban length applied by the `*line` actions on a hit.
const DNSBL_BAN: u64 = 86_400; // 1 day
const DNSBL_BAN: u64 = 86_400; // default ban length (1 day) if `dnsbl_duration` unset
/// Outcome of a DNSBL check for one connecting client.
pub enum Outcome {
@ -84,22 +84,11 @@ fn act(s: &mut Server, uid: Uid, zone: &str, reply: Ipv4Addr) {
));
let reason = format!("{} (listed on {zone})", s.dnsbl_reason);
let ipstr = ip.to_string();
let dur = s.conf_num("dnsbl_duration", DNSBL_BAN);
match action.as_str() {
"kline" => s.add_xline(
XKind::Kline,
&format!("*@{ipstr}"),
DNSBL_BAN,
"dnsbl",
&reason,
),
"gline" => s.add_xline(
XKind::Gline,
&format!("*@{ipstr}"),
DNSBL_BAN,
"dnsbl",
&reason,
),
"zline" => s.add_xline(XKind::Zline, &ipstr, DNSBL_BAN, "dnsbl", &reason),
"kline" => s.add_xline(XKind::Kline, &format!("*@{ipstr}"), dur, "dnsbl", &reason),
"gline" => s.add_xline(XKind::Gline, &format!("*@{ipstr}"), dur, "dnsbl", &reason),
"zline" => s.add_xline(XKind::Zline, &ipstr, dur, "dnsbl", &reason),
"kill" | "reject" => {}
_ => return, // "mark" or unknown: notify only, don't disconnect
}

View file

@ -21,7 +21,7 @@ use crate::server::{now, Server};
use crate::Uid;
/// Longest token chunk per EXTJWT line (keeps the whole line well under 512).
const CHUNK: usize = 200;
const CHUNK: usize = 200; // default token chunk size if `extjwt_chunk` unset
/// Resolve `(secret, duration)` for a service name (`*` = the default service).
fn service(s: &Server, name: &str) -> Option<(String, u64)> {
@ -133,10 +133,11 @@ impl Command for ExtJwt {
};
// send the token, chunked, with a `*` continuation marker on all but the last
let chunk_sz = s.conf_num("extjwt_chunk", CHUNK).max(1);
let bytes = token.as_bytes();
let mut i = 0;
while i < bytes.len() {
let end = (i + CHUNK).min(bytes.len());
let end = (i + chunk_sz).min(bytes.len());
let chunk = &token[i..end];
let more = end < bytes.len();
let line = if more {

View file

@ -10,8 +10,10 @@ use crate::module::{ModResult, Module};
use crate::server::{now, Server};
use crate::Uid;
const FLOOD_MAX: usize = 8; // messages allowed…
const FLOOD_WINDOW: u64 = 4; // …within this many seconds
// Defaults if unset in the config (`flood_messages` / `flood_seconds`): this many
// messages allowed within this many seconds.
const FLOOD_MAX: usize = 8;
const FLOOD_WINDOW: u64 = 4;
#[derive(Default)]
struct FloodState {
@ -34,6 +36,8 @@ impl Module for Flood {
_text: &str,
) -> ModResult {
let now = now();
let max = srv.conf_num("flood_messages", FLOOD_MAX);
let window = srv.conf_num("flood_seconds", FLOOD_WINDOW);
let (over, warn) = {
let Some(u) = srv.users.get_mut(&uid) else {
return ModResult::Passthru;
@ -42,9 +46,9 @@ impl Module for Flood {
return ModResult::Passthru; // opers bypass flood limits
}
let st = u.ext.get_or_insert_with(FloodState::default);
st.times.retain(|&t| now.saturating_sub(t) < FLOOD_WINDOW);
st.times.retain(|&t| now.saturating_sub(t) < window);
st.times.push(now);
let over = st.times.len() > FLOOD_MAX;
let over = st.times.len() > max;
let warn = over && !st.warned; // notice once per burst
st.warned = over;
(over, warn)

View file

@ -14,10 +14,20 @@ use crate::module::Module;
use crate::server::Server;
use crate::Uid;
/// Limits advertised in the `draft/multiline` cap and enforced while buffering.
/// Default limits (overridable via `multiline_maxbytes` / `multiline_maxlines`),
/// advertised in the `draft/multiline` cap and enforced while buffering.
pub const MAX_BYTES: usize = 4096;
pub const MAX_LINES: usize = 24;
/// The configured maximum total bytes of one multiline batch.
pub fn max_bytes(s: &Server) -> usize {
s.conf_num("multiline_maxbytes", MAX_BYTES)
}
/// The configured maximum number of lines in one multiline batch.
pub fn max_lines(s: &Server) -> usize {
s.conf_num("multiline_maxlines", MAX_LINES)
}
/// An in-progress inbound multiline batch — one long client message being
/// assembled from several `@batch=`-tagged PRIVMSG/NOTICE lines.
pub struct MlineBatch {
@ -58,9 +68,10 @@ pub fn accumulate(
text: &str,
concat: bool,
) -> bool {
let (max_lines, max_bytes) = (max_lines(s), max_bytes(s));
match s.ext.get_mut::<Mline>().and_then(|m| m.0.get_mut(&uid)) {
Some(mb) if mb.bref == bref => {
if mb.parts.len() < MAX_LINES && mb.bytes + text.len() <= MAX_BYTES {
if mb.parts.len() < max_lines && mb.bytes + text.len() <= max_bytes {
mb.notice = notice;
mb.bytes += text.len();
mb.parts.push((text.to_string(), concat));