engine: forget a split server's users on SQUIT
All checks were successful
CI / check (push) Successful in 3m47s

A netsplit is signalled once via SQUIT, not as a QUIT per user, so every
user behind the departed server lingered in services' state — stale
sessions, session-limit slots, and channel memberships. The parser now
surfaces ServerSplit and the engine forgets every uid carrying that
server's SID prefix, leaving other servers' users untouched.
This commit is contained in:
Jean Chevronnet 2026-07-16 10:10:20 +00:00
parent 63ecd79b12
commit 4aff734340
No known key found for this signature in database
5 changed files with 49 additions and 0 deletions

View file

@ -1057,6 +1057,14 @@ impl Engine {
self.forget_user(&uid);
Vec::new()
}
NetEvent::ServerSplit { server } => {
// Every user behind the departed server is gone in one message;
// forget them so their sessions, slots and memberships don't linger.
for uid in self.network.uids_on_server(&server) {
self.forget_user(&uid);
}
Vec::new()
}
NetEvent::Privmsg { from, to, text } => self.dispatch(&from, &to, &text),
NetEvent::AccountRequest { reqid, origin, kind, account, p2, p3 } => {
self.account_request(reqid, origin, kind, account, p2, p3)

View file

@ -180,6 +180,12 @@ impl Network {
self.users.get(uid).map(|u| u.host.as_str())
}
// Every known user whose uid carries `sid` as its prefix — i.e. those behind
// a server, used to forget them all when it splits (SQUIT).
pub fn uids_on_server(&self, sid: &str) -> Vec<String> {
self.users.keys().filter(|u| u.starts_with(sid)).cloned().collect()
}
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) });

View file

@ -432,6 +432,25 @@
assert!(parted, "the assigned bot parts the released channel");
}
// A server split (SQUIT) forgets exactly the users behind it (uids carrying
// that SID), leaving users on other servers untouched.
#[test]
fn squit_forgets_users_behind_the_split() {
let mut e = engine_with("squit", "alice", "sesame");
e.db.register("bob", "hunter2", None).unwrap();
e.handle(NetEvent::UserConnect { uid: "0IRAAAAAB".into(), nick: "alice".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::UserConnect { uid: "0XYAAAAAB".into(), nick: "bob".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::Privmsg { from: "0IRAAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
e.handle(NetEvent::Privmsg { from: "0XYAAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY hunter2".into() });
assert_eq!(e.network.account_of("0IRAAAAAB"), Some("alice"));
assert_eq!(e.network.account_of("0XYAAAAAB"), Some("bob"));
// Server 0IR splits away.
e.handle(NetEvent::ServerSplit { server: "0IR".into() });
assert!(e.network.account_of("0IRAAAAAB").is_none(), "users behind the split are forgotten");
assert_eq!(e.network.account_of("0XYAAAAAB"), Some("bob"), "users on other servers are 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]