connclass: verify a KDF class password off the core thread with a registration hold (auth_pending + Event::ConnclassAuth) — connect floods can't freeze the server
This commit is contained in:
parent
2b3495be65
commit
c231c6b8ef
4 changed files with 113 additions and 25 deletions
56
src/ircd.rs
56
src/ircd.rs
|
|
@ -73,6 +73,12 @@ pub enum Event {
|
||||||
title: String,
|
title: String,
|
||||||
vhost: 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 `"<module>:<detail>"`
|
/// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
|
||||||
/// so the core can route the reply back to the module that issued it (e.g.
|
/// 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
|
/// account registration, captcha verification). `status` is 0 on transport
|
||||||
|
|
@ -260,6 +266,28 @@ impl Ircd {
|
||||||
crate::modules::customtitle::deny(&self.server, uid);
|
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 {
|
Event::HttpResult {
|
||||||
uid,
|
uid,
|
||||||
tag,
|
tag,
|
||||||
|
|
@ -500,6 +528,7 @@ impl Ircd {
|
||||||
&& !u.cap
|
&& !u.cap
|
||||||
&& !u.dns_pending
|
&& !u.dns_pending
|
||||||
&& !u.ident_pending
|
&& !u.ident_pending
|
||||||
|
&& !u.auth_pending
|
||||||
&& u.waitpong.is_none()
|
&& u.waitpong.is_none()
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
@ -555,18 +584,29 @@ impl Ircd {
|
||||||
self.server.remove_user(uid, &reason);
|
self.server.remove_user(uid, &reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// connectclass: verify the class password and apply its on-connect modes
|
// connectclass: verify the class password and apply its on-connect modes.
|
||||||
if let Some(reason) = crate::modules::connclass::on_register(&mut self.server, uid) {
|
// A KDF password verifies off-core: `Pending` holds registration until the
|
||||||
self.server
|
// ConnclassAuth event lands, which then welcomes or rejects.
|
||||||
.numeric(uid, ERR_PASSWDMISMATCH, &format!(":{reason}"));
|
match crate::modules::connclass::on_register(&mut self.server, uid) {
|
||||||
self.server
|
crate::modules::connclass::AuthOutcome::Proceed => {}
|
||||||
.send(uid, format!("ERROR :Closing link: ({reason})"));
|
crate::modules::connclass::AuthOutcome::Pending => return,
|
||||||
self.server.remove_user(uid, &reason);
|
crate::modules::connclass::AuthOutcome::Reject(reason) => {
|
||||||
return;
|
self.reject_link(uid, &reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.server.welcome(uid);
|
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) {
|
fn quit_user(&mut self, uid: Uid, reason: &str) {
|
||||||
if !self.server.users.contains_key(&uid) {
|
if !self.server.users.contains_key(&uid) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -344,12 +344,22 @@ pub fn assign(s: &mut Server, uid: Uid) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// At registration: re-pick the class now the host is resolved (host masks), verify
|
/// Outcome of the connect-class check run at registration.
|
||||||
/// the class password, enforce a required client cert, and apply on-connect modes.
|
pub enum AuthOutcome {
|
||||||
/// Returns `Some(reason)` to reject.
|
/// All checks passed and on-connect modes applied; the caller should welcome.
|
||||||
pub fn on_register(s: &mut Server, uid: Uid) -> Option<String> {
|
Proceed,
|
||||||
let (ip, host, secure, has_cert, port, sent) = {
|
/// Refuse the connection with this reason.
|
||||||
let u = s.users.get(&uid)?;
|
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.addr.ip().to_string(),
|
||||||
u.host.clone(),
|
u.host.clone(),
|
||||||
|
|
@ -358,10 +368,12 @@ pub fn on_register(s: &mut Server, uid: Uid) -> Option<String> {
|
||||||
u.port,
|
u.port,
|
||||||
u.pass.clone(),
|
u.pass.clone(),
|
||||||
)
|
)
|
||||||
|
}) else {
|
||||||
|
return AuthOutcome::Proceed;
|
||||||
};
|
};
|
||||||
match pick(s, uid, &ip, &host, secure, has_cert, port) {
|
match pick(s, uid, &ip, &host, secure, has_cert, port) {
|
||||||
Pick::Deny(name) => {
|
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) => {
|
Pick::Class(c) => {
|
||||||
if let Some(u) = s.users.get_mut(&uid) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
|
@ -370,24 +382,57 @@ pub fn on_register(s: &mut Server, uid: Uid) -> Option<String> {
|
||||||
}
|
}
|
||||||
Pick::None => {} // keep whatever was assigned at connect
|
Pick::None => {} // keep whatever was assigned at connect
|
||||||
}
|
}
|
||||||
let name = s.users.get(&uid)?.class.clone()?;
|
let Some(class) = s.users.get(&uid).and_then(|u| u.class.clone()).and_then(|n| named(s, &n))
|
||||||
let class = named(s, &name)?;
|
else {
|
||||||
if let Some(pw) = &class.password {
|
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
|
let ok = sent
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|p| crate::modules::password_hash::verify(pw, p))
|
.map(|p| crate::modules::password_hash::verify(&pw, p))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if !ok {
|
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 {
|
finish_register(s, uid);
|
||||||
return Some("Your connection class requires a client certificate".to_string());
|
AuthOutcome::Proceed
|
||||||
}
|
}
|
||||||
if let Some(m) = class.modes {
|
|
||||||
|
/// 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);
|
crate::coremods::core_mode::svs_set_user_modes(s, uid, &m);
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- per-class getters consulted by the core / other modules -----------------
|
// --- per-class getters consulted by the core / other modules -----------------
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,7 @@ impl Server {
|
||||||
registered: false,
|
registered: false,
|
||||||
dns_pending: false,
|
dns_pending: false,
|
||||||
ident_pending: false,
|
ident_pending: false,
|
||||||
|
auth_pending: false,
|
||||||
waitpong: None,
|
waitpong: None,
|
||||||
class: None,
|
class: None,
|
||||||
pass: None,
|
pass: None,
|
||||||
|
|
@ -1109,6 +1110,7 @@ mod tests {
|
||||||
registered: true,
|
registered: true,
|
||||||
dns_pending: false,
|
dns_pending: false,
|
||||||
ident_pending: false,
|
ident_pending: false,
|
||||||
|
auth_pending: false,
|
||||||
waitpong: None,
|
waitpong: None,
|
||||||
class: None,
|
class: None,
|
||||||
pass: None,
|
pass: None,
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ pub struct User {
|
||||||
pub registered: bool,
|
pub registered: bool,
|
||||||
pub dns_pending: bool, // holding registration for a reverse-DNS lookup
|
pub dns_pending: bool, // holding registration for a reverse-DNS lookup
|
||||||
pub ident_pending: bool, // holding registration for an ident (RFC1413) 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<String>, // conn_waitpong: cookie the client must PONG before registering
|
pub waitpong: Option<String>, // conn_waitpong: cookie the client must PONG before registering
|
||||||
pub class: Option<String>, // connectclass: assigned connection class name
|
pub class: Option<String>, // connectclass: assigned connection class name
|
||||||
pub pass: Option<String>, // password sent via PASS (for connectclass passwords)
|
pub pass: Option<String>, // password sent via PASS (for connectclass passwords)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue