protocol: verify echo ircd module dependencies from CAPAB MODULES at link, warning per-feature if one is missing rather than failing silently
All checks were successful
CI / check (push) Successful in 3m49s

This commit is contained in:
Jean Chevronnet 2026-07-18 02:22:43 +00:00
parent 0ca06247f8
commit 2f70b0c133
No known key found for this signature in database
4 changed files with 97 additions and 0 deletions

View file

@ -1116,6 +1116,14 @@ impl Engine {
self.db.set_live_chanmodes(modes);
Vec::new()
}
NetEvent::ModulesAvailable { modules } => {
self.network.learn_modules(modules);
Vec::new()
}
NetEvent::CapabEnd => {
verify_ircd_dependencies(&self.network);
Vec::new()
}
NetEvent::Casemapping { name } => {
// echo folds identifiers as ascii. Verify the ircd agrees, rather than
// assuming it silently — a mismatch would desync account/channel identity.
@ -1619,6 +1627,39 @@ fn event_category(event: &db::Event) -> &'static str {
}
}
// echo's ircd module dependencies: (module, critical?, the feature it enables).
const IRCD_DEPS: &[(&str, bool, &str)] = &[
("account", true, "account login tracking"),
("services", true, "services-server privileges (SVS*, u-line)"),
("rline", false, "regex realname bans (SNLINE)"),
("chghost", false, "vhosts (HostServ)"),
("chgident", false, "vidents (HostServ)"),
("cban", false, "channel-name bans (CBAN)"),
("ircv3_ctctags", false, "tag messages (GameServ)"),
];
// At CAPAB END, check the modules the ircd advertised against echo's dependencies,
// so a missing one is a clear diagnostic at link rather than a silent malfunction.
fn verify_ircd_dependencies(net: &Network) {
if net.module_count() == 0 {
return; // the ircd didn't advertise its modules — nothing to verify against
}
let mut missing = 0;
for &(module, critical, feature) in IRCD_DEPS {
if !net.has_module(module) {
missing += 1;
if critical {
tracing::error!(module, "REQUIRED ircd module not loaded — {feature} will not work");
} else {
tracing::warn!(module, "ircd module not loaded — {feature} is unavailable");
}
}
}
if missing == 0 {
tracing::info!(count = net.module_count(), "verified ircd module dependencies");
}
}
// A one-line, human-readable summary of a logged event for the staff audit
// feed, or None for events that are private (memos), cosmetic self-service
// (greets, auto-join, personal settings), or otherwise not worth surfacing.

View file

@ -25,6 +25,9 @@ pub struct Network {
servers: HashMap<String, String>,
// SID -> server name, for the `server` extban. Rebuilt each link.
server_names: HashMap<String, String>,
// Module names the ircd advertised in its CAPAB burst. Rebuilt each link, used
// to verify echo's dependencies at CAPAB END.
ircd_modules: std::collections::HashSet<String>,
// Shared, namespaced stat counters any service contributes to (persisted via
// StatsSet). Read by StatServ and the gRPC Stats API.
stats: BTreeMap<String, u64>,
@ -161,6 +164,21 @@ impl Network {
self.server_names.insert(sid.to_string(), name);
}
// Accumulate ircd module names from a CAPAB MODULES/MODSUPPORT line.
pub fn learn_modules(&mut self, names: Vec<String>) {
self.ircd_modules.extend(names);
}
// Whether the ircd advertised module `name`. Empty set (never received) reads
// as unknown → callers should skip the dependency check.
pub fn has_module(&self, name: &str) -> bool {
self.ircd_modules.contains(name)
}
pub fn module_count(&self) -> usize {
self.ircd_modules.len()
}
// Mirror the engine's declarative config operators, so services can enumerate
// staff. Called whenever the config oper set changes.
pub fn set_config_opers(&mut self, accounts: Vec<String>) {
@ -292,6 +310,7 @@ impl Network {
pub fn clear_servers(&mut self) {
self.servers.clear();
self.server_names.clear();
self.ircd_modules.clear();
}
pub fn user_nick_change(&mut self, uid: &str, nick: String) {