CBAN (forbid channel-name globs) + SSLINFO command

This commit is contained in:
Jean Chevronnet 2026-08-09 01:04:33 +00:00
parent cab53016e8
commit 4a53c80d4d
5 changed files with 91 additions and 0 deletions

View file

@ -405,6 +405,17 @@ impl Server {
{ {
return; // unknown user, or already joined return; // unknown user, or already joined
} }
// CBAN — a forbidden channel name (opers bypass)
if !self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false) {
if let Some(reason) = self.matched_cban(&key) {
self.numeric(
uid,
ERR_BADCHANNEL,
&format!("{name} :Channel is CBAN'd: {reason}"),
);
return;
}
}
// an existing channel can refuse the join (+k / +b / +i / +l) // an existing channel can refuse the join (+k / +b / +i / +l)
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 {

View file

@ -14,9 +14,56 @@ pub fn commands() -> Vec<Box<dyn Command>> {
Box::new(Motd), Box::new(Motd),
Box::new(VersionCmd), Box::new(VersionCmd),
Box::new(Links), Box::new(Links),
Box::new(SslInfo),
] ]
} }
/// SSLINFO — report a user's TLS status and client-cert fingerprint (InspIRCd
/// `m_sslinfo`). You may query yourself; querying another user requires oper.
struct SslInfo;
impl Command for SslInfo {
fn name(&self) -> &'static str {
"SSLINFO"
}
fn min_params(&self) -> usize {
1
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
let target = params[0].clone();
let Some(tuid) = s.find_nick(&target) else {
s.numeric(
uid,
ERR_NOSUCHNICK,
&format!("{target} :No such nick/channel"),
);
return CmdResult::Fail;
};
if tuid != uid && !s.is_oper(uid) {
s.numeric(uid, ERR_NOPRIVILEGES, ":You may only SSLINFO yourself");
return CmdResult::Fail;
}
let asker = s
.users
.get(&uid)
.map(|u| u.nick.clone())
.unwrap_or_else(|| "*".to_string());
let (nick, secure, certfp) = {
let u = &s.users[&tuid];
(u.nick.clone(), u.secure, u.certfp.clone())
};
let tls = if secure { "yes" } else { "no" };
let fp = certfp.unwrap_or_else(|| "none".to_string());
s.send(
uid,
format!(
":{} NOTICE {asker} :SSLINFO {nick}: TLS={tls} certfp={fp}",
s.name
),
);
CmdResult::Ok
}
}
/// LINKS — the servers this one knows about (itself + every linked peer). /// LINKS — the servers this one knows about (itself + every linked peer).
struct Links; struct Links;
impl Command for Links { impl Command for Links {

View file

@ -34,6 +34,7 @@ pub fn commands() -> Vec<Box<dyn Command>> {
Box::new(Eline), Box::new(Eline),
Box::new(Shun), Box::new(Shun),
Box::new(Qline), Box::new(Qline),
Box::new(Cban),
Box::new(Connect), Box::new(Connect),
Box::new(ChgHost), Box::new(ChgHost),
Box::new(ChgIdent), Box::new(ChgIdent),
@ -683,6 +684,21 @@ impl Command for Qline {
} }
} }
/// CBAN — forbid a channel-name glob (opers bypass it). Mask alone removes; a
/// mask + duration adds. InspIRCd `m_cban`.
struct Cban;
impl Command for Cban {
fn name(&self) -> &'static str {
"CBAN"
}
fn min_params(&self) -> usize {
1
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
do_xline(s, uid, params, XKind::Cban)
}
}
/// CONNECT — dial a configured server link on demand. `CONNECT <servername>`. /// CONNECT — dial a configured server link on demand. `CONNECT <servername>`.
struct Connect; struct Connect;
impl Command for Connect { impl Command for Connect {

View file

@ -31,6 +31,7 @@ pub const ERR_UNAVAILRESOURCE: u16 = 437; // channel temporarily unavailable (+j
pub const ERR_LINKCHANNEL: u16 = 470; // +L — you were redirected to another channel pub const ERR_LINKCHANNEL: u16 = 470; // +L — you were redirected to another channel
pub const ERR_DELAYREJOIN: u16 = 495; // +J — must wait before rejoining after a kick pub const ERR_DELAYREJOIN: u16 = 495; // +J — must wait before rejoining after a kick
pub const ERR_CANTSENDTOUSER: u16 = 531; // +c — no shared channel with the target pub const ERR_CANTSENDTOUSER: u16 = 531; // +c — no shared channel with the target
pub const ERR_BADCHANNEL: u16 = 926; // CBAN — this channel name is forbidden
pub const RPL_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list pub const RPL_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list
pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry
pub const RPL_KNOCK: u16 = 710; // channel gets the knock pub const RPL_KNOCK: u16 = 710; // channel gets the knock

View file

@ -15,6 +15,7 @@ pub enum XKind {
Eline, // user@host / ip EXEMPT from K/G/Z-lines Eline, // user@host / ip EXEMPT from K/G/Z-lines
Shun, // user@host allowed to connect but whose commands are dropped Shun, // user@host allowed to connect but whose commands are dropped
Qline, // a reserved/forbidden nick glob Qline, // a reserved/forbidden nick glob
Cban, // a forbidden channel-name glob
} }
impl XKind { impl XKind {
@ -26,6 +27,7 @@ impl XKind {
XKind::Eline => "E", XKind::Eline => "E",
XKind::Shun => "SHUN", XKind::Shun => "SHUN",
XKind::Qline => "Q", XKind::Qline => "Q",
XKind::Cban => "CBAN",
} }
} }
} }
@ -109,6 +111,20 @@ impl Server {
.map(|x| x.reason.clone()) .map(|x| x.reason.clone())
} }
/// The reason channel `chan` is CBAN'd (forbidden), if any. Case-insensitive.
pub fn matched_cban(&self, chan: &str) -> Option<String> {
let n = now();
let c = chan.to_ascii_lowercase();
self.xlines
.iter()
.find(|x| {
x.kind == XKind::Cban
&& (x.expires == 0 || x.expires > n)
&& glob_match(&x.mask.to_ascii_lowercase(), &c)
})
.map(|x| x.reason.clone())
}
/// The reason a `user@host` / `ip` is banned by an active x-line, if any. /// The reason a `user@host` / `ip` is banned by an active x-line, if any.
/// An E-line (exemption) overrides every K/G/Z-line. /// An E-line (exemption) overrides every K/G/Z-line.
pub fn matched_xline(&self, ident: &str, host: &str, ip: &str) -> Option<String> { pub fn matched_xline(&self, ident: &str, host: &str, ip: &str) -> Option<String> {