extban: learn the ircd live set from CAPAB EXTBANS and validate AKICK/MODE against what it actually offers

This commit is contained in:
Jean Chevronnet 2026-07-17 21:47:02 +00:00
parent da94142bad
commit c990eb1e1e
No known key found for this signature in database
8 changed files with 95 additions and 1 deletions

View file

@ -176,6 +176,17 @@ impl Db {
self.extban_enabled.as_ref().is_none_or(|set| set.contains(&name.to_ascii_lowercase()))
}
/// Record the ircd's live extban set (its CAPAB EXTBANS burst).
pub fn set_live_extbans(&mut self, entries: Vec<echo_api::ExtbanCap>) {
self.live_extbans = Some(entries);
}
/// Whether this ircd actually offers `name`. True until the ircd tells us its
/// set (we don't second-guess before the burst).
pub fn extban_offered(&self, name: &str) -> bool {
self.live_extbans.as_ref().is_none_or(|set| set.iter().any(|e| e.name.eq_ignore_ascii_case(name)))
}
/// Display name used in email templates.
pub fn email_brand(&self) -> &str {
&self.email_brand

View file

@ -965,6 +965,9 @@ pub struct Db {
log: EventLog,
// Which extbans AKICK accepts (`[extban] enabled`). None = all echo knows.
extban_enabled: Option<std::collections::HashSet<String>>,
// The ircd's live extban set from its CAPAB EXTBANS burst. None until we link;
// once known, it's authoritative for what AKICK/MODE may set.
live_extbans: Option<Vec<echo_api::ExtbanCap>>,
// PBKDF2 cost baked into new SCRAM verifiers; lowered by tests.
pub(crate) scram_iterations: u32,
// Whether outbound email is configured, so email features can gate themselves.
@ -1065,7 +1068,7 @@ impl Db {
apply(&mut accounts, &mut channels, &mut grouped, &mut bots, &mut host_cfg, &mut net, event);
}
tracing::info!(accounts = accounts.len(), channels = channels.len(), "account store loaded");
Self { accounts, channels, grouped, log, extban_enabled: None, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), email_logo: String::new(), codes: HashMap::new(), auth_fails: HashMap::new(), vhost_req_times: HashMap::new(), report_times: HashMap::new(), bots, host_cfg, net, ignores: Vec::new(), defcon: 5, external_accounts: false }
Self { accounts, channels, grouped, log, extban_enabled: None, live_extbans: None, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), email_logo: String::new(), codes: HashMap::new(), auth_fails: HashMap::new(), vhost_req_times: HashMap::new(), report_times: HashMap::new(), bots, host_cfg, net, ignores: Vec::new(), defcon: 5, external_accounts: false }
}
/// Fold an entry authored by another node into the store — the services-side

View file

@ -57,6 +57,9 @@ impl Store for Db {
fn extban_enabled(&self, name: &str) -> bool {
Db::extban_allowed(self, name)
}
fn extban_offered(&self, name: &str) -> bool {
Db::extban_offered(self, name)
}
fn email_enabled(&self) -> bool {
Db::email_enabled(self)
}

View file

@ -1098,6 +1098,12 @@ impl Engine {
self.network.set_server_name(&sid, name);
Vec::new()
}
NetEvent::ExtbanRegistry { entries } => {
let names = entries.iter().map(|e| e.name.as_str()).collect::<Vec<_>>().join(" ");
tracing::info!(count = entries.len(), extbans = %names, "learned ircd extban set");
self.db.set_live_extbans(entries);
Vec::new()
}
NetEvent::UserConnect { uid, nick, host, ip } => {
let arriving_nick = nick.clone();
self.network.user_connect(uid.clone(), nick, host, ip.clone());