chanserv: full AKICK extban registry, [extban] enabled gate, passive extbans pushed as channel bans for the ircd to enforce
This commit is contained in:
parent
9a197fca3d
commit
f7c38cedad
10 changed files with 224 additions and 65 deletions
|
|
@ -45,6 +45,18 @@ pub struct Config {
|
|||
// credentials (`kc_…` over SASL) are not honoured.
|
||||
#[serde(default)]
|
||||
pub keycard: Option<Keycard>,
|
||||
// Which InspIRCd matching-extbans AKICK may use. Absent = every extban echo
|
||||
// knows (full compatibility). List `enabled` to restrict it — e.g. drop the
|
||||
// ones your ircd doesn't provide.
|
||||
#[serde(default)]
|
||||
pub extban: Option<Extban>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Extban {
|
||||
// Enabled extban names ("account", "realname", "country", …). Empty = all.
|
||||
#[serde(default)]
|
||||
pub enabled: Vec<String>,
|
||||
}
|
||||
|
||||
// Redeeming a website login keycard: a member already authenticated on the
|
||||
|
|
|
|||
|
|
@ -163,6 +163,19 @@ impl Db {
|
|||
self.email_enabled = on;
|
||||
}
|
||||
|
||||
/// Restrict which extbans AKICK accepts to the configured set (`[extban]
|
||||
/// enabled`). An empty list leaves the default (all extbans echo knows).
|
||||
pub fn set_extban_enabled(&mut self, names: Vec<String>) {
|
||||
if !names.is_empty() {
|
||||
self.extban_enabled = Some(names.into_iter().map(|n| n.to_ascii_lowercase()).collect());
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `name` is an enabled extban (all, unless narrowed by config).
|
||||
pub fn extban_allowed(&self, name: &str) -> bool {
|
||||
self.extban_enabled.as_ref().is_none_or(|set| set.contains(&name.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
/// Display name used in email templates.
|
||||
pub fn email_brand(&self) -> &str {
|
||||
&self.email_brand
|
||||
|
|
|
|||
|
|
@ -963,6 +963,8 @@ pub struct Db {
|
|||
channels: HashMap<String, ChannelInfo>, // keyed by casefolded name
|
||||
grouped: HashMap<String, String>, // casefolded alias nick -> canonical account name
|
||||
log: EventLog,
|
||||
// Which extbans AKICK accepts (`[extban] enabled`). None = all echo knows.
|
||||
extban_enabled: Option<std::collections::HashSet<String>>,
|
||||
// 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.
|
||||
|
|
@ -1063,7 +1065,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, 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, 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
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ impl Store for Db {
|
|||
fn channels_owned_by(&self, account: &str) -> Vec<String> {
|
||||
Db::channels_owned_by(self, account)
|
||||
}
|
||||
fn extban_enabled(&self, name: &str) -> bool {
|
||||
Db::extban_allowed(self, name)
|
||||
}
|
||||
fn email_enabled(&self) -> bool {
|
||||
Db::email_enabled(self)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,14 +78,25 @@
|
|||
|
||||
#[test]
|
||||
fn akick_add_del_and_match() {
|
||||
let mut db = Db::open(&tmp("akick"), "N1");
|
||||
let mut db = Db::open(tmp("akick"), "N1");
|
||||
db.register_channel("#c", "founder").unwrap();
|
||||
db.akick_add("#c", "*!*@bad.host", "spam").unwrap();
|
||||
// Also an account extban (the evasion-proof kind) and a realname extban.
|
||||
db.akick_add("#c", "account:baddie", "gone").unwrap();
|
||||
db.akick_add("#c", "realname:*viagra*", "spam").unwrap();
|
||||
let info = db.channel("#c").unwrap();
|
||||
let t = |nick, ident, host, gecos, account| echo_api::BanTarget { nick, ident, host, realhost: host, ip: "0.0.0.0", gecos, account };
|
||||
let t = |nick, ident, host, gecos, account| echo_api::BanTarget {
|
||||
nick,
|
||||
ident,
|
||||
host,
|
||||
realhost: host,
|
||||
ip: "0.0.0.0",
|
||||
gecos,
|
||||
account,
|
||||
server: "irc.test",
|
||||
fingerprint: None,
|
||||
channels: Vec::new(),
|
||||
};
|
||||
assert!(info.akick_match(&t("evil", "~e", "bad.host", "", None)).is_some(), "host mask");
|
||||
assert!(info.akick_match(&t("good", "~g", "ok.host", "", None)).is_none());
|
||||
assert!(info.akick_match(&t("x", "y", "ok.host", "", Some("baddie"))).is_some(), "account extban");
|
||||
|
|
@ -141,7 +152,7 @@
|
|||
#[test]
|
||||
fn access_rank_orders_all_tiers() {
|
||||
use echo_api::Rank;
|
||||
let mut db = Db::open(&tmp("rank"), "N1");
|
||||
let mut db = Db::open(tmp("rank"), "N1");
|
||||
db.register_channel("#c", "boss").unwrap();
|
||||
db.access_add("#c", "sop1", "sop").unwrap();
|
||||
db.access_add("#c", "op1", "op").unwrap();
|
||||
|
|
@ -277,7 +288,7 @@
|
|||
// be ground down online even though it is short.
|
||||
#[test]
|
||||
fn code_burns_after_too_many_wrong_guesses() {
|
||||
let mut db = Db::open(&tmp("code"), "N1");
|
||||
let mut db = Db::open(tmp("code"), "N1");
|
||||
db.scram_iterations = 4096;
|
||||
db.register("alice", "password", None).unwrap();
|
||||
let code = db.issue_code("alice", CodeKind::Reset);
|
||||
|
|
@ -290,7 +301,7 @@
|
|||
// Password auth backs off after a few failures and the throttle clears on success.
|
||||
#[test]
|
||||
fn auth_throttle_locks_then_clears() {
|
||||
let mut db = Db::open(&tmp("throttle"), "N1");
|
||||
let mut db = Db::open(tmp("throttle"), "N1");
|
||||
for _ in 0..=AUTH_FREE_TRIES {
|
||||
db.note_auth("mallory", false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,9 +227,19 @@ impl Network {
|
|||
ip: &u.ip,
|
||||
gecos: &u.gecos,
|
||||
account: self.accounts.get(uid).map(String::as_str),
|
||||
// TODO(extban): capture the user's server + TLS fingerprint from the s2s
|
||||
// stream to match the `server`/`fingerprint` extbans.
|
||||
server: "",
|
||||
fingerprint: None,
|
||||
channels: self.channels_of(uid),
|
||||
})
|
||||
}
|
||||
|
||||
/// The channels `uid` is currently in (for the `channel` extban).
|
||||
pub fn channels_of(&self, uid: &str) -> Vec<String> {
|
||||
self.channels.iter().filter(|(_, c)| c.members.contains(uid)).map(|(k, _)| k.clone()).collect()
|
||||
}
|
||||
|
||||
// Every known user whose uid carries `sid` as its prefix — i.e. those behind
|
||||
// a server, used to forget them all when it splits (SQUIT).
|
||||
pub fn uids_on_server(&self, sid: &str) -> Vec<String> {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,9 @@ async fn main() -> Result<()> {
|
|||
db.set_outbound(gossip_tx.clone());
|
||||
db.set_email_enabled(cfg.email.is_some());
|
||||
db.set_external_accounts(cfg.auth.as_ref().is_some_and(|a| a.external));
|
||||
if let Some(extban) = &cfg.extban {
|
||||
db.set_extban_enabled(extban.enabled.clone());
|
||||
}
|
||||
if let Some(email) = &cfg.email {
|
||||
db.set_email_brand(&email.brand);
|
||||
db.set_email_accent(&email.accent);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue