feat(daemon): enforce per-module/per-host caps and shared auth lockout
Wire the shared registry into the accept loop (parent claims a slot before fork, blocks SIGCHLD across fork+pid publication, and reclaims the dead child's slot from the SIGCHLD handler so per-module/per-source counts are released even on SIGKILL). The connection child records the selected module and normalized peer IP once the config frame names them: an over-cap module or source is refused at the config gate with an audit log, and a source that exceeded the auth-failure threshold is refused before a SCRAM challenge (the counter is shared across children and cleared on success). The existing global cap and host ACLs are untouched.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include "transport_tcp.h"
|
||||
#include "daemon_limits.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
@@ -18,15 +19,25 @@
|
||||
|
||||
static volatile sig_atomic_t g_active_connections = 0;
|
||||
|
||||
/* Shared registry installed on the active server; the SIGCHLD handler needs a
|
||||
* file-scope pointer so it can reclaim the dead child's slot. Set once by
|
||||
* accept_loop before the fork loop (single-threaded parent). */
|
||||
static DaemonLimitRegistry* g_limit_registry = NULL;
|
||||
/* Slot reserved by the parent for the connection child currently being forked.
|
||||
* Written before fork(), read by the child (which inherits the value). */
|
||||
static int g_current_slot = DAEMON_LIMITS_NO_SLOT;
|
||||
|
||||
static void tcp_apply_socket_timeout(int fd);
|
||||
static void tcp_enable_nodelay_default(int fd, int family);
|
||||
|
||||
static void sigchld_handler(int sig) {
|
||||
(void)sig;
|
||||
int saved_errno = errno;
|
||||
while (waitpid(-1, NULL, WNOHANG) > 0) {
|
||||
pid_t pid;
|
||||
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
|
||||
if (g_active_connections > 0)
|
||||
g_active_connections--;
|
||||
daemon_limits_reclaim_pid(g_limit_registry, (long)pid);
|
||||
}
|
||||
errno = saved_errno;
|
||||
}
|
||||
@@ -108,6 +119,7 @@ Server* server_create_ex(int port, const ServerBindOptions* bind_opts) {
|
||||
server->ssl_ctx = NULL;
|
||||
server->max_connections = 100;
|
||||
server->active_connections = 0;
|
||||
server->limit_registry = NULL;
|
||||
|
||||
return server;
|
||||
}
|
||||
@@ -121,6 +133,15 @@ void server_set_max_connections(Server* server, unsigned int max_connections) {
|
||||
server->max_connections = max_connections;
|
||||
}
|
||||
|
||||
void server_set_limit_registry(Server* server, struct DaemonLimitRegistry* registry) {
|
||||
if (server)
|
||||
server->limit_registry = registry;
|
||||
}
|
||||
|
||||
int transport_tcp_current_slot(void) {
|
||||
return g_current_slot;
|
||||
}
|
||||
|
||||
void server_delete(Server** server) {
|
||||
if (server == NULL || *server == NULL)
|
||||
return;
|
||||
@@ -140,6 +161,7 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
||||
return;
|
||||
}
|
||||
signal(SIGCHLD, sigchld_handler);
|
||||
g_limit_registry = server->limit_registry;
|
||||
while (1) {
|
||||
struct sockaddr_storage client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
@@ -159,9 +181,31 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
int slot = DAEMON_LIMITS_NO_SLOT;
|
||||
if (server->limit_registry) {
|
||||
slot = daemon_limits_claim_slot(server->limit_registry);
|
||||
if (slot == DAEMON_LIMITS_NO_SLOT) {
|
||||
/* The global cap bounds live children, so this only happens when the
|
||||
* fixed registry is smaller than the configured cap; fail closed. */
|
||||
log_message(LOG_LEVEL_WARNING, "Connection registry slots exhausted (max %u), rejecting %s",
|
||||
server->max_connections, peer);
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
log_message(LOG_LEVEL_INFO, "%s from %s", log_fmt, peer);
|
||||
g_current_slot = slot;
|
||||
/* Block SIGCHLD across fork() and the parent's pid publication: a child
|
||||
* that exits immediately must not be reaped before its slot records its
|
||||
* pid, which would leak the slot and its module/source counts. */
|
||||
sigset_t blocked;
|
||||
sigset_t previous;
|
||||
sigemptyset(&blocked);
|
||||
sigaddset(&blocked, SIGCHLD);
|
||||
sigprocmask(SIG_BLOCK, &blocked, &previous);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
sigprocmask(SIG_SETMASK, &previous, NULL);
|
||||
/* Connection children must not run the parent's global cleanup(): it
|
||||
* frees state (credentials / daemon conf) that the child's worker
|
||||
* threads may still be reading and closes fd numbers the child could
|
||||
@@ -177,7 +221,13 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
||||
_exit(0);
|
||||
} else if (pid > 0) {
|
||||
g_active_connections++;
|
||||
if (server->limit_registry)
|
||||
daemon_limits_set_slot_pid(server->limit_registry, slot, (long)pid);
|
||||
} else if (server->limit_registry) {
|
||||
/* fork() failed: release the reservation so the slot is not leaked. */
|
||||
daemon_limits_reclaim_slot(server->limit_registry, slot);
|
||||
}
|
||||
sigprocmask(SIG_SETMASK, &previous, NULL);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user