Release v2.26.0 #284

Merged
TapTap merged 210 commits from dev into main 2026-09-18 19:05:52 +02:00
6 changed files with 662 additions and 0 deletions
Showing only changes of commit 5334397b81 - Show all commits
+2
View File
@@ -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
+356
View File
@@ -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(&registry->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(&registry->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(&registry->host_key[idx], &expected, key,
memory_order_acq_rel, memory_order_acquire))
return (int)idx;
if (atomic_load_explicit(&registry->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(&registry->slot_module[i], -1);
atomic_store(&registry->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(&registry->slot_state[i], &expected, SLOT_CLAIMED)) {
atomic_store(&registry->slot_pid[i], 0);
atomic_store(&registry->slot_module[i], -1);
atomic_store(&registry->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(&registry->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(&registry->slot_state[slot], SLOT_FREE, memory_order_acq_rel);
if (previous == SLOT_REGISTERED) {
int module = atomic_load(&registry->slot_module[slot]);
int host = atomic_load(&registry->slot_host[slot]);
if (module >= 0 && module < registry->module_count) {
int current = atomic_load(&registry->module_active[module]);
while (current > 0 &&
!atomic_compare_exchange_weak(&registry->module_active[module], &current, current - 1))
;
}
if (host >= 0 && host < registry->host_slots) {
int current = atomic_load(&registry->host_active[host]);
while (current > 0 &&
!atomic_compare_exchange_weak(&registry->host_active[host], &current, current - 1))
;
}
}
atomic_store(&registry->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(&registry->slot_state[i]) == SLOT_FREE)
continue;
if (atomic_load(&registry->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(&registry->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(&registry->module_active[module_index], 1) + 1;
if (module_cap > 0 && module_count > module_cap) {
atomic_fetch_sub(&registry->module_active[module_index], 1);
return DAEMON_LIMIT_MODULE_FULL;
}
if (host >= 0) {
int host_count = atomic_fetch_add(&registry->host_active[host], 1) + 1;
if (registry->per_host_cap > 0 && host_count > registry->per_host_cap) {
atomic_fetch_sub(&registry->host_active[host], 1);
atomic_fetch_sub(&registry->module_active[module_index], 1);
return DAEMON_LIMIT_HOST_FULL;
}
}
atomic_store(&registry->slot_module[slot], module_index);
atomic_store(&registry->slot_host[slot], host);
atomic_store_explicit(&registry->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(&registry->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(&registry->host_fail[bucket], 0);
atomic_store(&registry->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(&registry->host_fail[bucket], 1) + 1;
if (failures >= registry->lockout_threshold) {
long long now = (long long)time(NULL);
atomic_store(&registry->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(&registry->host_fail[bucket], 0);
atomic_store(&registry->host_until[bucket], 0);
}
+102
View File
@@ -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
+2
View File
@@ -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);
+194
View File
@@ -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();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_DAEMON_LIMITS_H
#define TEST_DAEMON_LIMITS_H
void test_daemon_limits();
#endif