From 06883ced9fbc466d1fafaddd099df4152de759b0 Mon Sep 17 00:00:00 2001 From: reverse Date: Mon, 10 Aug 2026 09:37:51 +0000 Subject: [PATCH] extbanbanlist: matching extban b:#chan (share another channel's ban list) --- echoircd.conf.example | 3 +++ src/channels.rs | 3 ++- src/modules/extbanbanlist.rs | 37 ++++++++++++++++++++++++++++++++++++ src/modules/mod.rs | 1 + src/server.rs | 2 +- 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/modules/extbanbanlist.rs diff --git a/echoircd.conf.example b/echoircd.conf.example index 64567f5..76ff73f 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -120,6 +120,9 @@ amu_target = both # --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so # staff can watch it in a normal window. Set the channel (create/keep it opped): # chanlog = #snotices +# --- extbanbanlist (m_extbanbanlist): no config — adds the matching extban +# `b:<#channel>`, so `+b b:#staff` catches everyone banned in #staff (shares a +# ban list between channels). # --- relaymsg (m_relaymsg / draft/relaymsg): a member whose client negotiated the # capability can /RELAYMSG <#chan> to speak under a spoofed relay # nick (for bridges). The nick must contain a separator and not collide. diff --git a/src/channels.rs b/src/channels.rs index e9ba574..dc8043f 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -939,6 +939,7 @@ impl Server { Some(b'j') => crate::modules::channelban::matches(self, uid, &b.mask[2..]), Some(b's') => crate::modules::serverban::matches(self, uid, &b.mask[2..]), Some(b'G') => crate::modules::geoip::geoban_match(self, uid, &b.mask[2..]), + Some(b'b') => crate::modules::extbanbanlist::matches(self, uid, &b.mask[2..]), _ => false, } } else { @@ -1151,7 +1152,7 @@ pub fn normalize_ban_mask(m: &str) -> String { // These extbans carry a name / spec / channel / server, not a host mask, // so they must not be host-normalised: g: (security group), y: (reputation // score), r: (realname), j: (channel), s: (server name). - if matches!(b[0], b'g' | b'y' | b'r' | b'j' | b's' | b'G') { + if matches!(b[0], b'g' | b'y' | b'r' | b'j' | b's' | b'G' | b'b') { return m.to_string(); } return format!("{}:{}", &m[..1], normalize_mask(&m[2..])); diff --git a/src/modules/extbanbanlist.rs b/src/modules/extbanbanlist.rs new file mode 100644 index 0000000..f6f520f --- /dev/null +++ b/src/modules/extbanbanlist.rs @@ -0,0 +1,37 @@ +//! extbanbanlist — the matching extban `b:<#channel>`: a user is caught if they are +//! on `#channel`'s ban list. Lets one channel share (borrow) another's bans, e.g. +//! `+b b:#staff` bans everyone banned in #staff. Reference: InspIRCd's +//! `m_extbanbanlist`. Original native Rust. +//! +//! The match is deliberately *non-recursive* — it only tests the referenced +//! channel's plain host-mask bans (and its plain excepts), never that channel's own +//! extbans, so two channels referencing each other can't loop. + +use crate::channels::glob_match; +use crate::server::Server; +use crate::Uid; + +/// A plain host-mask ban entry (not itself an extban). +fn is_plain(mask: &str) -> bool { + mask.as_bytes().get(1) != Some(&b':') +} + +/// True if `uid` is on `chan`'s (plain) ban list with no matching plain exception. +pub fn matches(s: &Server, uid: Uid, chan: &str) -> bool { + let Some(who) = s.users.get(&uid).map(|u| u.prefix()) else { + return false; + }; + let Some(ch) = s.channels.get(&chan.to_ascii_lowercase()) else { + return false; + }; + if ch + .excepts + .iter() + .any(|e| is_plain(&e.mask) && glob_match(&e.mask, &who)) + { + return false; + } + ch.bans + .iter() + .any(|b| is_plain(&b.mask) && glob_match(&b.mask, &who)) +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index d0a065d..c84b2dc 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -24,6 +24,7 @@ pub mod dccallow; pub mod denychans; pub mod disable; pub mod dnsbl; +pub mod extbanbanlist; pub mod extended_isupport; pub mod extjwt; pub mod filehost; diff --git a/src/server.rs b/src/server.rs index fe8b655..98230b4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -596,7 +596,7 @@ impl Server { let maxnick = self.conf_num("maxnick", 30usize); let maxchan = self.conf_num("maxchannel", 50usize); let mut lines = vec![format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}", + "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gbcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}", self.network )]; if let Some(tok) = crate::modules::network_icon::isupport(self) {