engine: track the server tree so a hub SQUIT cascades
All checks were successful
CI / check (push) Successful in 3m50s
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:
parent
e5e04b4130
commit
320a591ae3
5 changed files with 97 additions and 7 deletions
|
|
@ -46,8 +46,11 @@ pub enum NetEvent {
|
||||||
// A user was forcibly removed (KILL). Handled like a quit, except a killed
|
// A user was forcibly removed (KILL). Handled like a quit, except a killed
|
||||||
// services bot is reintroduced rather than forgotten.
|
// services bot is reintroduced rather than forgotten.
|
||||||
UserKilled { uid: String },
|
UserKilled { uid: String },
|
||||||
// A server split away (SQUIT). `server` is its SID; every user behind it
|
// A downstream server was introduced (a sourced SERVER). `parent` is the SID
|
||||||
// (whose uid carries that SID prefix) is gone, since a split is signalled once
|
// that introduced it, so we can track the tree and cascade a hub's SQUIT.
|
||||||
|
ServerLink { sid: String, parent: String },
|
||||||
|
// A server split away (SQUIT). `server` is its SID; every user behind it — and
|
||||||
|
// behind any server in its subtree — is gone, since a split is signalled once
|
||||||
// rather than as a QUIT per user.
|
// rather than as a QUIT per user.
|
||||||
ServerSplit { server: String },
|
ServerSplit { server: String },
|
||||||
// An ircd relaying an IRCv3 account-registration request to us as the authority.
|
// An ircd relaying an IRCv3 account-registration request to us as the authority.
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,19 @@ impl Protocol for InspIrcd {
|
||||||
None => return vec![],
|
None => return vec![],
|
||||||
};
|
};
|
||||||
match cmd.to_ascii_uppercase().as_str() {
|
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],
|
"ENDBURST" => vec![NetEvent::EndBurst],
|
||||||
"PING" => {
|
"PING" => {
|
||||||
let token = tokens
|
let token = tokens
|
||||||
|
|
@ -483,6 +495,16 @@ mod tests {
|
||||||
assert!(matches!(ev.as_slice(), [NetEvent::UserKilled { uid }] if uid == "0IRAAAAAB"), "{ev:?}");
|
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.
|
// A SQUIT surfaces as a ServerSplit carrying the departed server's SID.
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_squit() {
|
fn parses_squit() {
|
||||||
|
|
|
||||||
|
|
@ -783,6 +783,7 @@ impl Engine {
|
||||||
self.bot_idents.clear();
|
self.bot_idents.clear();
|
||||||
self.bot_channels.clear();
|
self.bot_channels.clear();
|
||||||
self.network.clear_bots();
|
self.network.clear_bots();
|
||||||
|
self.network.clear_servers(); // the burst re-introduces the server tree
|
||||||
self.next_bot_index = 0;
|
self.next_bot_index = 0;
|
||||||
let mut out = vec![NetAction::Burst];
|
let mut out = vec![NetAction::Burst];
|
||||||
for svc in &self.services {
|
for svc in &self.services {
|
||||||
|
|
@ -1068,11 +1069,17 @@ impl Engine {
|
||||||
self.forget_user(&uid);
|
self.forget_user(&uid);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
NetEvent::ServerLink { sid, parent } => {
|
||||||
|
self.network.server_link(&sid, &parent);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
NetEvent::ServerSplit { server } => {
|
NetEvent::ServerSplit { server } => {
|
||||||
// Every user behind the departed server is gone in one message;
|
// A hub's SQUIT arrives once but takes its whole subtree with it;
|
||||||
// forget them so their sessions, slots and memberships don't linger.
|
// forget every user behind the departed server and its descendants.
|
||||||
for uid in self.network.uids_on_server(&server) {
|
for sid in self.network.server_split(&server) {
|
||||||
self.forget_user(&uid);
|
for uid in self.network.uids_on_server(&sid) {
|
||||||
|
self.forget_user(&uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ pub struct Network {
|
||||||
// `users` so a reconnect can clear them wholesale without disturbing the
|
// `users` so a reconnect can clear them wholesale without disturbing the
|
||||||
// uplink-sourced user map.
|
// uplink-sourced user map.
|
||||||
bots: HashMap<String, String>,
|
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,
|
// Shared, namespaced stat counters any service contributes to (ephemeral,
|
||||||
// ordered for a stable snapshot). Read by StatServ and the gRPC Stats API.
|
// ordered for a stable snapshot). Read by StatServ and the gRPC Stats API.
|
||||||
stats: BTreeMap<String, u64>,
|
stats: BTreeMap<String, u64>,
|
||||||
|
|
@ -186,6 +189,37 @@ impl Network {
|
||||||
self.users.keys().filter(|u| u.starts_with(sid)).cloned().collect()
|
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) {
|
pub fn user_nick_change(&mut self, uid: &str, nick: String) {
|
||||||
if let Some(user) = self.users.get_mut(uid) {
|
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) });
|
self.seen.insert(lc(&nick), Seen { nick: nick.clone(), ts: now(), what: format!("changing nick from {}", user.nick) });
|
||||||
|
|
|
||||||
|
|
@ -451,6 +451,30 @@
|
||||||
assert_eq!(e.network.account_of("0XYAAAAAB"), Some("bob"), "users on other servers are kept");
|
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
|
// A netburst that replays a user's accountname restores their services login
|
||||||
// (so a services restart doesn't silently log everyone out); empty = logout.
|
// (so a services restart doesn't silently log everyone out); empty = logout.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue