Test NAT boundary validation, hash collisions, expiry and allocation failures
This commit is contained in:
1 parent
cecc5d7d1f
commit
cfc0051088
2 files changed
+183
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user