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

@ -53,7 +53,19 @@ impl Protocol for InspIrcd {
None => return vec![],
};
match cmd.to_ascii_uppercase().as_str() {
"SERVER" => vec![NetEvent::Registered],
// A sourced SERVER ( :<parent> SERVER <name> <sid> [hops] :<desc> ) is a
// downstream link — track it for the server tree. The unsourced auth
// handshake ( SERVER <name> <pass> <sid> … ) is our uplink registering.
"SERVER" => match source {
Some(parent) => {
let _name = tokens.next();
match tokens.next() {
Some(sid) if !sid.is_empty() => vec![NetEvent::ServerLink { sid: sid.to_string(), parent }],
_ => vec![],
}
}
None => vec![NetEvent::Registered],
},
"ENDBURST" => vec![NetEvent::EndBurst],
"PING" => {
let token = tokens
@ -483,6 +495,16 @@ mod tests {
assert!(matches!(ev.as_slice(), [NetEvent::UserKilled { uid }] if uid == "0IRAAAAAB"), "{ev:?}");
}
// The unsourced auth handshake registers our uplink; a sourced SERVER is a
// downstream link tracked (with its parent) for the server tree.
#[test]
fn parses_server_link() {
let mut p = proto();
assert!(matches!(p.parse("SERVER hub.example password 0IR :a hub").as_slice(), [NetEvent::Registered]), "uplink handshake");
assert!(matches!(p.parse(":0IR SERVER leaf.example 0XY :a leaf").as_slice(),
[NetEvent::ServerLink { sid, parent }] if sid == "0XY" && parent == "0IR"), "downstream link");
}
// A SQUIT surfaces as a ServerSplit carrying the departed server's SID.
#[test]
fn parses_squit() {