Test NAT boundary validation, hash collisions, expiry and allocation failures

This commit is contained in:
evgeny committed 2026-10-21 22:34:06 +02:00
1 parent cecc5d7d1f
commit cfc0051088
2 files changed
+183

No files matched your search

+3
View File
@@ -520,6 +520,9 @@ test_stun_nat_LDFLAGS = $(test_clock_ldflags)
test_nat_engine_SOURCES = test_nat_engine.c
test_nat_engine_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_nat_engine_LDADD = $(LIBUTUN) $(COMMON_LIBS)
if OS_LINUX
test_nat_engine_LDFLAGS = -Wl,--wrap=u_calloc_impl
endif
test_nat_transport_SOURCES = test_nat_transport.c
test_nat_transport_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
+180
View File
@@ -12,10 +12,20 @@
#include <stdlib.h>
#include <string.h>
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../src/eim_nat.h"
#include "etcp.h"
#include "../src/config_parser.h"
#ifdef __linux__
static unsigned int calloc_fail_at, calloc_calls;
void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* location) {
if (calloc_fail_at && ++calloc_calls == calloc_fail_at) return NULL;
return __real_u_calloc_impl(count, size, location);
}
#endif
#ifdef ENABLE_STATIC_ASSERT
static_assert(sizeof(struct eim_nat_entry) <= 64, "entry size ok");
#endif
@@ -1041,6 +1051,173 @@ static void test_static_forward_owners(void) {
PASS();
}
/* Независимая проверка взаимного соответствия индексов, отсутствия циклов и полного покрытия свободного пула. */
static int check_indices(const struct eim_nat_ctx* ctx) {
uint8_t seen[EIM_NAT_TABLE_SIZE] = {0};
size_t indexed = 0, occupied = 0, free_ports = 0;
for (uint32_t bucket = 0; bucket < EIM_NAT_TABLE_SIZE; bucket++) {
for (uint16_t port = ctx->buckets[bucket]; port; port = ctx->table[port].hash_next) {
if (seen[port] || ctx->table[port].state == EIM_NAT_ENTRY_FREE) return 0;
seen[port] = 1; indexed++;
}
}
uint16_t previous = 0;
for (uint16_t port = ctx->next_port; port; port = ctx->table[port].free_next) {
if (seen[port] || port < ctx->port_start || port > ctx->port_end || ctx->table[port].free_prev != previous ||
ctx->table[port].state != EIM_NAT_ENTRY_FREE) return 0;
seen[port] = 2; free_ports++; previous = port;
}
for (uint32_t port = 1; port < EIM_NAT_TABLE_SIZE; port++) {
if (ctx->table[port].state != EIM_NAT_ENTRY_FREE) { if (seen[port] != 1) return 0; occupied++; }
else if (port >= ctx->port_start && port <= ctx->port_end && seen[port] != 2) return 0;
}
return indexed == occupied && free_ports == ctx->free_count && previous == ctx->free_tail;
}
/* Некорректные пакеты не меняют ни одного байта и не расходуют порты; проверяются оба направления и alignment=1. */
static void test_packet_boundaries(void) {
TEST("packet_validation_atomicity_and_alignment");
struct global_config g = make_global_config();
struct eim_nat_ctx ctx;
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "validation context initialized");
for (unsigned int i = 0; i < 11; i++) {
uint8_t* original = i == 7 || i == 8 ? make_tcp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_GW_HOST, TEST_PORT_START) :
make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_GW_HOST, TEST_PORT_START);
size_t len = i == 7 || i == 8 ? 54 : 42;
int expected = EIM_NAT_ERROR;
if (i == 0) original[0] = 0x65;
if (i == 1 || i == 2) { uint16_t total = htobe16(i == 1 ? 20 : 100); memcpy(original + 2, &total, 2); }
if (i == 3) original[0] = 0x4f;
if (i == 4 || i == 5) { uint16_t udp_len = htobe16(i == 4 ? 7 : 23); memcpy(original + 24, &udp_len, 2); }
if (i == 6) { original[6] = 0x20; expected = EIM_NAT_SKIP; }
if (i == 7 || i == 8) original[32] = i == 7 ? 0x40 : 0xf0;
if (i == 9) { original[9] = 99; expected = EIM_NAT_SKIP; }
if (i == 10) len = 19;
union { uint64_t aligned; uint8_t bytes[80]; } storage;
memcpy(storage.bytes + 1, original, len);
uint32_t free_before = ctx.free_count;
int result = eim_nat_egress(&ctx, storage.bytes + 1, len, 1, NULL);
int unchanged = memcmp(storage.bytes + 1, original, len) == 0;
struct eim_nat_entry* mapping = &ctx.table[1];
int incoming = eim_nat_ingress(&ctx, storage.bytes + 1, len, &mapping);
unchanged = unchanged && memcmp(storage.bytes + 1, original, len) == 0;
free(original);
ASSERT_EQ(result, expected, "outgoing packet classified correctly");
ASSERT_EQ(incoming, expected, "incoming packet classified correctly");
ASSERT(!mapping && unchanged && ctx.free_count == free_before, "failure/skip is atomic");
ASSERT(check_indices(&ctx), "invalid input preserves all indices");
}
uint8_t* packet = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT);
union { uint64_t aligned; uint8_t bytes[64]; } storage;
memcpy(storage.bytes + 1, packet, 42); free(packet);
ASSERT_EQ(eim_nat_egress(&ctx, storage.bytes + 1, 42, 1, NULL), EIM_NAT_OK, "unaligned egress works");
packet = make_udp_pkt(TEST_IP_DST_HOST, TEST_DST_PORT, TEST_GW_HOST, TEST_PORT_START);
memcpy(storage.bytes + 1, packet, 42); free(packet);
struct eim_nat_entry* mapping = NULL;
ASSERT_EQ(eim_nat_ingress(&ctx, storage.bytes + 1, 42, &mapping), EIM_NAT_OK, "unaligned ingress works");
ASSERT(mapping && verify_ip_checksum(storage.bytes + 1), "unaligned roundtrip preserves checksum");
eim_nat_destroy_ctx(&ctx);
PASS();
}
/* Полная ёмкость, реальные коллизии, удаление головы/середины/хвоста и повторное использование тех же слотов. */
static void test_hash_lifecycle(void) {
TEST("hash_collisions_full_capacity_and_reuse");
struct global_config g = make_global_config(); g.nat_port_start = 1; g.nat_port_end = 65535;
struct eim_nat_ctx ctx;
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "hash context initialized");
uint8_t* original = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT);
size_t live = u_get_allocated_count();
uint8_t packet[42];
for (uint32_t node = 1; node <= 65535; node++) {
memcpy(packet, original, 42);
ASSERT_EQ(eim_nat_egress(&ctx, packet, 42, node, NULL), EIM_NAT_OK, "fill full hash capacity");
}
ASSERT_EQ(u_get_allocated_count(), live, "creating mappings allocates no heap blocks");
ASSERT(!ctx.free_count && check_indices(&ctx), "full hash/free-list invariants");
uint16_t removed[3] = {0};
for (uint32_t bucket = 0; bucket < EIM_NAT_TABLE_SIZE; bucket++) {
uint16_t head = ctx.buckets[bucket];
if (!head || !ctx.table[head].hash_next || !ctx.table[ctx.table[head].hash_next].hash_next) continue;
removed[0] = head; removed[1] = ctx.table[head].hash_next;
uint16_t tail = removed[1];
while (ctx.table[tail].hash_next) tail = ctx.table[tail].hash_next;
removed[2] = tail;
break;
}
ASSERT(removed[0], "found collision chain of at least three distinct keys");
for (size_t i = 0; i < 3; i++) {
ASSERT_EQ(eim_nat_remove(&ctx, removed[i]), 1, "remove collision-chain member");
ASSERT_EQ(eim_nat_remove(&ctx, removed[i]), 0, "repeated removal does not duplicate free port");
ASSERT(check_indices(&ctx), "indices valid after each removal");
}
for (uint32_t node = 1; node <= 65535; node++) {
if (node == removed[0] || node == removed[1] || node == removed[2]) continue;
memcpy(packet, original, 42);
ASSERT_EQ(eim_nat_egress(&ctx, packet, 42, node, NULL), EIM_NAT_OK, "remaining collision keys still found");
uint16_t external; memcpy(&external, packet + 20, 2);
ASSERT_EQ(be16toh(external), node, "hash lookup preserves each existing mapping");
}
for (size_t i = 0; i < 3; i++) {
memcpy(packet, original, 42);
ASSERT_EQ(eim_nat_egress(&ctx, packet, 42, 100000 + i, NULL), EIM_NAT_OK, "reuse freed port");
uint16_t external; memcpy(&external, packet + 20, 2);
ASSERT_EQ(be16toh(external), removed[i], "free ports reused in FIFO order");
memcpy(packet, original, 42);
ASSERT_EQ(eim_nat_egress(&ctx, packet, 42, removed[i], NULL), EIM_NAT_ERROR, "removed key does not alias reused slot");
ASSERT(memcmp(packet, original, 42) == 0, "full-pool error preserves packet");
}
free(original);
ASSERT(check_indices(&ctx), "final index invariants");
eim_nat_destroy_ctx(&ctx);
PASS();
}
/* Таймауты на границе, ограниченный бюджет, отсутствие expiry у STATIC и освобождение порта после expiry. */
static void test_mapping_expiry(void) {
const uint8_t protocols[] = {IPPROTO_TCP_UINT8, IPPROTO_UDP_UINT8, IPPROTO_ICMP_UINT8};
for (size_t i = 0; i < sizeof(protocols); i++) {
TEST("idle_timeout_boundary_budget_and_static");
struct global_config g = make_global_config();
struct eim_nat_ctx ctx;
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "expiry context initialized");
ASSERT_EQ(eim_nat_add_forward(&ctx, IPPROTO_UDP_UINT8, TEST_IP_SRC_HOST, htobe16(1234), 80, 2), 0, "static fixture");
uint8_t proto = protocols[i];
uint8_t* pkt = proto == IPPROTO_TCP_UINT8 ? make_tcp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, 80) :
proto == IPPROTO_UDP_UINT8 ? make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, 53) :
make_icmp_echo_pkt(TEST_IP_SRC_HOST, TEST_IP_DST_HOST, 8, TEST_SRC_PORT, 1);
size_t len = proto == IPPROTO_TCP_UINT8 ? 54 : 42;
ASSERT_EQ(eim_nat_egress(&ctx, pkt, len, 1, NULL), EIM_NAT_OK, "create expiring mapping");
free(pkt);
uint64_t last = ctx.table[TEST_PORT_START].last_seen;
uint64_t timeout = proto == IPPROTO_TCP_UINT8 ? EIM_NAT_TCP_IDLE_TB :
proto == IPPROTO_UDP_UINT8 ? EIM_NAT_UDP_IDLE_TB : EIM_NAT_ICMP_IDLE_TB;
ASSERT_EQ(eim_nat_expire(&ctx, last + timeout, 1), 0, "GC respects budget before reaching occupied port");
ASSERT_EQ(ctx.gc_cursor, 2, "one GC slot visited");
ASSERT_EQ(eim_nat_expire(&ctx, last + timeout - 1, 65535), 0, "mapping survives before exact timeout");
ASSERT_EQ(eim_nat_expire(&ctx, last + timeout, 65535), 1, "mapping removed at exact timeout");
ASSERT_EQ(ctx.table[80].state, EIM_NAT_ENTRY_STATIC, "static mapping never expires");
ASSERT_EQ(ctx.free_count, TEST_PORT_END - TEST_PORT_START + 1, "expired port returned to pool");
ASSERT(check_indices(&ctx), "expiry preserves all indices");
eim_nat_destroy_ctx(&ctx);
PASS();
}
#ifdef __linux__
TEST("init_allocation_failure_balance");
for (unsigned int fail = 1; fail <= 2; fail++) {
struct global_config g = make_global_config(); struct eim_nat_ctx ctx;
size_t baseline = u_get_allocated_count();
calloc_fail_at = fail; calloc_calls = 0;
int result = eim_nat_init_ctx(&ctx, &g);
calloc_fail_at = 0;
ASSERT_EQ(result, EIM_NAT_ERROR, "allocation failure rejects initialization");
ASSERT(!ctx.initialized && !ctx.table && !ctx.buckets, "allocation failure leaves no live NAT resources");
ASSERT_EQ(u_get_allocated_count(), baseline, "init failure releases every allocation");
}
PASS();
#endif
}
/* ================================================================
* Main
* ================================================================ */
@@ -1065,6 +1242,9 @@ int main(void) {
test_udp_checksum_encoding(1);
test_node_mapping_isolation();
test_static_forward_owners();
test_packet_boundaries();
test_hash_lifecycle();
test_mapping_expiry();
printf("\n=== Results: %d run, %d passed, %d failed ===\n",
stats.run, stats.passed, stats.failed);