engine: forget a split server's users on SQUIT
All checks were successful
CI / check (push) Successful in 3m47s
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:
parent
63ecd79b12
commit
4aff734340
5 changed files with 49 additions and 0 deletions
|
|
@ -46,6 +46,10 @@ 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
|
||||||
|
// (whose uid carries that SID prefix) is gone, since a split is signalled once
|
||||||
|
// rather than as a QUIT per user.
|
||||||
|
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.
|
||||||
AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String },
|
AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String },
|
||||||
// An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D.
|
// An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D.
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,11 @@ impl Protocol for InspIrcd {
|
||||||
Some(uid) if !uid.is_empty() => vec![NetEvent::UserKilled { uid: uid.to_string() }],
|
Some(uid) if !uid.is_empty() => vec![NetEvent::UserKilled { uid: uid.to_string() }],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
},
|
},
|
||||||
|
// :<src> SQUIT <sid> :<reason> — a server split; forget its users.
|
||||||
|
"SQUIT" => match tokens.next() {
|
||||||
|
Some(server) if !server.is_empty() => vec![NetEvent::ServerSplit { server: server.to_string() }],
|
||||||
|
_ => vec![],
|
||||||
|
},
|
||||||
// ENCAP <target> <subcmd> … — we care about relayed SASL and the
|
// ENCAP <target> <subcmd> … — we care about relayed SASL and the
|
||||||
// account-registration relay (the ircd's account module forwards a
|
// account-registration relay (the ircd's account module forwards a
|
||||||
// leaf's REGISTER/VERIFY/RESEND/STATUS to us, the authority server).
|
// leaf's REGISTER/VERIFY/RESEND/STATUS to us, the authority server).
|
||||||
|
|
@ -478,6 +483,13 @@ 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:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A SQUIT surfaces as a ServerSplit carrying the departed server's SID.
|
||||||
|
#[test]
|
||||||
|
fn parses_squit() {
|
||||||
|
let ev = proto().parse(":42S SQUIT 0IR :ping timeout");
|
||||||
|
assert!(matches!(ev.as_slice(), [NetEvent::ServerSplit { server }] if server == "0IR"), "{ev:?}");
|
||||||
|
}
|
||||||
|
|
||||||
// A UID burst introduces the user under their current nick.
|
// A UID burst introduces the user under their current nick.
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_uid_burst() {
|
fn parses_uid_burst() {
|
||||||
|
|
|
||||||
|
|
@ -1057,6 +1057,14 @@ impl Engine {
|
||||||
self.forget_user(&uid);
|
self.forget_user(&uid);
|
||||||
Vec::new()
|
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::Privmsg { from, to, text } => self.dispatch(&from, &to, &text),
|
||||||
NetEvent::AccountRequest { reqid, origin, kind, account, p2, p3 } => {
|
NetEvent::AccountRequest { reqid, origin, kind, account, p2, p3 } => {
|
||||||
self.account_request(reqid, origin, kind, account, p2, p3)
|
self.account_request(reqid, origin, kind, account, p2, p3)
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,12 @@ impl Network {
|
||||||
self.users.get(uid).map(|u| u.host.as_str())
|
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) {
|
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) });
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,25 @@
|
||||||
assert!(parted, "the assigned bot parts the released channel");
|
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
|
// 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