engine: track the server tree so a hub SQUIT cascades
All checks were successful
CI / check (push) Successful in 3m50s

SQUIT arrives once for a splitting server (by SID), but a hub takes its
whole subtree with it — users behind downstream servers were left as stale
state. The parser now distinguishes a sourced downstream SERVER (tracked
with its parent) from the unsourced uplink handshake, the engine keeps the
SID->parent tree, and a SQUIT forgets every user across the departed
server's subtree. Tree is rebuilt each link. Wire formats verified against
the InspIRCd 4 m_spanningtree source.
This commit is contained in:
Jean Chevronnet 2026-07-16 10:31:29 +00:00
parent e5e04b4130
commit 320a591ae3
No known key found for this signature in database
5 changed files with 97 additions and 7 deletions

View file

@ -20,6 +20,9 @@ pub struct Network {
// `users` so a reconnect can clear them wholesale without disturbing the
// uplink-sourced user map.
bots: HashMap<String, String>,
// Downstream server tree: SID -> the SID that introduced it. Lets a hub's
// SQUIT cascade to every server (and user) behind it. Rebuilt each link.
servers: HashMap<String, String>,
// Shared, namespaced stat counters any service contributes to (ephemeral,
// ordered for a stable snapshot). Read by StatServ and the gRPC Stats API.
stats: BTreeMap<String, u64>,
@ -186,6 +189,37 @@ impl Network {
self.users.keys().filter(|u| u.starts_with(sid)).cloned().collect()
}
// Record a downstream server and the SID that introduced it (its parent).
pub fn server_link(&mut self, sid: &str, parent: &str) {
self.servers.insert(sid.to_string(), parent.to_string());
}
// Forget the server subtree behind (and including) `sid` — its own SID plus
// every server introduced beneath it — and return the whole set so their users
// can be forgotten too. A hub's SQUIT arrives once but takes its children with it.
pub fn server_split(&mut self, sid: &str) -> Vec<String> {
let mut subtree = vec![sid.to_string()];
let mut i = 0;
while i < subtree.len() {
let parent = subtree[i].clone();
for (child, up) in &self.servers {
if up == &parent && !subtree.contains(child) {
subtree.push(child.clone());
}
}
i += 1;
}
for s in &subtree {
self.servers.remove(s);
}
subtree
}
// Drop the whole server tree (on a fresh link, before the burst rebuilds it).
pub fn clear_servers(&mut self) {
self.servers.clear();
}
pub fn user_nick_change(&mut self, uid: &str, nick: String) {
if let Some(user) = self.users.get_mut(uid) {
self.seen.insert(lc(&nick), Seen { nick: nick.clone(), ts: now(), what: format!("changing nick from {}", user.nick) });