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

@ -451,6 +451,30 @@
assert_eq!(e.network.account_of("0XYAAAAAB"), Some("bob"), "users on other servers are kept");
}
// A hub's SQUIT cascades: users behind the hub AND behind every server in its
// subtree are forgotten, while unrelated servers' users are kept.
#[test]
fn squit_cascades_to_the_whole_subtree() {
let mut e = engine_with("squitcascade", "alice", "sesame");
e.db.register("bob", "hunter2", None).unwrap();
e.db.register("carol", "pw", None).unwrap();
// 0HB is a hub under our uplink; 0LF is a leaf behind the hub.
e.handle(NetEvent::ServerLink { sid: "0HB".into(), parent: "42S".into() });
e.handle(NetEvent::ServerLink { sid: "0LF".into(), parent: "0HB".into() });
e.handle(NetEvent::UserConnect { uid: "0HBAAAAAA".into(), nick: "alice".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::UserConnect { uid: "0LFAAAAAA".into(), nick: "bob".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::UserConnect { uid: "0OKAAAAAA".into(), nick: "carol".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::Privmsg { from: "0HBAAAAAA".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
e.handle(NetEvent::Privmsg { from: "0LFAAAAAA".into(), to: "42SAAAAAA".into(), text: "IDENTIFY hunter2".into() });
e.handle(NetEvent::Privmsg { from: "0OKAAAAAA".into(), to: "42SAAAAAA".into(), text: "IDENTIFY pw".into() });
// The hub splits — it and everything behind it are gone.
e.handle(NetEvent::ServerSplit { server: "0HB".into() });
assert!(e.network.account_of("0HBAAAAAA").is_none(), "hub user forgotten");
assert!(e.network.account_of("0LFAAAAAA").is_none(), "user on a leaf behind the hub forgotten");
assert_eq!(e.network.account_of("0OKAAAAAA"), Some("carol"), "unrelated server's user kept");
}
// A netburst that replays a user's accountname restores their services login
// (so a services restart doesn't silently log everyone out); empty = logout.
#[test]