solvemsg: arithmetic challenge before an un-vouched user's PMs deliver (anti-spam)

This commit is contained in:
Jean Chevronnet 2026-08-09 23:11:57 +00:00
parent 0bf6992d8a
commit 349200d695
3 changed files with 116 additions and 0 deletions

View file

@ -117,6 +117,10 @@ amu_target = both
# hidewhois_hide_server = yes # hide 312 # hidewhois_hide_server = yes # hide 312
# hidewhois_hide_idle = yes # hide 317 # hidewhois_hide_idle = yes # hide 317
# hidewhois_hide_secure = yes # hide 671 # hidewhois_hide_secure = yes # hide 671
# --- solvemsg (m_solvemsg): an un-vouched user must answer one arithmetic
# question before their private messages are delivered (opers & logged-in
# accounts are exempt). Cheap anti-spam-bot gate.
# solvemsg = yes
# --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient # --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient
# ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also # ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also
# gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST. # gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST.

View file

@ -51,6 +51,7 @@ pub mod securelist;
pub mod securitygroups; pub mod securitygroups;
pub mod serverban; pub mod serverban;
pub mod showfile; pub mod showfile;
pub mod solvemsg;
pub mod snoop; pub mod snoop;
pub mod tline; pub mod tline;
pub mod whoisport; pub mod whoisport;
@ -86,6 +87,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
Box::new(disable::Disable), Box::new(disable::Disable),
Box::new(maphide::MapHide), Box::new(maphide::MapHide),
Box::new(dccallow::DccAllow), Box::new(dccallow::DccAllow),
Box::new(solvemsg::SolveMsg),
] ]
} }

110
src/modules/solvemsg.rs Normal file
View file

@ -0,0 +1,110 @@
//! solvemsg — a lightweight anti-spam gate: before an un-vouched user's *private*
//! messages are delivered, they must answer one small arithmetic question. Opers
//! and users logged into an account are exempt. Off unless `solvemsg = yes`.
//!
//! Flow: the first PM is held and a question is posed; the user replies with the
//! number (that reply is consumed), and once correct every later message passes.
//! Reference: InspIRCd's `m_solvemsg`. Original native Rust.
use crate::module::{ModResult, Module};
use crate::server::Server;
use crate::Uid;
/// Per-user challenge state, on `User.ext`.
#[derive(Default)]
struct SolveState {
solved: bool,
answer: Option<i64>,
}
/// A uniform-ish random byte in `0..max` via openssl (no `rand` crate).
fn rnd(max: u8) -> u8 {
let mut b = [0u8; 1];
let _ = openssl::rand::rand_bytes(&mut b);
b[0] % max
}
pub struct SolveMsg;
impl Module for SolveMsg {
fn name(&self) -> &'static str {
"solvemsg"
}
fn on_pre_message(
&mut self,
s: &mut Server,
uid: Uid,
target: &str,
text: &str,
) -> ModResult {
if !s.conf_bool("solvemsg", false) {
return ModResult::Passthru;
}
// trusted: opers and logged-in accounts never see a challenge
let exempt = s
.users
.get(&uid)
.map(|u| u.flags.oper || u.account.is_some())
.unwrap_or(true);
if exempt {
return ModResult::Passthru;
}
// only gate PMs to another user (channels have their own controls)
let Some(tuid) = s.find_nick(target) else {
return ModResult::Passthru;
};
if tuid == uid {
return ModResult::Passthru;
}
let st = s.users.get(&uid).and_then(|u| u.ext.get::<SolveState>());
if st.map(|st| st.solved).unwrap_or(false) {
return ModResult::Passthru; // already solved
}
let pending = st.and_then(|st| st.answer);
let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
// is this message the answer to an outstanding challenge?
if let Some(ans) = pending {
if text.trim().parse::<i64>().ok() == Some(ans) {
if let Some(u) = s.users.get_mut(&uid) {
u.ext.set(SolveState {
solved: true,
answer: None,
});
}
s.send(
uid,
format!(
":{} NOTICE {nick} :*** Correct — you may now message freely; please resend your message.",
s.name
),
);
return ModResult::Deny; // consume the answer itself
}
}
// pose a fresh question
let a = (rnd(9) + 1) as i64;
let b = (rnd(9) + 1) as i64;
let (sym, ans) = match rnd(3) {
0 => ("+", a + b),
1 => ("-", a - b),
_ => ("*", a * b),
};
if let Some(u) = s.users.get_mut(&uid) {
u.ext.set(SolveState {
solved: false,
answer: Some(ans),
});
}
s.send(
uid,
format!(
":{} NOTICE {nick} :*** To cut spam, answer this to send your message — what is {a} {sym} {b} ?",
s.name
),
);
ModResult::Deny
}
}