Add OperServ with AKILL network bans

The first operator module. AKILL manages network bans as a typed,
event-sourced list with lazy expiry, mirroring the patterns the other
modules use:

- AKILL ADD [+expiry] <user@host> <reason> stores the ban and drives an
  ircd G-line (applied to matching users already online and to future
  connections); AKILL DEL <mask|number> lifts it; AKILL LIST [pattern]
  shows the live list, numbered. Admin-only.
- Bans are Global events, so every node holds the list and re-asserts each
  one at burst with its remaining duration. Expired bans are hidden lazily
  and dropped at compaction.
- Masks are normalised to user@host (a nick! prefix is stripped, both
  sides lowercased) and an all-wildcard mask is refused.

Email (send_email) is already reachable from the api via ServiceCtx, and
adds AddLine/DelLine to the action vocabulary for any future X-line kinds.
This commit is contained in:
Jean Chevronnet 2026-07-14 00:12:29 +00:00
parent 253d4c2a2d
commit d2c1d076fb
No known key found for this signature in database
12 changed files with 430 additions and 12 deletions

View file

@ -318,6 +318,14 @@ impl Protocol for InspIrcd {
NetAction::Invite { from, uid, channel } => {
vec![format!(":{} INVITE {} {} 1 0", from, uid, channel)]
}
// ADDLINE <type> <mask> <setter> <set-time> <duration> :<reason>. A
// duration of 0 is permanent; the ircd applies it to matching users
// already online and propagates it across the network.
NetAction::AddLine { kind, mask, setter, duration, reason } => {
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(self.ts);
vec![self.sourced(format!("ADDLINE {} {} {} {} {} :{}", kind, mask, setter, now, duration, reason))]
}
NetAction::DelLine { kind, mask } => vec![self.sourced(format!("DELLINE {} {}", kind, mask))],
NetAction::Raw(s) => vec![s.clone()],
// Internal: the link layer handles these before serialization.
NetAction::DeferRegister { .. } | NetAction::DeferPassword { .. } | NetAction::SendEmail { .. } => vec![],
@ -485,6 +493,23 @@ mod tests {
assert!(!lines[0].contains('\n') && !lines[0].contains('\r'));
}
// A network ban serializes to ADDLINE / DELLINE, sourced from our server.
#[test]
fn serializes_network_bans() {
let add = proto().serialize(&NetAction::AddLine {
kind: "G".into(),
mask: "*@evil.host".into(),
setter: "staff".into(),
duration: 3600,
reason: "spamming".into(),
});
assert_eq!(add.len(), 1);
assert!(add[0].starts_with(":42S ADDLINE G *@evil.host staff "), "{add:?}");
assert!(add[0].ends_with(" 3600 :spamming"), "{add:?}");
let del = proto().serialize(&NetAction::DelLine { kind: "G".into(), mask: "*@evil.host".into() });
assert_eq!(del, vec![":42S DELLINE G *@evil.host".to_string()]);
}
#[test]
fn parses_ftopic_and_filters_own() {
let ev = proto().parse(":0IRAAAAAB FTOPIC #chan 1783845132 1783845140 :Welcome all");