diff --git a/modules/operserv/src/forbid.rs b/modules/operserv/src/forbid.rs index 6377acd..0544247 100644 --- a/modules/operserv/src/forbid.rs +++ b/modules/operserv/src/forbid.rs @@ -20,10 +20,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: let reason = args[4..].join(" "); let setter = from.account.unwrap_or(from.nick); let label = kind.wire().to_lowercase(); - match db.forbid_add(kind, mask, setter, &reason) { - Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Forbade {label} \x02{mask}\x02.", label = label, mask = mask)), - Ok(false) => ctx.notice(me, from.uid, t!(ctx, "Updated the forbid on {label} \x02{mask}\x02.", label = label, mask = mask)), - Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), + let ok = match db.forbid_add(kind, mask, setter, &reason) { + Ok(true) => { ctx.notice(me, from.uid, t!(ctx, "Forbade {label} \x02{mask}\x02.", label = label, mask = mask)); true } + Ok(false) => { ctx.notice(me, from.uid, t!(ctx, "Updated the forbid on {label} \x02{mask}\x02.", label = label, mask = mask)); true } + Err(_) => { ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."); false } + }; + // A forbidden NICK is Q-lined so it can't even be USED, not only registered. + if ok && matches!(kind, ForbidKind::Nick) { + ctx.add_line(echo_api::XlineKind::Qline, mask, setter, 0, &reason); } } Some("DEL") | Some("REMOVE") => { @@ -33,7 +37,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: }; let label = kind.wire().to_lowercase(); match db.forbid_del(kind, mask) { - Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed the forbid on {label} \x02{mask}\x02.", label = label, mask = mask)), + Ok(true) => { + ctx.notice(me, from.uid, t!(ctx, "Removed the forbid on {label} \x02{mask}\x02.", label = label, mask = mask)); + if matches!(kind, ForbidKind::Nick) { + ctx.del_line(echo_api::XlineKind::Qline, mask); + } + } Ok(false) => ctx.notice(me, from.uid, t!(ctx, "No forbid on {label} \x02{mask}\x02.", label = label, mask = mask)), Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 72d85fc..04bc42b 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1245,6 +1245,13 @@ impl Engine { let duration = a.expires.map(|e| e.saturating_sub(now)).unwrap_or(0); out.push(NetAction::AddLine { kind: a.kind.wire().to_string(), mask: a.mask, setter: a.setter, duration, reason: a.reason }); } + // Re-assert a Q-line for every forbidden nick, so a FORBID keeps the nick + // unusable across a relink (services X-lines don't survive the split). + for f in self.db.forbids() { + if matches!(f.kind, echo_api::ForbidKind::Nick) { + out.push(NetAction::AddLine { kind: echo_api::XlineKind::Qline.wire().to_string(), mask: f.mask, setter: f.setter, duration: 0, reason: f.reason }); + } + } // Re-assert the spam filters so they survive an ircd restart (m_filter has // no s2s delete, so echo is the durable source of truth for them). for f in self.db.filters() { diff --git a/src/engine/tests.rs b/src/engine/tests.rs index ccc7e5f..27afc22 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -5372,6 +5372,37 @@ assert!(e.db.is_verified("bob"), "bob is now active after the vouch"); } + // FORBID NICK also Q-lines the nick so it can't be used, and the Q-line is + // removed on unforbid and re-asserted on a relink. + #[test] + fn forbid_nick_qlines_it() { + use echo_operserv::OperServ; + let path = std::env::temp_dir().join("echo-forbid-qline.jsonl"); + let _ = std::fs::remove_file(&path); + let mut db = Db::open(&path, "42S"); + db.scram_iterations = 4096; + db.register("alice", "sesame", None).unwrap(); + let ns = NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }; + let os = OperServ { uid: "42SAAAAAH".into() }; + let mut e = Engine::new(vec![Box::new(ns), Box::new(os)], db); + e.set_sid("42S".into()); + let mut opers = std::collections::HashMap::new(); + opers.insert("alice".to_string(), Privs::default().with(echo_api::Priv::Admin)); + e.set_opers(opers); + e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h".into(), ip: "0.0.0.0".into() }); + e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() }); + let to_os = |e: &mut Engine, text: &str| e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAH".into(), text: text.into() }); + + let out = to_os(&mut e, "FORBID ADD NICK badname reserved"); + assert!(out.iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "Q" && mask == "badname")), "qline set: {out:?}"); + + let burst = e.startup_actions(); + assert!(burst.iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "Q" && mask == "badname")), "qline re-asserted on relink: {burst:?}"); + + let out = to_os(&mut e, "FORBID DEL NICK badname"); + assert!(out.iter().any(|a| matches!(a, NetAction::DelLine { kind, mask } if kind == "Q" && mask == "badname")), "qline removed: {out:?}"); + } + // ChanServ SET: description and founder transfer, founder-gated. #[test] fn chanserv_set() {