auth: offload IDENTIFY + SASL PLAIN verify off the engine lock
All checks were successful
CI / check (push) Successful in 3m47s

Both ran scram verify_plain (~1s PBKDF2 at 1.2M iters) inline under the engine
lock, freezing the whole daemon per login. Add NetAction::DeferAuthenticate +
AuthThen continuation + ctx.defer_authenticate, mirroring DeferPassword: the
module/SASL path fetches the verifier cheaply and defers; the link layer runs
verify_plain on spawn_blocking, then Engine::complete_authenticate finishes the
login (IDENTIFY reuses the same ctx helpers, so its login/AJOIN/vhost/memo
side-effects are unchanged). Tests resolve the defer inline (cfg(test)). The
gRPC web-login path was already fixed; no login path stalls services now.
(GHOST/DROP/CERT/GROUP still verify inline but are rare account ops, not logins.)
This commit is contained in:
Jean Chevronnet 2026-07-16 19:04:18 +00:00
parent 63fc2fb2c0
commit 2f9790feac
No known key found for this signature in database
9 changed files with 144 additions and 36 deletions

View file

@ -122,6 +122,12 @@ pub enum NetAction {
// Internal only: a password change awaiting the same off-thread derivation.
// The link layer derives, then calls Engine::complete_password_change.
DeferPassword { account: String, password: String, agent: String, uid: String },
// Internal only: a password VERIFY (IDENTIFY / SASL PLAIN) awaiting the same
// off-thread work — the PBKDF2 that verify_plain runs is ~1s at production
// iteration counts, so it must never run under the engine lock. The link layer
// fetches `verifier` cheaply, runs verify_plain off-thread, then calls
// Engine::complete_authenticate with the boolean result and `then`.
DeferAuthenticate { verifier: String, password: String, then: AuthThen },
// Internal only: send an email (plaintext + optional HTML). The link layer
// pipes it to the configured mail command off-thread; never serialized.
SendEmail { to: String, subject: String, text: String, html: Option<String> },
@ -141,6 +147,18 @@ pub enum RegReply {
NickServ { agent: String, uid: String, nick: String },
}
/// What to do once a deferred password verify (see [`NetAction::DeferAuthenticate`])
/// completes, given the boolean result. The cheap pre-checks already ran; this is
/// only the success/failure finish.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthThen {
// NickServ IDENTIFY: `uid` logs in, `agent` (NickServ uid) sends the notices,
// `name` is what the user typed (for lockout/note_auth), `account` is canonical.
Identify { uid: String, agent: String, name: String, account: String },
// SASL PLAIN: finish the SASL exchange for `client`, sourced from `agent`.
Sasl { agent: String, client: String, account: String },
}
/// The ircd link layer. The engine only ever sees [`NetEvent`] / [`NetAction`];
/// raw server-to-server lines live entirely behind a `Protocol` impl, so a new
/// ircd is one new module and the engine is untouched.
@ -317,6 +335,16 @@ impl ServiceCtx {
});
}
// Hand a password verify (IDENTIFY / SASL PLAIN) to the engine to finish: the
// ~1s PBKDF2 runs off the reactor, then the engine completes it via `then`.
pub fn defer_authenticate(&mut self, verifier: impl Into<String>, password: impl Into<String>, then: AuthThen) {
self.actions.push(NetAction::DeferAuthenticate {
verifier: verifier.into(),
password: password.into(),
then,
});
}
// Log a user into an account: sets the accountname the ircd turns into
// RPL_LOGGEDIN (900) and exposes to account-tag / WHOX, the same login the
// SASL agent applies. Used after a successful REGISTER / IDENTIFY.
@ -926,6 +954,10 @@ pub trait Store {
fn accounts_by_email(&self, pattern: &str) -> Vec<String>;
// The canonical account name if the password is correct, else None.
fn authenticate(&self, name: &str, password: &str) -> Option<&str>;
/// The account's canonical name and its SHA-256 verifier (owned), so the
/// caller can run the ~1s PBKDF2 verify OFF the engine lock via
/// `ctx.defer_authenticate` rather than the blocking `authenticate`.
fn scram_verifier(&self, name: &str) -> Option<(String, String)>;
fn grouped_nicks(&self, account: &str) -> Vec<String>;
fn certfps(&self, account: &str) -> &[String];
fn is_verified(&self, account: &str) -> bool;