operserv: NOTIFY honours a config exclude list so relay/pylinked clients cannot flood the feed
This commit is contained in:
parent
c415117ec5
commit
8f19a94928
8 changed files with 43 additions and 1 deletions
|
|
@ -123,6 +123,12 @@ impl Expire {
|
|||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Log {
|
||||
pub channel: String,
|
||||
// Masks (nick globs / user@host / extbans) whose users are never announced to
|
||||
// the feed. Set "*/*" to silence PyLink relay clients — their nicks carry a
|
||||
// `/network` suffix — which would otherwise flood the channel through NOTIFY
|
||||
// watches. Reloadable with REHASH.
|
||||
#[serde(default)]
|
||||
pub notify_exclude: Vec<String>,
|
||||
}
|
||||
|
||||
// One services operator: an account and the privileges it holds.
|
||||
|
|
|
|||
|
|
@ -176,6 +176,12 @@ impl Db {
|
|||
self.extban_enabled.as_ref().is_none_or(|set| set.contains(&name.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
/// Masks whose users NOTIFY never matches (`[log] notify_exclude`). Replaces
|
||||
/// the list wholesale so a REHASH can shrink it, not only grow it.
|
||||
pub fn set_notify_exclude(&mut self, masks: Vec<String>) {
|
||||
self.notify_exclude = masks;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
|
|
|||
|
|
@ -1004,6 +1004,9 @@ pub struct Db {
|
|||
log: EventLog,
|
||||
// Which extbans AKICK accepts (`[extban] enabled`). None = all echo knows.
|
||||
extban_enabled: Option<std::collections::HashSet<String>>,
|
||||
// Masks whose users are never matched by NOTIFY (`[log] notify_exclude`), so
|
||||
// relay/pylinked clients don't flood the staff feed. Empty = exclude nobody.
|
||||
notify_exclude: Vec<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>>,
|
||||
|
|
@ -1110,7 +1113,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, live_extbans: None, live_chanmodes: 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, notify_exclude: Vec::new(), live_extbans: None, live_chanmodes: 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
|
||||
|
|
|
|||
|
|
@ -187,6 +187,15 @@ impl Db {
|
|||
/// channel: a `#`/`&` mask tests the channel name, any other tests the user's
|
||||
/// identity. The engine tests the result for a given event's letter.
|
||||
pub fn notify_flags(&self, target: Option<&echo_api::BanTarget>, chan: Option<&str>) -> String {
|
||||
// A user on the exclude list (e.g. a `*/*` PyLink relay client) is never
|
||||
// matched, so relayed traffic can't flood the staff feed.
|
||||
if let Some(t) = target {
|
||||
if self.notify_exclude.iter().any(|m| {
|
||||
echo_api::akick_matches(m, t) || (!m.contains(['@', '!']) && glob_match(m, t.nick))
|
||||
}) {
|
||||
return String::new();
|
||||
}
|
||||
}
|
||||
let now = now();
|
||||
let mut flags = String::new();
|
||||
for n in self.net.notifies.iter().filter(|n| n.expires.is_none_or(|e| e > now)) {
|
||||
|
|
|
|||
|
|
@ -860,6 +860,7 @@ impl Engine {
|
|||
self.set_services_channel(cfg.server.services_channel.clone());
|
||||
self.set_standard_replies(cfg.server.standard_replies);
|
||||
self.set_log_channel(cfg.log.as_ref().map(|l| l.channel.clone()));
|
||||
self.db.set_notify_exclude(cfg.log.as_ref().map(|l| l.notify_exclude.clone()).unwrap_or_default());
|
||||
self.set_guest_nick(&cfg.server.guest_nick);
|
||||
if let Some(expire) = &cfg.expire {
|
||||
self.set_expiry(expire.account_ttl(), expire.channel_ttl(), expire.warn_ttl());
|
||||
|
|
|
|||
|
|
@ -5379,6 +5379,12 @@ fn notify_flags_match_user_nick_channel_and_expiry() {
|
|||
// The expired host watch never matches and is hidden from the list.
|
||||
assert!(db.notify_flags(Some(&bt("x", "spam.net")), None).is_empty(), "expired entry never matches");
|
||||
assert_eq!(db.notifies().len(), 2, "expired entry hidden from the list");
|
||||
// A PyLink relay client (nick carries a /network suffix) is excluded wholesale,
|
||||
// even against a watch it would otherwise match — and a normal nick still hits.
|
||||
db.set_notify_exclude(vec!["*/*".to_string()]);
|
||||
assert!(db.notify_flags(Some(&bt("baddie7/fun", "h.example")), None).is_empty(), "relay client excluded from every watch");
|
||||
assert!(db.notify_flags(Some(&bt("baddie7", "h.example")), None).contains('c'), "a normal nick still matches after exclude is set");
|
||||
db.set_notify_exclude(vec![]);
|
||||
// DEL by mask removes just that one.
|
||||
assert!(db.notify_del("baddie*!*@*").unwrap());
|
||||
assert_eq!(db.notifies().len(), 1);
|
||||
|
|
|
|||
|
|
@ -184,6 +184,12 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
_ => tracing::info!("extban policy: all extbans the ircd offers are accepted"),
|
||||
}
|
||||
if let Some(log) = &cfg.log {
|
||||
if !log.notify_exclude.is_empty() {
|
||||
db.set_notify_exclude(log.notify_exclude.clone());
|
||||
tracing::info!(masks = %log.notify_exclude.join(" "), "notify: excluding masks from the staff feed");
|
||||
}
|
||||
}
|
||||
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