banredirect: +b mask$#chan bounces a banned user to another channel (loop-guarded)

This commit is contained in:
Jean Chevronnet 2026-08-09 23:17:32 +00:00
parent 349200d695
commit 1a53c5ae5a
4 changed files with 53 additions and 1 deletions

View file

@ -117,6 +117,10 @@ amu_target = both
# hidewhois_hide_server = yes # hide 312
# hidewhois_hide_idle = yes # hide 317
# hidewhois_hide_secure = yes # hide 671
# --- banredirect (m_banredirect): no config needed — it extends ban syntax. A
# ban `+b <mask>$<#channel>` bounces a matching user into #channel instead of
# refusing them, e.g. /MODE #main +b *!*@*.spammer.net$#quarantine
# The redirect fires at most once (never loops).
# --- 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.

View file

@ -560,6 +560,21 @@ impl Server {
// (both honour the g: security-group extban)
if self.ban_list_hit(uid, &ch.bans) && !self.ban_list_hit(uid, &ch.excepts) {
if !is_oper {
// banredirect: `+b mask$#chan` bounces the user into #chan (once)
if let Some(t) = crate::modules::banredirect::redirect_target(self, uid, &key) {
let tl = t.to_ascii_lowercase();
if !self.in_redirect && tl != key && !self.is_member(uid, &tl) {
self.numeric(
uid,
ERR_LINKCHANNEL,
&format!("{name} {t} :Cannot join channel (+b), redirecting"),
);
self.in_redirect = true;
self.join(uid, &t, None);
self.in_redirect = false;
return;
}
}
self.numeric(
uid,
ERR_BANNEDFROMCHAN,
@ -925,7 +940,8 @@ impl Server {
_ => false,
}
} else {
glob_match(&b.mask, &who)
// strip any `$#chan` banredirect suffix before matching the mask
glob_match(crate::modules::banredirect::mask_part(&b.mask), &who)
}
})
}

View file

@ -0,0 +1,31 @@
//! banredirect — a ban of the form `+b <mask>$<#channel>` bounces a matching,
//! banned user into `#channel` instead of refusing them outright. The redirect
//! fires at most once, guarded by `Server.in_redirect` (shared with the `+L`
//! full-channel redirect), so it can never loop. Reference: InspIRCd's
//! `m_banredirect`. Original native Rust.
use crate::channels::glob_match;
use crate::server::Server;
use crate::Uid;
/// The mask part of a ban, without any `$#chan` redirect suffix — used both for
/// matching (a redirect ban must still *block*) and for reading the target.
pub fn mask_part(mask: &str) -> &str {
mask.split('$').next().unwrap_or(mask)
}
/// If `uid` is caught by a redirect ban (`mask$#chan`) in `key` with no matching
/// exception, return the target channel. The caller has already established the
/// user is banned; this extracts the `$#chan` destination.
pub fn redirect_target(s: &Server, uid: Uid, key: &str) -> Option<String> {
let ch = s.channels.get(key)?;
let who = s.users.get(&uid)?.prefix();
// a matching +e exception cancels the ban, hence the redirect
if ch.excepts.iter().any(|e| glob_match(mask_part(&e.mask), &who)) {
return None;
}
ch.bans.iter().find_map(|b| {
let (mask, redir) = b.mask.split_once('$')?;
(redir.starts_with('#') && glob_match(mask, &who)).then(|| redir.to_string())
})
}

View file

@ -6,6 +6,7 @@
pub mod account_registration;
pub mod antimixedutf8;
pub mod antirandom;
pub mod banredirect;
pub mod blockamsg;
pub mod channames;
pub mod channelban;