oper override: bypass +i/+k/+b/+z/+R/+l/+J on JOIN with a snotice

This commit is contained in:
Jean Chevronnet 2026-08-09 01:34:20 +00:00
parent f022ea5a42
commit ef219bc98f

View file

@ -411,8 +411,12 @@ impl Server {
{ {
return; // unknown user, or already joined return; // unknown user, or already joined
} }
// IRC operators override the join restrictions below (m_override); each
// bypass sets `overrode`, snoticed once the join succeeds (accountability).
let is_oper = self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false);
let mut overrode = false;
// CBAN — a forbidden channel name (opers bypass) // CBAN — a forbidden channel name (opers bypass)
if !self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false) { if !is_oper {
if let Some(reason) = self.matched_cban(&key) { if let Some(reason) = self.matched_cban(&key) {
self.numeric( self.numeric(
uid, uid,
@ -422,10 +426,11 @@ impl Server {
return; return;
} }
} }
// an existing channel can refuse the join (+k / +b / +i / +l) // an existing channel can refuse the join (+k / +b / +i / +z / +R / +J)
if let Some(ch) = self.channels.get(&key) { if let Some(ch) = self.channels.get(&key) {
if let Some(k) = &ch.modes.key { if let Some(k) = &ch.modes.key {
if key_arg != Some(k.as_str()) { if key_arg != Some(k.as_str()) {
if !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_BADCHANNELKEY, ERR_BADCHANNELKEY,
@ -433,12 +438,15 @@ impl Server {
); );
return; return;
} }
overrode = true;
}
} }
// +b — bans block even an invited user, unless a +e exception matches // +b — bans block even an invited user, unless a +e exception matches
let mask = self.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); let mask = self.users.get(&uid).map(|u| u.prefix()).unwrap_or_default();
if ch.bans.iter().any(|b| glob_match(&b.mask, &mask)) if ch.bans.iter().any(|b| glob_match(&b.mask, &mask))
&& !ch.excepts.iter().any(|e| glob_match(&e.mask, &mask)) && !ch.excepts.iter().any(|e| glob_match(&e.mask, &mask))
{ {
if !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_BANNEDFROMCHAN, ERR_BANNEDFROMCHAN,
@ -446,11 +454,14 @@ impl Server {
); );
return; return;
} }
overrode = true;
}
// +i — unless invited or matched by a +I invite exception // +i — unless invited or matched by a +I invite exception
if ch.modes.invite_only if ch.modes.invite_only
&& !ch.invites.contains(&uid) && !ch.invites.contains(&uid)
&& !ch.invex.iter().any(|e| glob_match(&e.mask, &mask)) && !ch.invex.iter().any(|e| glob_match(&e.mask, &mask))
{ {
if !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_INVITEONLYCHAN, ERR_INVITEONLYCHAN,
@ -458,8 +469,11 @@ impl Server {
); );
return; return;
} }
overrode = true;
}
// +z — TLS-connected users only // +z — TLS-connected users only
if ch.modes.secure_only && !self.users.get(&uid).map(|u| u.secure).unwrap_or(false) { if ch.modes.secure_only && !self.users.get(&uid).map(|u| u.secure).unwrap_or(false) {
if !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_SECUREONLYCHAN, ERR_SECUREONLYCHAN,
@ -467,8 +481,10 @@ impl Server {
); );
return; return;
} }
// +O — IRC operators only overrode = true;
if ch.modes.oper_only && !self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false) { }
// +O — IRC operators only (opers are allowed by definition)
if ch.modes.oper_only && !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_CANTJOINOPERSONLY, ERR_CANTJOINOPERSONLY,
@ -484,6 +500,7 @@ impl Server {
.map(|u| u.account.is_none()) .map(|u| u.account.is_none())
.unwrap_or(true) .unwrap_or(true)
{ {
if !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_NEEDREGGEDNICK, ERR_NEEDREGGEDNICK,
@ -491,25 +508,34 @@ impl Server {
); );
return; return;
} }
overrode = true;
}
// +J <secs> — can't rejoin within N seconds of being kicked // +J <secs> — can't rejoin within N seconds of being kicked
if let Some(secs) = ch.modes.kicknorejoin { if let Some(secs) = ch.modes.kicknorejoin {
if let Some(&kt) = ch.recent_kicks.get(&uid) { if let Some(&kt) = ch.recent_kicks.get(&uid) {
if now().saturating_sub(kt) < secs as u64 { if now().saturating_sub(kt) < secs as u64 {
if !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_DELAYREJOIN, ERR_DELAYREJOIN,
&format!("{name} :You must wait {secs}s after a kick to rejoin (+J)"), &format!(
"{name} :You must wait {secs}s after a kick to rejoin (+J)"
),
); );
return; return;
} }
overrode = true;
} }
} }
} }
// +l full — with +L redirect, bounce the user to the target instead }
// +l full — with +L redirect, bounce the user to the target instead (opers override)
if let Some(ch) = self.channels.get(&key) { if let Some(ch) = self.channels.get(&key) {
let full = ch.modes.limit.is_some_and(|l| ch.members.len() as u32 >= l); let full = ch.modes.limit.is_some_and(|l| ch.members.len() as u32 >= l);
let redirect = ch.modes.redirect.clone(); let redirect = ch.modes.redirect.clone();
if full { if full && is_oper {
overrode = true;
} else if full {
match redirect { match redirect {
Some(t) Some(t)
if !self.in_redirect if !self.in_redirect
@ -537,8 +563,8 @@ impl Server {
} }
} }
} }
// +j join flood — once tripped, the channel locks new joins out for 60s // +j join flood — once tripped, the channel locks new joins out for 60s (opers exempt)
if self.channels.contains_key(&key) && self.joinflood_check(&key) { if !is_oper && self.channels.contains_key(&key) && self.joinflood_check(&key) {
self.numeric( self.numeric(
uid, uid,
ERR_UNAVAILRESOURCE, ERR_UNAVAILRESOURCE,
@ -546,6 +572,14 @@ impl Server {
); );
return; return;
} }
if overrode {
let nick = self
.users
.get(&uid)
.map(|u| u.nick.clone())
.unwrap_or_default();
self.snotice(&format!("{nick} used oper override to join {name}"));
}
let is_new = !self.channels.contains_key(&key); let is_new = !self.channels.contains_key(&key);
let ch = self let ch = self
.channels .channels