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

@ -86,6 +86,12 @@ pub enum NetAction {
Topic { from: String, channel: String, topic: String },
// Invite a user to a channel, sourced from pseudoclient `from`.
Invite { from: String, uid: String, channel: String },
// Add a network ban (X-line — `kind` is the ircd's line type, e.g. "G" for a
// G-line) covering `mask`, applied to matching users already online and to
// future connections. `duration` in seconds, 0 = permanent.
AddLine { kind: String, mask: String, setter: String, duration: u64, reason: String },
// Lift a network ban previously set with AddLine.
DelLine { kind: String, mask: String },
Raw(String),
// Internal only, never serialized to the wire: a registration whose password
// still needs its (expensive) key derivation. The link layer runs the
@ -324,6 +330,22 @@ impl ServiceCtx {
channel: channel.to_string(),
});
}
// Set a network ban (X-line) at the ircd. `duration` seconds, 0 = permanent.
pub fn add_line(&mut self, kind: &str, mask: &str, setter: &str, duration: u64, reason: &str) {
self.actions.push(NetAction::AddLine {
kind: kind.to_string(),
mask: mask.to_string(),
setter: setter.to_string(),
duration,
reason: reason.to_string(),
});
}
// Lift a network ban previously set with `add_line`.
pub fn del_line(&mut self, kind: &str, mask: &str) {
self.actions.push(NetAction::DelLine { kind: kind.to_string(), mask: mask.to_string() });
}
}
// ---------------------------------------------------------------------------
@ -386,6 +408,16 @@ pub struct VhostView {
pub expires: Option<u64>,
}
// A network ban (AKILL / G-line) held by OperServ.
#[derive(Debug, Clone)]
pub struct AkillView {
pub mask: String,
pub setter: String,
pub reason: String,
pub ts: u64,
pub expires: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct BotView {
pub nick: String,
@ -632,6 +664,12 @@ pub trait Store {
// Inactivity-expiry pins (oper-only, gated on Priv::Admin at the command layer).
fn set_account_noexpire(&mut self, account: &str, on: bool) -> Result<bool, RegError>;
fn set_channel_noexpire(&mut self, channel: &str, on: bool) -> Result<bool, ChanError>;
// Network bans (AKILL / G-lines, oper-only). `akill_add` returns whether the
// mask was newly added (false = an existing entry was refreshed); `akills`
// lists only entries that haven't lazily expired, oldest first.
fn akill_add(&mut self, mask: &str, setter: &str, reason: &str, expires: Option<u64>) -> Result<bool, RegError>;
fn akill_del(&mut self, mask: &str) -> Result<bool, RegError>;
fn akills(&self) -> Vec<AkillView>;
fn register_channel(&mut self, name: &str, founder: &str) -> Result<(), ChanError>;
fn drop_channel(&mut self, name: &str) -> Result<(), ChanError>;
fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError>;