Release v2.26.0 #284
@@ -86,6 +86,7 @@ set(SHARED_SRCS
|
|||||||
src/shared/config.c
|
src/shared/config.c
|
||||||
src/shared/credentials.c
|
src/shared/credentials.c
|
||||||
src/shared/daemon_conf.c
|
src/shared/daemon_conf.c
|
||||||
|
src/shared/daemon_limits.c
|
||||||
src/shared/data.c
|
src/shared/data.c
|
||||||
src/shared/delay_updates.c
|
src/shared/delay_updates.c
|
||||||
src/shared/delta.c
|
src/shared/delta.c
|
||||||
@@ -205,6 +206,7 @@ set(TEST_SRCS
|
|||||||
tests/test_config.c
|
tests/test_config.c
|
||||||
tests/test_credentials.c
|
tests/test_credentials.c
|
||||||
tests/test_daemon_conf.c
|
tests/test_daemon_conf.c
|
||||||
|
tests/test_daemon_limits.c
|
||||||
tests/test_data.c
|
tests/test_data.c
|
||||||
tests/test_delay_updates.c
|
tests/test_delay_updates.c
|
||||||
tests/test_delta.c
|
tests/test_delta.c
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
#include "daemon_limits.h"
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* Slot lifecycle states (stored in slot_state). */
|
||||||
|
enum {
|
||||||
|
SLOT_FREE = 0,
|
||||||
|
SLOT_CLAIMED = 1,
|
||||||
|
SLOT_REGISTERED = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The registry header lives at the base of the shared mapping; the pointer
|
||||||
|
* fields point at the arrays carved out of the same mapping. Absolute pointers
|
||||||
|
* remain valid in a forked child because fork() clones the address space and
|
||||||
|
* mapping, so parent and child observe the same virtual addresses. */
|
||||||
|
struct DaemonLimitRegistry {
|
||||||
|
int max_slots;
|
||||||
|
int module_count;
|
||||||
|
int host_slots; /* power of two; 1 when no per-source tracking is needed */
|
||||||
|
int per_host_cap;
|
||||||
|
int lockout_threshold;
|
||||||
|
int lockout_duration_sec;
|
||||||
|
size_t map_size;
|
||||||
|
_Atomic int* slot_state;
|
||||||
|
_Atomic int* slot_pid;
|
||||||
|
_Atomic int* slot_module;
|
||||||
|
_Atomic int* slot_host; /* per-source table bucket, or -1 */
|
||||||
|
_Atomic int* module_active;
|
||||||
|
_Atomic uint64_t* host_key; /* 0 == empty bucket */
|
||||||
|
_Atomic int* host_active;
|
||||||
|
_Atomic int* host_fail;
|
||||||
|
_Atomic long long* host_until; /* epoch seconds the lockout expires */
|
||||||
|
};
|
||||||
|
|
||||||
|
static size_t round_up(size_t n, size_t align) {
|
||||||
|
return (n + align - 1) & ~(align - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t next_pow2(size_t n) {
|
||||||
|
size_t p = 1;
|
||||||
|
while (p < n)
|
||||||
|
p <<= 1;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse a numeric IPv4/IPv6 peer string into family + raw bytes. */
|
||||||
|
static bool parse_peer_ip(const char* peer_ip, int* family, unsigned char* bytes) {
|
||||||
|
if (!peer_ip || *peer_ip == '\0')
|
||||||
|
return false;
|
||||||
|
struct in_addr v4;
|
||||||
|
if (inet_pton(AF_INET, peer_ip, &v4) == 1) {
|
||||||
|
memcpy(bytes, &v4, sizeof(v4));
|
||||||
|
*family = AF_INET;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
struct in6_addr v6;
|
||||||
|
if (inet_pton(AF_INET6, peer_ip, &v6) == 1) {
|
||||||
|
memcpy(bytes, &v6, sizeof(v6));
|
||||||
|
*family = AF_INET6;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t daemon_limits_host_hash(const char* peer_ip, bool* ok) {
|
||||||
|
if (ok)
|
||||||
|
*ok = false;
|
||||||
|
unsigned char bytes[16];
|
||||||
|
int family = AF_UNSPEC;
|
||||||
|
if (!parse_peer_ip(peer_ip, &family, bytes))
|
||||||
|
return 0;
|
||||||
|
uint64_t hash = 14695981039346656037ULL ^ (uint64_t)(uint32_t)family;
|
||||||
|
size_t length = family == AF_INET ? 4 : 16;
|
||||||
|
for (size_t i = 0; i < length; i++) {
|
||||||
|
hash ^= bytes[i];
|
||||||
|
hash *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
if (hash == 0)
|
||||||
|
hash = 0x9e3779b97f4a7c15ULL;
|
||||||
|
if (ok)
|
||||||
|
*ok = true;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find the bucket holding `peer_ip`, or -1 when it has no entry. */
|
||||||
|
static int host_lookup(DaemonLimitRegistry* registry, const char* peer_ip) {
|
||||||
|
bool ok = false;
|
||||||
|
uint64_t key = daemon_limits_host_hash(peer_ip, &ok);
|
||||||
|
if (!ok)
|
||||||
|
return -1;
|
||||||
|
size_t mask = (size_t)registry->host_slots - 1;
|
||||||
|
size_t start = (size_t)(key & mask);
|
||||||
|
for (size_t i = 0; i < (size_t)registry->host_slots; i++) {
|
||||||
|
size_t idx = (start + i) & mask;
|
||||||
|
uint64_t current = atomic_load_explicit(®istry->host_key[idx], memory_order_acquire);
|
||||||
|
if (current == key)
|
||||||
|
return (int)idx;
|
||||||
|
if (current == 0)
|
||||||
|
return -1; /* no tombstones: an empty bucket ends the probe chain */
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find or insert the bucket for `peer_ip`. Insertion is a lock-free CAS so two
|
||||||
|
* forked children racing on the same source converge on one bucket. Returns -1
|
||||||
|
* when the table is full or the address is unparseable (callers fail open: the
|
||||||
|
* global/module caps and ACLs still apply). */
|
||||||
|
static int host_intern(DaemonLimitRegistry* registry, const char* peer_ip) {
|
||||||
|
bool ok = false;
|
||||||
|
uint64_t key = daemon_limits_host_hash(peer_ip, &ok);
|
||||||
|
if (!ok)
|
||||||
|
return -1;
|
||||||
|
size_t mask = (size_t)registry->host_slots - 1;
|
||||||
|
size_t start = (size_t)(key & mask);
|
||||||
|
for (size_t i = 0; i < (size_t)registry->host_slots; i++) {
|
||||||
|
size_t idx = (start + i) & mask;
|
||||||
|
uint64_t current = atomic_load_explicit(®istry->host_key[idx], memory_order_acquire);
|
||||||
|
if (current == key)
|
||||||
|
return (int)idx;
|
||||||
|
if (current == 0) {
|
||||||
|
uint64_t expected = 0;
|
||||||
|
if (atomic_compare_exchange_strong_explicit(®istry->host_key[idx], &expected, key,
|
||||||
|
memory_order_acq_rel, memory_order_acquire))
|
||||||
|
return (int)idx;
|
||||||
|
if (atomic_load_explicit(®istry->host_key[idx], memory_order_acquire) == key)
|
||||||
|
return (int)idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonLimitRegistry* daemon_limits_create(int max_slots, int module_count, int per_host_cap,
|
||||||
|
int lockout_threshold, int lockout_duration_sec) {
|
||||||
|
if (max_slots < DAEMON_LIMITS_MIN_SLOTS)
|
||||||
|
max_slots = DAEMON_LIMITS_MIN_SLOTS;
|
||||||
|
if (max_slots > DAEMON_LIMITS_MAX_SLOTS)
|
||||||
|
max_slots = DAEMON_LIMITS_MAX_SLOTS;
|
||||||
|
if (module_count < 1)
|
||||||
|
module_count = 1;
|
||||||
|
if (per_host_cap < 0)
|
||||||
|
per_host_cap = 0;
|
||||||
|
if (lockout_threshold < 0)
|
||||||
|
lockout_threshold = 0;
|
||||||
|
if (lockout_duration_sec < 0)
|
||||||
|
lockout_duration_sec = 0;
|
||||||
|
|
||||||
|
bool need_hosts = per_host_cap > 0 || (lockout_threshold > 0 && lockout_duration_sec > 0);
|
||||||
|
int host_slots = 1;
|
||||||
|
if (need_hosts) {
|
||||||
|
size_t want = (size_t)max_slots * 4;
|
||||||
|
if (want < 64)
|
||||||
|
want = 64;
|
||||||
|
if (want > DAEMON_LIMITS_MAX_HOST_SLOTS)
|
||||||
|
want = DAEMON_LIMITS_MAX_HOST_SLOTS;
|
||||||
|
host_slots = (int)next_pow2(want);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t header = round_up(sizeof(DaemonLimitRegistry), 16);
|
||||||
|
size_t slot_bytes =
|
||||||
|
round_up((size_t)max_slots * sizeof(_Atomic int), 16) * 4; /* state,pid,module,host */
|
||||||
|
size_t module_bytes = round_up((size_t)module_count * sizeof(_Atomic int), 16);
|
||||||
|
size_t host_key_bytes = round_up((size_t)host_slots * sizeof(_Atomic uint64_t), 16);
|
||||||
|
size_t host_int_bytes = round_up((size_t)host_slots * sizeof(_Atomic int), 16) * 2;
|
||||||
|
size_t host_until_bytes = round_up((size_t)host_slots * sizeof(_Atomic long long), 16);
|
||||||
|
size_t total =
|
||||||
|
header + slot_bytes + module_bytes + host_key_bytes + host_int_bytes + host_until_bytes + 16;
|
||||||
|
|
||||||
|
void* map = mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
|
||||||
|
if (map == MAP_FAILED)
|
||||||
|
return NULL;
|
||||||
|
memset(map, 0, total);
|
||||||
|
|
||||||
|
DaemonLimitRegistry* registry = (DaemonLimitRegistry*)map;
|
||||||
|
registry->max_slots = max_slots;
|
||||||
|
registry->module_count = module_count;
|
||||||
|
registry->host_slots = host_slots;
|
||||||
|
registry->per_host_cap = per_host_cap;
|
||||||
|
registry->lockout_threshold = lockout_threshold;
|
||||||
|
registry->lockout_duration_sec = lockout_duration_sec;
|
||||||
|
registry->map_size = total;
|
||||||
|
|
||||||
|
unsigned char* cursor = (unsigned char*)map + header;
|
||||||
|
registry->slot_state = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)max_slots * sizeof(_Atomic int);
|
||||||
|
registry->slot_pid = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)max_slots * sizeof(_Atomic int);
|
||||||
|
registry->slot_module = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)max_slots * sizeof(_Atomic int);
|
||||||
|
registry->slot_host = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)max_slots * sizeof(_Atomic int);
|
||||||
|
registry->module_active = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)module_count * sizeof(_Atomic int);
|
||||||
|
cursor = (unsigned char*)round_up((size_t)(uintptr_t)cursor, 16);
|
||||||
|
registry->host_key = (_Atomic uint64_t*)cursor;
|
||||||
|
cursor += (size_t)host_slots * sizeof(_Atomic uint64_t);
|
||||||
|
registry->host_active = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)host_slots * sizeof(_Atomic int);
|
||||||
|
registry->host_fail = (atomic_int*)cursor;
|
||||||
|
cursor += (size_t)host_slots * sizeof(_Atomic int);
|
||||||
|
cursor = (unsigned char*)round_up((size_t)(uintptr_t)cursor, 16);
|
||||||
|
registry->host_until = (atomic_llong*)cursor;
|
||||||
|
|
||||||
|
for (int i = 0; i < max_slots; i++) {
|
||||||
|
atomic_store(®istry->slot_module[i], -1);
|
||||||
|
atomic_store(®istry->slot_host[i], -1);
|
||||||
|
}
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void daemon_limits_destroy(DaemonLimitRegistry* registry) {
|
||||||
|
if (!registry)
|
||||||
|
return;
|
||||||
|
munmap(registry, registry->map_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int daemon_limits_claim_slot(DaemonLimitRegistry* registry) {
|
||||||
|
if (!registry)
|
||||||
|
return DAEMON_LIMITS_NO_SLOT;
|
||||||
|
for (int i = 0; i < registry->max_slots; i++) {
|
||||||
|
int expected = SLOT_FREE;
|
||||||
|
if (atomic_compare_exchange_strong(®istry->slot_state[i], &expected, SLOT_CLAIMED)) {
|
||||||
|
atomic_store(®istry->slot_pid[i], 0);
|
||||||
|
atomic_store(®istry->slot_module[i], -1);
|
||||||
|
atomic_store(®istry->slot_host[i], -1);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DAEMON_LIMITS_NO_SLOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void daemon_limits_set_slot_pid(DaemonLimitRegistry* registry, int slot, long pid) {
|
||||||
|
if (!registry || slot < 0 || slot >= registry->max_slots)
|
||||||
|
return;
|
||||||
|
atomic_store(®istry->slot_pid[slot], (int)pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void daemon_limits_reclaim_slot(DaemonLimitRegistry* registry, int slot) {
|
||||||
|
if (!registry || slot < 0 || slot >= registry->max_slots)
|
||||||
|
return;
|
||||||
|
int previous =
|
||||||
|
atomic_exchange_explicit(®istry->slot_state[slot], SLOT_FREE, memory_order_acq_rel);
|
||||||
|
if (previous == SLOT_REGISTERED) {
|
||||||
|
int module = atomic_load(®istry->slot_module[slot]);
|
||||||
|
int host = atomic_load(®istry->slot_host[slot]);
|
||||||
|
if (module >= 0 && module < registry->module_count) {
|
||||||
|
int current = atomic_load(®istry->module_active[module]);
|
||||||
|
while (current > 0 &&
|
||||||
|
!atomic_compare_exchange_weak(®istry->module_active[module], ¤t, current - 1))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
if (host >= 0 && host < registry->host_slots) {
|
||||||
|
int current = atomic_load(®istry->host_active[host]);
|
||||||
|
while (current > 0 &&
|
||||||
|
!atomic_compare_exchange_weak(®istry->host_active[host], ¤t, current - 1))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atomic_store(®istry->slot_pid[slot], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void daemon_limits_reclaim_pid(DaemonLimitRegistry* registry, long pid) {
|
||||||
|
if (!registry || pid <= 0)
|
||||||
|
return;
|
||||||
|
for (int i = 0; i < registry->max_slots; i++) {
|
||||||
|
if (atomic_load(®istry->slot_state[i]) == SLOT_FREE)
|
||||||
|
continue;
|
||||||
|
if (atomic_load(®istry->slot_pid[i]) == (int)pid) {
|
||||||
|
daemon_limits_reclaim_slot(registry, i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonLimitResult daemon_limits_register(DaemonLimitRegistry* registry, int slot, int module_index,
|
||||||
|
const char* peer_ip, int module_cap) {
|
||||||
|
if (!registry || slot < 0 || slot >= registry->max_slots)
|
||||||
|
return DAEMON_LIMIT_UNAVAILABLE;
|
||||||
|
if (module_index < 0 || module_index >= registry->module_count)
|
||||||
|
return DAEMON_LIMIT_UNAVAILABLE;
|
||||||
|
if (atomic_load_explicit(®istry->slot_state[slot], memory_order_acquire) != SLOT_CLAIMED)
|
||||||
|
return DAEMON_LIMIT_UNAVAILABLE;
|
||||||
|
|
||||||
|
int host = -1;
|
||||||
|
if (registry->per_host_cap > 0 || registry->lockout_threshold > 0)
|
||||||
|
host = host_intern(registry, peer_ip);
|
||||||
|
|
||||||
|
int module_count = atomic_fetch_add(®istry->module_active[module_index], 1) + 1;
|
||||||
|
if (module_cap > 0 && module_count > module_cap) {
|
||||||
|
atomic_fetch_sub(®istry->module_active[module_index], 1);
|
||||||
|
return DAEMON_LIMIT_MODULE_FULL;
|
||||||
|
}
|
||||||
|
if (host >= 0) {
|
||||||
|
int host_count = atomic_fetch_add(®istry->host_active[host], 1) + 1;
|
||||||
|
if (registry->per_host_cap > 0 && host_count > registry->per_host_cap) {
|
||||||
|
atomic_fetch_sub(®istry->host_active[host], 1);
|
||||||
|
atomic_fetch_sub(®istry->module_active[module_index], 1);
|
||||||
|
return DAEMON_LIMIT_HOST_FULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atomic_store(®istry->slot_module[slot], module_index);
|
||||||
|
atomic_store(®istry->slot_host[slot], host);
|
||||||
|
atomic_store_explicit(®istry->slot_state[slot], SLOT_REGISTERED, memory_order_release);
|
||||||
|
return DAEMON_LIMIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool daemon_limits_auth_locked(DaemonLimitRegistry* registry, const char* peer_ip,
|
||||||
|
int* seconds_remaining) {
|
||||||
|
if (!registry || registry->lockout_threshold <= 0 || registry->lockout_duration_sec <= 0)
|
||||||
|
return false;
|
||||||
|
int bucket = host_lookup(registry, peer_ip);
|
||||||
|
if (bucket < 0)
|
||||||
|
return false;
|
||||||
|
long long until = atomic_load(®istry->host_until[bucket]);
|
||||||
|
long long now = (long long)time(NULL);
|
||||||
|
if (until > now) {
|
||||||
|
if (seconds_remaining)
|
||||||
|
*seconds_remaining = (int)(until - now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (until != 0) {
|
||||||
|
/* The previous lockout has expired: clear the stale counter so the source
|
||||||
|
* gets a fresh allowance. */
|
||||||
|
atomic_store(®istry->host_fail[bucket], 0);
|
||||||
|
atomic_store(®istry->host_until[bucket], 0);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void daemon_limits_auth_record_failure(DaemonLimitRegistry* registry, const char* peer_ip) {
|
||||||
|
if (!registry || registry->lockout_threshold <= 0 || registry->lockout_duration_sec <= 0)
|
||||||
|
return;
|
||||||
|
int bucket = host_intern(registry, peer_ip);
|
||||||
|
if (bucket < 0)
|
||||||
|
return;
|
||||||
|
int failures = atomic_fetch_add(®istry->host_fail[bucket], 1) + 1;
|
||||||
|
if (failures >= registry->lockout_threshold) {
|
||||||
|
long long now = (long long)time(NULL);
|
||||||
|
atomic_store(®istry->host_until[bucket], now + (long long)registry->lockout_duration_sec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void daemon_limits_auth_record_success(DaemonLimitRegistry* registry, const char* peer_ip) {
|
||||||
|
if (!registry)
|
||||||
|
return;
|
||||||
|
int bucket = host_lookup(registry, peer_ip);
|
||||||
|
if (bucket < 0)
|
||||||
|
return;
|
||||||
|
atomic_store(®istry->host_fail[bucket], 0);
|
||||||
|
atomic_store(®istry->host_until[bucket], 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#ifndef DAEMON_LIMITS_H
|
||||||
|
#define DAEMON_LIMITS_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Cross-process daemon connection registry.
|
||||||
|
*
|
||||||
|
* The daemon listener forks ONE child per accepted connection, so any
|
||||||
|
* per-module / per-source accounting must live in state shared across the
|
||||||
|
* forked children. This module owns a fixed-size registry carved out of an
|
||||||
|
* anonymous shared mapping (mmap(MAP_SHARED | MAP_ANONYMOUS)) created by the
|
||||||
|
* accept-loop PARENT before it forks; every child inherits the mapping (and the
|
||||||
|
* pointer to it) across fork().
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - ONLY C11 atomics (atomic_*); never mtx_t/pthread locks, which can deadlock
|
||||||
|
* in a forked child if another thread held them at fork time.
|
||||||
|
* - No heap allocation after fork: the mapping is fixed-size and all access is
|
||||||
|
* atomic load/store/CAS over preallocated arrays.
|
||||||
|
*
|
||||||
|
* Slot lifecycle (the parent reclaims even when a child is SIGKILLed):
|
||||||
|
* FREE --(parent claim_slot)--> CLAIMED
|
||||||
|
* CLAIMED --(child register)--> REGISTERED
|
||||||
|
* any --(parent reclaim)--> FREE
|
||||||
|
* The child records its module index and per-source bucket into the slot before
|
||||||
|
* publishing REGISTERED; the parent's SIGCHLD handler matches the reaped pid to
|
||||||
|
* the slot and, when REGISTERED, decrements the module/per-source counters.
|
||||||
|
* A child killed before registering holds no counts, so reclaiming a CLAIMED
|
||||||
|
* slot only frees the slot.
|
||||||
|
*
|
||||||
|
* Per-source identity is the normalized numeric peer IP (IPv4-mapped IPv6 is
|
||||||
|
* already collapsed to IPv4 by utils_fd_peer_ip); it is interned into an
|
||||||
|
* open-addressed, linear-probing table keyed by a 64-bit hash. The same table
|
||||||
|
* also carries the cross-process auth-failure counter and lockout deadline.
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct DaemonLimitRegistry DaemonLimitRegistry;
|
||||||
|
|
||||||
|
/* Result of a per-connection admission check. */
|
||||||
|
typedef enum {
|
||||||
|
DAEMON_LIMIT_OK = 0, /* admitted; slot is now REGISTERED */
|
||||||
|
DAEMON_LIMIT_MODULE_FULL, /* module's `max connections` cap reached */
|
||||||
|
DAEMON_LIMIT_HOST_FULL, /* global `max connections per host` cap reached */
|
||||||
|
DAEMON_LIMIT_UNAVAILABLE, /* registry/slot unusable (caller fails open) */
|
||||||
|
} DaemonLimitResult;
|
||||||
|
|
||||||
|
/* Bounds for registry sizing. A slot is one concurrently live child. */
|
||||||
|
#define DAEMON_LIMITS_MIN_SLOTS 16
|
||||||
|
#define DAEMON_LIMITS_MAX_SLOTS 65536
|
||||||
|
#define DAEMON_LIMITS_MAX_HOST_SLOTS 65536
|
||||||
|
#define DAEMON_LIMITS_NO_SLOT (-1)
|
||||||
|
|
||||||
|
/* Create the shared registry in the calling (parent) process. `max_slots` is
|
||||||
|
* the number of concurrently live children to track (clamped to
|
||||||
|
* [DAEMON_LIMITS_MIN_SLOTS, DAEMON_LIMITS_MAX_SLOTS]); `module_count` is the
|
||||||
|
* number of daemon modules (clamped to >= 1); `per_host_cap` and the lockout
|
||||||
|
* pair come from the daemon config (0 disables). Returns NULL on failure (e.g.
|
||||||
|
* mmap allocation); callers must degrade gracefully (global cap + ACLs still
|
||||||
|
* apply). */
|
||||||
|
DaemonLimitRegistry* daemon_limits_create(int max_slots, int module_count, int per_host_cap,
|
||||||
|
int lockout_threshold, int lockout_duration_sec);
|
||||||
|
|
||||||
|
/* Unmap the registry. Only the creating process may call this. */
|
||||||
|
void daemon_limits_destroy(DaemonLimitRegistry* registry);
|
||||||
|
|
||||||
|
/* Parent side: reserve a slot for the next fork. Returns the slot index or
|
||||||
|
* DAEMON_LIMITS_NO_SLOT when every slot is in use. */
|
||||||
|
int daemon_limits_claim_slot(DaemonLimitRegistry* registry);
|
||||||
|
/* Parent side: record the forked child's pid in a claimed slot. */
|
||||||
|
void daemon_limits_set_slot_pid(DaemonLimitRegistry* registry, int slot, long pid);
|
||||||
|
/* Parent side: release a slot, decrementing the module/per-source counters when
|
||||||
|
* the slot was actually REGISTERED. Idempotent. */
|
||||||
|
void daemon_limits_reclaim_slot(DaemonLimitRegistry* registry, int slot);
|
||||||
|
/* Parent SIGCHLD side: reclaim the slot owned by `pid` (no-op when not found). */
|
||||||
|
void daemon_limits_reclaim_pid(DaemonLimitRegistry* registry, long pid);
|
||||||
|
|
||||||
|
/* Child side: admit the connection for `module_index` from `peer_ip`. Always
|
||||||
|
* tracks the module/per-source occupancy (so the parent's reclaim is
|
||||||
|
* symmetric); when `module_cap` > 0 it additionally enforces the per-module
|
||||||
|
* cap. Returns DAEMON_LIMIT_OK and publishes the slot, or a refusal reason. */
|
||||||
|
DaemonLimitResult daemon_limits_register(DaemonLimitRegistry* registry, int slot, int module_index,
|
||||||
|
const char* peer_ip, int module_cap);
|
||||||
|
|
||||||
|
/* Child side: true when `peer_ip` is currently locked out after too many failed
|
||||||
|
* authentications. `seconds_remaining` may be NULL. */
|
||||||
|
bool daemon_limits_auth_locked(DaemonLimitRegistry* registry, const char* peer_ip,
|
||||||
|
int* seconds_remaining);
|
||||||
|
/* Child side: count one failed authentication for `peer_ip`; once the threshold
|
||||||
|
* is reached the source is locked out for the configured duration. */
|
||||||
|
void daemon_limits_auth_record_failure(DaemonLimitRegistry* registry, const char* peer_ip);
|
||||||
|
/* Child side: clear the failure counter/lockout for a source that authenticated
|
||||||
|
* successfully (no-op when the source has no table entry). */
|
||||||
|
void daemon_limits_auth_record_success(DaemonLimitRegistry* registry, const char* peer_ip);
|
||||||
|
|
||||||
|
/* Pure helper: 64-bit FNV-1a hash of a numeric peer IP plus its family, used to
|
||||||
|
* index the per-source table. *ok is set false (and 0 returned) for a NULL or
|
||||||
|
* non-numeric address. Exposed for unit testing. */
|
||||||
|
uint64_t daemon_limits_host_hash(const char* peer_ip, bool* ok);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "test_credentials.h"
|
#include "test_credentials.h"
|
||||||
#include "test_data.h"
|
#include "test_data.h"
|
||||||
#include "test_daemon_conf.h"
|
#include "test_daemon_conf.h"
|
||||||
|
#include "test_daemon_limits.h"
|
||||||
#include "test_delay_updates.h"
|
#include "test_delay_updates.h"
|
||||||
#include "test_delta.h"
|
#include "test_delta.h"
|
||||||
#include "test_file.h"
|
#include "test_file.h"
|
||||||
@@ -85,6 +86,7 @@ int main() {
|
|||||||
RUN_TEST(test_client_cli);
|
RUN_TEST(test_client_cli);
|
||||||
RUN_TEST(test_server);
|
RUN_TEST(test_server);
|
||||||
RUN_TEST(test_daemon_conf);
|
RUN_TEST(test_daemon_conf);
|
||||||
|
RUN_TEST(test_daemon_limits);
|
||||||
RUN_TEST(test_motd);
|
RUN_TEST(test_motd);
|
||||||
RUN_TEST(test_server_cli);
|
RUN_TEST(test_server_cli);
|
||||||
RUN_TEST(test_fuzz_smoke);
|
RUN_TEST(test_fuzz_smoke);
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
#include "test_daemon_limits.h"
|
||||||
|
#include "daemon_limits.h"
|
||||||
|
#include "test_utils.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* The per-source hash is a pure helper: numeric addresses hash to a nonzero,
|
||||||
|
* stable value and unparseable input reports failure. */
|
||||||
|
static void test_daemon_limits_host_hash() {
|
||||||
|
bool ok = false;
|
||||||
|
uint64_t v4 = daemon_limits_host_hash("127.0.0.1", &ok);
|
||||||
|
EXPECT_TRUE(ok);
|
||||||
|
EXPECT_TRUE(v4 != 0);
|
||||||
|
EXPECT_EQ_INT((int)(daemon_limits_host_hash("127.0.0.1", NULL) == v4), 1);
|
||||||
|
|
||||||
|
bool ok6 = false;
|
||||||
|
uint64_t v6 = daemon_limits_host_hash("2001:db8::1", &ok6);
|
||||||
|
EXPECT_TRUE(ok6);
|
||||||
|
EXPECT_TRUE(v6 != 0);
|
||||||
|
/* Distinct textual forms of different addresses must differ. */
|
||||||
|
EXPECT_TRUE(v4 != v6);
|
||||||
|
|
||||||
|
bool bad = true;
|
||||||
|
EXPECT_TRUE(daemon_limits_host_hash("not-an-ip", &bad) == 0);
|
||||||
|
EXPECT_FALSE(bad);
|
||||||
|
bad = true;
|
||||||
|
EXPECT_TRUE(daemon_limits_host_hash(NULL, &bad) == 0);
|
||||||
|
EXPECT_FALSE(bad);
|
||||||
|
bad = true;
|
||||||
|
EXPECT_TRUE(daemon_limits_host_hash("", &bad) == 0);
|
||||||
|
EXPECT_FALSE(bad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slot reservation is a plain parent-side resource: claim until exhausted,
|
||||||
|
* reclaim, then claim again. */
|
||||||
|
static void test_daemon_limits_slots() {
|
||||||
|
DaemonLimitRegistry* registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 2, 0, 0, 0);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
int slots[DAEMON_LIMITS_MIN_SLOTS];
|
||||||
|
for (int i = 0; i < DAEMON_LIMITS_MIN_SLOTS; i++) {
|
||||||
|
slots[i] = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_EQ_INT(slots[i], i);
|
||||||
|
}
|
||||||
|
EXPECT_EQ_INT(daemon_limits_claim_slot(registry), DAEMON_LIMITS_NO_SLOT);
|
||||||
|
daemon_limits_reclaim_slot(registry, slots[3]);
|
||||||
|
int reclaimed = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_EQ_INT(reclaimed, slots[3]);
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-module accounting: the cap is enforced across slots and a reclaimed slot
|
||||||
|
* frees a module count. */
|
||||||
|
static void test_daemon_limits_module_cap() {
|
||||||
|
DaemonLimitRegistry* registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 2, 0, 0, 0);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
|
||||||
|
int slot0 = daemon_limits_claim_slot(registry);
|
||||||
|
int slot1 = daemon_limits_claim_slot(registry);
|
||||||
|
int slot2 = daemon_limits_claim_slot(registry);
|
||||||
|
int slot3 = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_TRUE(slot0 >= 0 && slot1 >= 0 && slot2 >= 0 && slot3 >= 0);
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot0, 0, "10.0.0.1", 2), DAEMON_LIMIT_OK);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.2", 2), DAEMON_LIMIT_OK);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot2, 0, "10.0.0.3", 2),
|
||||||
|
DAEMON_LIMIT_MODULE_FULL);
|
||||||
|
/* A different module has its own counter. */
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot2, 1, "10.0.0.3", 2), DAEMON_LIMIT_OK);
|
||||||
|
/* A module cap of 0 is unlimited. */
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot3, 0, "10.0.0.3", 0), DAEMON_LIMIT_OK);
|
||||||
|
|
||||||
|
daemon_limits_reclaim_slot(registry, slot0);
|
||||||
|
daemon_limits_reclaim_slot(registry, slot1);
|
||||||
|
int slot4 = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_TRUE(slot4 >= 0);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot4, 0, "10.0.0.4", 2), DAEMON_LIMIT_OK);
|
||||||
|
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-source accounting: the same peer hits the cap, a different peer does not. */
|
||||||
|
static void test_daemon_limits_host_cap() {
|
||||||
|
DaemonLimitRegistry* registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 1, 1, 0, 0);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
|
||||||
|
int slot0 = daemon_limits_claim_slot(registry);
|
||||||
|
int slot1 = daemon_limits_claim_slot(registry);
|
||||||
|
int slot2 = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_TRUE(slot0 >= 0 && slot1 >= 0 && slot2 >= 0);
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot0, 0, "10.0.0.1", 0), DAEMON_LIMIT_OK);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.1", 0), DAEMON_LIMIT_HOST_FULL);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot2, 0, "10.0.0.2", 0), DAEMON_LIMIT_OK);
|
||||||
|
/* Reclaiming the first source frees its per-host allowance. */
|
||||||
|
daemon_limits_reclaim_slot(registry, slot0);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.1", 0), DAEMON_LIMIT_OK);
|
||||||
|
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The pid-indexed reclaim is what the parent's SIGCHLD handler uses: a dead
|
||||||
|
* child's module/source counts must be released. */
|
||||||
|
static void test_daemon_limits_reclaim_pid() {
|
||||||
|
DaemonLimitRegistry* registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 1, 1, 0, 0);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
|
||||||
|
int slot0 = daemon_limits_claim_slot(registry);
|
||||||
|
int slot1 = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_TRUE(slot0 >= 0 && slot1 >= 0);
|
||||||
|
daemon_limits_set_slot_pid(registry, slot0, 4242);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot0, 0, "10.0.0.1", 1), DAEMON_LIMIT_OK);
|
||||||
|
/* Cap (module 1) and per-host (1) are both saturated. */
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.1", 1),
|
||||||
|
DAEMON_LIMIT_MODULE_FULL);
|
||||||
|
|
||||||
|
daemon_limits_reclaim_pid(registry, 4242);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.1", 1), DAEMON_LIMIT_OK);
|
||||||
|
/* Reclaiming an unknown pid is a no-op. */
|
||||||
|
daemon_limits_reclaim_pid(registry, 999999);
|
||||||
|
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cross-process lockout: failures counted in the shared mapping lock the source
|
||||||
|
* out after the threshold; a success clears it; threshold 0 disables it. */
|
||||||
|
static void test_daemon_limits_auth_lockout() {
|
||||||
|
DaemonLimitRegistry* registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 1, 0, 2, 300);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
|
||||||
|
int remaining = 0;
|
||||||
|
EXPECT_FALSE(daemon_limits_auth_locked(registry, "10.0.0.1", &remaining));
|
||||||
|
daemon_limits_auth_record_failure(registry, "10.0.0.1");
|
||||||
|
EXPECT_FALSE(daemon_limits_auth_locked(registry, "10.0.0.1", &remaining));
|
||||||
|
daemon_limits_auth_record_failure(registry, "10.0.0.1");
|
||||||
|
EXPECT_TRUE(daemon_limits_auth_locked(registry, "10.0.0.1", &remaining));
|
||||||
|
EXPECT_TRUE(remaining > 0 && remaining <= 300);
|
||||||
|
/* Another source is unaffected. */
|
||||||
|
EXPECT_FALSE(daemon_limits_auth_locked(registry, "10.0.0.2", &remaining));
|
||||||
|
/* A successful authentication clears the lockout. */
|
||||||
|
daemon_limits_auth_record_success(registry, "10.0.0.1");
|
||||||
|
EXPECT_FALSE(daemon_limits_auth_locked(registry, "10.0.0.1", &remaining));
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
|
||||||
|
/* threshold 0 disables the lockout entirely. */
|
||||||
|
registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 1, 0, 0, 300);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
daemon_limits_auth_record_failure(registry, "10.0.0.1");
|
||||||
|
EXPECT_FALSE(daemon_limits_auth_locked(registry, "10.0.0.1", &remaining));
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The registry must be visible across fork(): a child's registration is seen by
|
||||||
|
* the parent, and the parent's pid reclaim releases it. */
|
||||||
|
static void test_daemon_limits_fork_shared() {
|
||||||
|
if (is_running_under_valgrind())
|
||||||
|
return; /* fork + shared mapping is slow/noisy under valgrind */
|
||||||
|
DaemonLimitRegistry* registry = daemon_limits_create(DAEMON_LIMITS_MIN_SLOTS, 1, 0, 0, 0);
|
||||||
|
EXPECT_NOT_NULL(registry);
|
||||||
|
|
||||||
|
int slot0 = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_TRUE(slot0 >= 0);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
if (daemon_limits_register(registry, slot0, 0, "10.0.0.1", 1) != DAEMON_LIMIT_OK)
|
||||||
|
_exit(1);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(pid > 0);
|
||||||
|
daemon_limits_set_slot_pid(registry, slot0, (long)pid);
|
||||||
|
int status = 0;
|
||||||
|
EXPECT_TRUE(waitpid(pid, &status, 0) == pid);
|
||||||
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||||
|
/* The child's module count is still held in the shared mapping. */
|
||||||
|
int slot1 = daemon_limits_claim_slot(registry);
|
||||||
|
EXPECT_TRUE(slot1 >= 0);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.2", 1),
|
||||||
|
DAEMON_LIMIT_MODULE_FULL);
|
||||||
|
/* The parent reclaims the dead child's slot by pid. */
|
||||||
|
daemon_limits_reclaim_pid(registry, (long)pid);
|
||||||
|
EXPECT_EQ_INT(daemon_limits_register(registry, slot1, 0, "10.0.0.2", 1), DAEMON_LIMIT_OK);
|
||||||
|
daemon_limits_destroy(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_daemon_limits() {
|
||||||
|
test_daemon_limits_host_hash();
|
||||||
|
test_daemon_limits_slots();
|
||||||
|
test_daemon_limits_module_cap();
|
||||||
|
test_daemon_limits_host_cap();
|
||||||
|
test_daemon_limits_reclaim_pid();
|
||||||
|
test_daemon_limits_auth_lockout();
|
||||||
|
test_daemon_limits_fork_shared();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef TEST_DAEMON_LIMITS_H
|
||||||
|
#define TEST_DAEMON_LIMITS_H
|
||||||
|
|
||||||
|
void test_daemon_limits();
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user