diff --git a/src/ircd.rs b/src/ircd.rs index b9cafce..349de26 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -73,6 +73,12 @@ pub enum Event { title: String, vhost: String, }, + /// A background connect-class password verify finished (see `crate::modules::connclass`); + /// registration was held until now. + ConnclassAuth { + uid: Uid, + ok: bool, + }, /// A module's async HTTP request finished. `tag` is `":"` /// so the core can route the reply back to the module that issued it (e.g. /// account registration, captcha verification). `status` is 0 on transport @@ -260,6 +266,28 @@ impl Ircd { crate::modules::customtitle::deny(&self.server, uid); } } + Event::ConnclassAuth { uid, ok } => { + // registration was held pending this off-core class-password verify + let held = self + .server + .users + .get_mut(&uid) + .map(|u| { + let was = u.auth_pending; + u.auth_pending = false; + was + }) + .unwrap_or(false); + if !held { + return; // user vanished (or wasn't actually waiting) + } + if ok { + crate::modules::connclass::finish_register(&mut self.server, uid); + self.server.welcome(uid); + } else { + self.reject_link(uid, "Password mismatch for your connection class"); + } + } Event::HttpResult { uid, tag, @@ -500,6 +528,7 @@ impl Ircd { && !u.cap && !u.dns_pending && !u.ident_pending + && !u.auth_pending && u.waitpong.is_none() }) .unwrap_or(false); @@ -555,18 +584,29 @@ impl Ircd { self.server.remove_user(uid, &reason); return; } - // connectclass: verify the class password and apply its on-connect modes - if let Some(reason) = crate::modules::connclass::on_register(&mut self.server, uid) { - self.server - .numeric(uid, ERR_PASSWDMISMATCH, &format!(":{reason}")); - self.server - .send(uid, format!("ERROR :Closing link: ({reason})")); - self.server.remove_user(uid, &reason); - return; + // connectclass: verify the class password and apply its on-connect modes. + // A KDF password verifies off-core: `Pending` holds registration until the + // ConnclassAuth event lands, which then welcomes or rejects. + match crate::modules::connclass::on_register(&mut self.server, uid) { + crate::modules::connclass::AuthOutcome::Proceed => {} + crate::modules::connclass::AuthOutcome::Pending => return, + crate::modules::connclass::AuthOutcome::Reject(reason) => { + self.reject_link(uid, &reason); + return; + } } self.server.welcome(uid); } + /// Refuse a link at registration: numeric + ERROR line + drop the user. + fn reject_link(&mut self, uid: Uid, reason: &str) { + self.server + .numeric(uid, ERR_PASSWDMISMATCH, &format!(":{reason}")); + self.server + .send(uid, format!("ERROR :Closing link: ({reason})")); + self.server.remove_user(uid, reason); + } + fn quit_user(&mut self, uid: Uid, reason: &str) { if !self.server.users.contains_key(&uid) { return; diff --git a/src/modules/connclass.rs b/src/modules/connclass.rs index bbddfd7..249529a 100644 --- a/src/modules/connclass.rs +++ b/src/modules/connclass.rs @@ -344,12 +344,22 @@ pub fn assign(s: &mut Server, uid: Uid) -> Option { None } -/// At registration: re-pick the class now the host is resolved (host masks), verify -/// the class password, enforce a required client cert, and apply on-connect modes. -/// Returns `Some(reason)` to reject. -pub fn on_register(s: &mut Server, uid: Uid) -> Option { - let (ip, host, secure, has_cert, port, sent) = { - let u = s.users.get(&uid)?; +/// Outcome of the connect-class check run at registration. +pub enum AuthOutcome { + /// All checks passed and on-connect modes applied; the caller should welcome. + Proceed, + /// Refuse the connection with this reason. + Reject(String), + /// A slow (KDF) class password is being verified off the core thread; hold + /// registration until the resulting `Event::ConnclassAuth` lands. + Pending, +} + +/// At registration: re-pick the class now the host is resolved (host masks), enforce a +/// required client cert, verify the class password, and apply on-connect modes. A KDF +/// password is verified off the core thread ([`AuthOutcome::Pending`]). +pub fn on_register(s: &mut Server, uid: Uid) -> AuthOutcome { + let Some((ip, host, secure, has_cert, port, sent)) = s.users.get(&uid).map(|u| { ( u.addr.ip().to_string(), u.host.clone(), @@ -358,10 +368,12 @@ pub fn on_register(s: &mut Server, uid: Uid) -> Option { u.port, u.pass.clone(), ) + }) else { + return AuthOutcome::Proceed; }; match pick(s, uid, &ip, &host, secure, has_cert, port) { Pick::Deny(name) => { - return Some(format!("Connection class {name} denies your address")); + return AuthOutcome::Reject(format!("Connection class {name} denies your address")); } Pick::Class(c) => { if let Some(u) = s.users.get_mut(&uid) { @@ -370,24 +382,57 @@ pub fn on_register(s: &mut Server, uid: Uid) -> Option { } Pick::None => {} // keep whatever was assigned at connect } - let name = s.users.get(&uid)?.class.clone()?; - let class = named(s, &name)?; - if let Some(pw) = &class.password { + let Some(class) = s.users.get(&uid).and_then(|u| u.class.clone()).and_then(|n| named(s, &n)) + else { + return AuthOutcome::Proceed; + }; + // cheap cert check before the (possibly slow) password verify + if class.ssl_trusted && !has_cert { + return AuthOutcome::Reject("Your connection class requires a client certificate".into()); + } + if let Some(pw) = class.password.clone() { + // a KDF class password is slow — verify it off the core thread and hold + // registration, so connect floods to a password-protected class can't freeze us. + if crate::modules::password_hash::is_slow(&pw) { + let started = s.spawn_crypto(move || { + let ok = sent + .as_deref() + .map(|p| crate::modules::password_hash::verify(&pw, p)) + .unwrap_or(false); + crate::ircd::Event::ConnclassAuth { uid, ok } + }); + if !started { + return AuthOutcome::Reject("Server busy, try again".into()); + } + if let Some(u) = s.users.get_mut(&uid) { + u.auth_pending = true; + } + return AuthOutcome::Pending; + } let ok = sent .as_deref() - .map(|p| crate::modules::password_hash::verify(pw, p)) + .map(|p| crate::modules::password_hash::verify(&pw, p)) .unwrap_or(false); if !ok { - return Some("Password mismatch for your connection class".to_string()); + return AuthOutcome::Reject("Password mismatch for your connection class".into()); } } - if class.ssl_trusted && !has_cert { - return Some("Your connection class requires a client certificate".to_string()); - } - if let Some(m) = class.modes { + finish_register(s, uid); + AuthOutcome::Proceed +} + +/// Apply the assigned class's on-connect user modes. Runs after the password check +/// (inline, or from the `ConnclassAuth` handler once an off-core verify succeeds). +pub fn finish_register(s: &mut Server, uid: Uid) { + let modes = s + .users + .get(&uid) + .and_then(|u| u.class.clone()) + .and_then(|n| named(s, &n)) + .and_then(|c| c.modes); + if let Some(m) = modes { crate::coremods::core_mode::svs_set_user_modes(s, uid, &m); } - None } // --- per-class getters consulted by the core / other modules ----------------- diff --git a/src/server.rs b/src/server.rs index 39854c6..b86c172 100644 --- a/src/server.rs +++ b/src/server.rs @@ -319,6 +319,7 @@ impl Server { registered: false, dns_pending: false, ident_pending: false, + auth_pending: false, waitpong: None, class: None, pass: None, @@ -1109,6 +1110,7 @@ mod tests { registered: true, dns_pending: false, ident_pending: false, + auth_pending: false, waitpong: None, class: None, pass: None, diff --git a/src/users.rs b/src/users.rs index 73493a3..59bb99a 100644 --- a/src/users.rs +++ b/src/users.rs @@ -294,6 +294,7 @@ pub struct User { pub registered: bool, pub dns_pending: bool, // holding registration for a reverse-DNS lookup pub ident_pending: bool, // holding registration for an ident (RFC1413) lookup + pub auth_pending: bool, // holding registration for an off-core connect-class password verify pub waitpong: Option, // conn_waitpong: cookie the client must PONG before registering pub class: Option, // connectclass: assigned connection class name pub pass: Option, // password sent via PASS (for connectclass passwords)