nickserv/chanserv: announce a blocked look-alike registration to the staff feed (ServiceCtx alert drained by dispatch)
All checks were successful
CI / check (push) Successful in 3m44s

This commit is contained in:
Jean Chevronnet 2026-07-19 00:24:27 +00:00
parent 89ac01d0b0
commit 0d16240606
No known key found for this signature in database
5 changed files with 46 additions and 0 deletions

View file

@ -422,6 +422,10 @@ pub struct ServiceCtx {
// folds these into the shared registry after the command runs — so every // folds these into the shared registry after the command runs — so every
// service reports its own stats through one shared pipe. // service reports its own stats through one shared pipe.
pub stats: Vec<String>, pub stats: Vec<String>,
// Staff-feed alerts (category, text) about what the sender just did. The
// engine drains these to the log channel, prefixing the sender's nick/host —
// e.g. a blocked look-alike registration attempt.
pub alerts: Vec<(String, String)>,
} }
impl ServiceCtx { impl ServiceCtx {
@ -467,6 +471,12 @@ impl ServiceCtx {
self.stats.push(key.into()); self.stats.push(key.into());
} }
// Send a line to the staff feed about what the sender just did. The engine
// prefixes the sender's nick/host and routes it to the log channel.
pub fn alert(&mut self, category: impl Into<String>, text: impl Into<String>) {
self.alerts.push((category.into(), text.into()));
}
// A channel/target message sourced from one of our pseudo-clients (e.g. a // A channel/target message sourced from one of our pseudo-clients (e.g. a
// bot answering a fantasy command, or BotServ SAY). // bot answering a fantasy command, or BotServ SAY).
pub fn privmsg(&mut self, from: &str, to: &str, text: impl Into<String>) { pub fn privmsg(&mut self, from: &str, to: &str, text: impl Into<String>) {

View file

@ -135,6 +135,7 @@ impl Service for ChanServ {
// homoglyph of a real channel), unless the guard is turned off. // homoglyph of a real channel), unless the guard is turned off.
if db.confusable_check_enabled() { if db.confusable_check_enabled() {
if let Some(reason) = echo_api::confusable_reason(chan) { if let Some(reason) = echo_api::confusable_reason(chan) {
ctx.alert("REGISTER", format!("tried to register the look-alike channel \x02{chan}\x02 (blocked)"));
ctx.notice(me, from.uid, reason); ctx.notice(me, from.uid, reason);
return; return;
} }

View file

@ -16,6 +16,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
// unless the guard is turned off. // unless the guard is turned off.
if db.confusable_check_enabled() { if db.confusable_check_enabled() {
if let Some(reason) = echo_api::confusable_reason(from.nick) { if let Some(reason) = echo_api::confusable_reason(from.nick) {
ctx.alert("REGISTER", "tried to register a look-alike nick (blocked)");
ctx.notice(me, from.uid, reason); ctx.notice(me, from.uid, reason);
return; return;
} }

View file

@ -96,11 +96,19 @@ impl Engine {
for key in std::mem::take(&mut ctx.stats) { for key in std::mem::take(&mut ctx.stats) {
self.bump(&key); self.bump(&key);
} }
// Staff-feed alerts a command raised (e.g. a blocked look-alike name),
// prefixed with who the sender is, routed to the log channel.
let alerts = std::mem::take(&mut ctx.alerts);
// A command may have changed the bot registry (BotServ BOT ADD/DEL). // A command may have changed the bot registry (BotServ BOT ADD/DEL).
let mut out = ctx.actions; let mut out = ctx.actions;
out.extend(self.reconcile_bots()); out.extend(self.reconcile_bots());
// Announce whatever this command changed to the staff audit channel. // Announce whatever this command changed to the staff audit channel.
out.extend(self.audit_feed(audit_mark, &nick, account.as_deref())); out.extend(self.audit_feed(audit_mark, &nick, account.as_deref()));
for (cat, text) in alerts {
if let Some(line) = self.feed(&cat, format!("{} {text}", self.who(from))) {
out.push(line);
}
}
self.apply_dict_limit(&mut out); self.apply_dict_limit(&mut out);
self.apply_msg_style(&mut out); self.apply_msg_style(&mut out);
out out

View file

@ -5528,6 +5528,32 @@ fn fantasy_dict_emits_a_lookup_via_the_bot() {
); );
} }
// A blocked look-alike channel registration refuses the user AND tells the staff
// feed who tried it.
#[test]
fn confusable_register_alerts_the_staff_feed() {
use echo_chanserv::ChanServ;
let mut db = svc_db("confalert");
db.register("boss", "sesame", None).unwrap();
let mut e = Engine::new(vec![ns(), Box::new(ChanServ { uid: "42SAAAAAB".into() })], db);
e.set_sid("42S".into());
e.set_log_channel(Some("#services".into()));
e.set_debugserv_uid("42SAAAAAO");
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "host.fr".into(), ip: "1.2.3.4".into() });
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
let chan = "#teѕt"; // Latin te + Cyrillic ѕ + Latin t — a look-alike
e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: chan.into(), op: true });
let out = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAB".into(), text: format!("REGISTER {chan}") });
// The user is refused,
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { to, text, .. } if to == "000AAAAAB" && text.contains("alphabets"))), "user refused: {out:?}");
// and #services is told who tried it.
assert!(
out.iter().any(|a| matches!(a, NetAction::Privmsg { from, to, text }
if from == "42SAAAAAO" && to == "#services" && text.contains("[REGISTER]") && text.contains("boss") && text.contains("look-alike"))),
"staff feed alert: {out:?}"
);
}
// Perf harness — not part of the normal suite. Run: // Perf harness — not part of the normal suite. Run:
// cargo test --release -p echo -- --ignored --nocapture bench_engine // cargo test --release -p echo -- --ignored --nocapture bench_engine
// Measures the two costs that actually bound the single-threaded engine: the // Measures the two costs that actually bound the single-threaded engine: the