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,16 +426,19 @@ 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()) {
self.numeric( if !is_oper {
uid, self.numeric(
ERR_BADCHANNELKEY, uid,
&format!("{name} :Cannot join channel (+k)"), ERR_BADCHANNELKEY,
); &format!("{name} :Cannot join channel (+k)"),
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
@ -439,36 +446,45 @@ impl Server {
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))
{ {
self.numeric( if !is_oper {
uid, self.numeric(
ERR_BANNEDFROMCHAN, uid,
&format!("{name} :Cannot join channel (+b)"), ERR_BANNEDFROMCHAN,
); &format!("{name} :Cannot join channel (+b)"),
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))
{ {
self.numeric( if !is_oper {
uid, self.numeric(
ERR_INVITEONLYCHAN, uid,
&format!("{name} :Cannot join channel (+i)"), ERR_INVITEONLYCHAN,
); &format!("{name} :Cannot join channel (+i)"),
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) {
self.numeric( if !is_oper {
uid, self.numeric(
ERR_SECUREONLYCHAN, uid,
&format!("{name} :Cannot join channel; TLS users only (+z is set)"), ERR_SECUREONLYCHAN,
); &format!("{name} :Cannot join channel; TLS users only (+z is set)"),
return; );
return;
}
overrode = true;
} }
// +O — IRC operators only // +O — IRC operators only (opers are allowed by definition)
if ch.modes.oper_only && !self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false) { if ch.modes.oper_only && !is_oper {
self.numeric( self.numeric(
uid, uid,
ERR_CANTJOINOPERSONLY, ERR_CANTJOINOPERSONLY,
@ -484,32 +500,42 @@ impl Server {
.map(|u| u.account.is_none()) .map(|u| u.account.is_none())
.unwrap_or(true) .unwrap_or(true)
{ {
self.numeric( if !is_oper {
uid, self.numeric(
ERR_NEEDREGGEDNICK, uid,
&format!("{name} :Cannot join channel; you must be logged in (+R is set)"), ERR_NEEDREGGEDNICK,
); &format!("{name} :Cannot join channel; you must be logged in (+R is set)"),
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 {
self.numeric( if !is_oper {
uid, self.numeric(
ERR_DELAYREJOIN, uid,
&format!("{name} :You must wait {secs}s after a kick to rejoin (+J)"), ERR_DELAYREJOIN,
); &format!(
return; "{name} :You must wait {secs}s after a kick to rejoin (+J)"
),
);
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