http: bound spawn_http concurrency (http_max_concurrent, default 32) like spawn_crypto — it spawned one unbounded OS thread per call, so a pre-auth VERIFY/REGISTER flood could exhaust threads and hammer the accounts backend; at capacity the command now fails with TEMPORARILY_UNAVAILABLE instead

This commit is contained in:
Jean Chevronnet 2026-08-19 00:39:01 +00:00
parent 801614605f
commit bbe4ee4567
2 changed files with 40 additions and 5 deletions

View file

@ -203,13 +203,21 @@ impl Command for Register {
urlencode(&ip), urlencode(&ip),
port port
); );
s.spawn_http( if !s.spawn_http(
uid, uid,
format!("acctreg:register:{account}"), format!("acctreg:register:{account}"),
url, url,
body, body,
apikey_headers(s), apikey_headers(s),
) {
s.fail(
uid,
"REGISTER",
"TEMPORARILY_UNAVAILABLE",
"The server is busy; please try again in a moment.",
); );
return CmdResult::Fail;
}
CmdResult::Ok CmdResult::Ok
} }
} }
@ -252,13 +260,21 @@ impl Command for Verify {
urlencode(&account), urlencode(&account),
urlencode(&params[1]) urlencode(&params[1])
); );
s.spawn_http( if !s.spawn_http(
uid, uid,
format!("acctreg:verify:{account}"), format!("acctreg:verify:{account}"),
url, url,
body, body,
apikey_headers(s), apikey_headers(s),
) {
s.fail(
uid,
"VERIFY",
"TEMPORARILY_UNAVAILABLE",
"The server is busy; please try again in a moment.",
); );
return CmdResult::Fail;
}
CmdResult::Ok CmdResult::Ok
} }
} }

View file

@ -462,6 +462,10 @@ impl Server {
/// the core as `Event::HttpResult { uid, tag, .. }` — the same self-injection /// the core as `Event::HttpResult { uid, tag, .. }` — the same self-injection
/// pattern as the DNS resolver, so a slow endpoint never blocks the main loop. /// pattern as the DNS resolver, so a slow endpoint never blocks the main loop.
/// `tag` is `"<module>:<detail>"`; the core routes the reply by its prefix. /// `tag` is `"<module>:<detail>"`; the core routes the reply by its prefix.
/// Returns `false` when at capacity (`http_max_concurrent`, default 32) so the
/// caller can reject instead of spawning an unbounded number of threads — a
/// pre-auth flood (e.g. VERIFY) would otherwise exhaust threads and hammer the
/// backend. A `Drop` guard keeps the counter correct even if the task panics.
pub fn spawn_http( pub fn spawn_http(
&self, &self,
uid: Uid, uid: Uid,
@ -469,10 +473,24 @@ impl Server {
url: String, url: String,
body: String, body: String,
headers: Vec<(String, String)>, headers: Vec<(String, String)>,
) { ) -> bool {
use std::sync::atomic::{AtomicUsize, Ordering};
static ACTIVE: AtomicUsize = AtomicUsize::new(0);
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
ACTIVE.fetch_sub(1, Ordering::Relaxed);
}
}
let max = self.conf_num("http_max_concurrent", 32usize);
if ACTIVE.fetch_add(1, Ordering::Relaxed) >= max {
ACTIVE.fetch_sub(1, Ordering::Relaxed);
return false;
}
let tx = self.event_tx.clone(); let tx = self.event_tx.clone();
let verify = self.conf_bool("http_tls_verify", true); let verify = self.conf_bool("http_tls_verify", true);
std::thread::spawn(move || { std::thread::spawn(move || {
let _guard = Guard; // decrements even on panic
let (status, body) = crate::http::post( let (status, body) = crate::http::post(
&url, &url,
"application/x-www-form-urlencoded", "application/x-www-form-urlencoded",
@ -489,6 +507,7 @@ impl Server {
body, body,
}); });
}); });
true
} }
/// Run an expensive credential operation (a KDF: bcrypt / pbkdf2) on a worker /// Run an expensive credential operation (a KDF: bcrypt / pbkdf2) on a worker