diff --git a/tests/Makefile.am b/tests/Makefile.am index 9c7037d1..0144756c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -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 diff --git a/tests/test_nat_engine.c b/tests/test_nat_engine.c index 940e9b14..be325ef4 100644 --- a/tests/test_nat_engine.c +++ b/tests/test_nat_engine.c @@ -12,10 +12,20 @@ #include #include #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);