You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
791 lines
31 KiB
791 lines
31 KiB
/** |
|
* @file test_nat_engine.c |
|
* @brief Unit-тесты чистого NAT engine (eim_nat.c/h) |
|
* |
|
* Тестирует eim_nat_init_ctx, eim_nat_egress, eim_nat_ingress, |
|
* eim_nat_add_forward, eim_nat_destroy_ctx. |
|
* |
|
* Без TUN, без ETCP, без UTUN_INSTANCE — только crafted IP пакеты. |
|
*/ |
|
|
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include "../lib/debug_config.h" |
|
#include "../src/eim_nat.h" |
|
#include "../src/etcp.h" |
|
#include "../src/config_parser.h" |
|
|
|
#ifdef ENABLE_STATIC_ASSERT |
|
static_assert(sizeof(struct eim_nat_entry) <= 64, "entry size ok"); |
|
#endif |
|
|
|
static struct { |
|
int run, passed, failed; |
|
} stats = {0}; |
|
|
|
#define TEST(name) do { \ |
|
printf("TEST: %-50s ", name); fflush(stdout); \ |
|
stats.run++; \ |
|
} while(0) |
|
|
|
#define PASS() do { puts("PASS"); stats.passed++; } while(0) |
|
#define FAIL(msg) do { printf("FAIL: %s\n", msg); stats.failed++; } while(0) |
|
|
|
#define ASSERT(c, m) do { if (!(c)) { FAIL(m); return; } } while(0) |
|
#define ASSERT_EQ(a,b,m) ASSERT((a)==(b),m) |
|
|
|
/* ================================================================ |
|
* Helpers: build IP packets (uses htobe32/htobe16 + memcpy: network byte order in wire) |
|
* ================================================================ */ |
|
|
|
static void build_ip_hdr(uint8_t* buf, uint8_t proto, uint32_t src_host, uint32_t dst_host, uint16_t total_len) { |
|
buf[0] = 0x45; |
|
buf[1] = 0x00; |
|
uint16_t n = htobe16(total_len); memcpy(buf + 2, &n, 2); |
|
buf[4] = 0x12; buf[5] = 0x34; |
|
memset(buf + 6, 0, 2); |
|
buf[8] = 64; |
|
buf[9] = proto; |
|
memset(buf + 10, 0, 2); // checksum slot = 0 for now |
|
uint32_t src_net = htobe32(src_host); |
|
uint32_t dst_net = htobe32(dst_host); |
|
memcpy(buf + 12, &src_net, 4); |
|
memcpy(buf + 16, &dst_net, 4); |
|
} |
|
|
|
static void compute_ip_checksum(uint8_t* ip) { |
|
uint32_t sum = 0; |
|
for (int i = 0; i < 10; i++) { uint16_t w; memcpy(&w, ip + i*2, 2); sum += w; } |
|
sum = (sum & 0xFFFF) + (sum >> 16); |
|
sum += (sum >> 16); |
|
uint16_t c = (uint16_t)(~sum); |
|
memcpy(ip + 10, &c, 2); |
|
} |
|
|
|
static int verify_ip_checksum(uint8_t* ip) { |
|
uint32_t sum = 0; |
|
for (int i = 0; i < 10; i++) { uint16_t w; memcpy(&w, ip + i*2, 2); sum += w; } |
|
sum = (sum & 0xFFFF) + (sum >> 16); |
|
sum += (sum >> 16); |
|
return (uint16_t)(~sum) == 0; |
|
} |
|
|
|
#define TEST_IP_SRC_HOST 0x0A000002 // 10.0.0.2 |
|
#define TEST_IP_DST_HOST 0x08080808 // 8.8.8.8 |
|
#define TEST_GW_HOST 0x0A000001 // 10.0.0.1 - gateway NAT IP |
|
#define TEST_PORT_START 10000 |
|
#define TEST_PORT_END 20000 |
|
#define TEST_SRC_PORT 40000 |
|
#define TEST_DST_PORT 53 |
|
#define TEST_PAYLOAD_LEN 14 |
|
|
|
static struct ETCP_CONN mock_conn; |
|
|
|
static int mock_conn_initialized = 0; |
|
static struct ETCP_CONN* get_mock_conn(void) { |
|
if (!mock_conn_initialized) { |
|
memset(&mock_conn, 0, sizeof(mock_conn)); |
|
mock_conn.peer_node_id = 0xAAAA000000000001ULL; |
|
mock_conn_initialized = 1; |
|
} |
|
return &mock_conn; |
|
} |
|
|
|
static struct global_config make_global_config(void) { |
|
struct global_config g; |
|
memset(&g, 0, sizeof(g)); |
|
g.nat_enabled = 1; |
|
g.nat_port_start = TEST_PORT_START; |
|
g.nat_port_end = TEST_PORT_END; |
|
g.nat_tun_ip.family = AF_INET; |
|
g.nat_tun_ip.addr.v4.s_addr = htobe32(TEST_GW_HOST); |
|
return g; |
|
} |
|
|
|
/* ================================================================ |
|
* Test 1: Init / Destroy |
|
* ================================================================ */ |
|
static void test_init_destroy(void) { |
|
TEST("init_destroy_normal"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
int r = eim_nat_init_ctx(&ctx, &g); |
|
ASSERT_EQ(r, 0, "init returns 0"); |
|
ASSERT(ctx.initialized == 1, "ctx.initialized=1"); |
|
ASSERT(ctx.gateway_ip == TEST_GW_HOST, "gateway_ip correct"); |
|
ASSERT(ctx.port_start == TEST_PORT_START, "port_start correct"); |
|
ASSERT(ctx.port_end == TEST_PORT_END, "port_end correct"); |
|
ASSERT(ctx.table != NULL, "table allocated"); |
|
eim_nat_destroy_ctx(&ctx); |
|
ASSERT(ctx.initialized == 0, "destroy clears initialized"); |
|
ASSERT(ctx.table == NULL, "destroy frees table"); |
|
} |
|
PASS(); |
|
|
|
TEST("init_null_ctx"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
int r = eim_nat_init_ctx(NULL, &g); |
|
ASSERT_EQ(r, -1, "returns -1"); |
|
} |
|
PASS(); |
|
|
|
TEST("init_null_config"); |
|
{ |
|
struct eim_nat_ctx ctx; |
|
int r = eim_nat_init_ctx(&ctx, NULL); |
|
ASSERT_EQ(r, -1, "returns -1"); |
|
} |
|
PASS(); |
|
|
|
TEST("init_nat_disabled"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
g.nat_enabled = 0; |
|
struct eim_nat_ctx ctx; |
|
int r = eim_nat_init_ctx(&ctx, &g); |
|
ASSERT_EQ(r, 0, "returns 0"); |
|
ASSERT(ctx.initialized == 0, "not initialized"); |
|
ASSERT(ctx.table == NULL, "no table"); |
|
} |
|
PASS(); |
|
|
|
TEST("init_bad_port_range"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
g.nat_port_start = 20000; g.nat_port_end = 10000; |
|
struct eim_nat_ctx ctx; |
|
int r = eim_nat_init_ctx(&ctx, &g); |
|
ASSERT_EQ(r, -1, "returns -1"); |
|
} |
|
PASS(); |
|
|
|
TEST("destroy_twice_safe"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
eim_nat_destroy_ctx(&ctx); |
|
eim_nat_destroy_ctx(&ctx); // second call should be no-op |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 2: Port Allocation |
|
* ================================================================ */ |
|
|
|
static uint8_t* make_udp_pkt(uint32_t src_host, uint16_t src_port_host, uint32_t dst_host, uint16_t dst_port_host) { |
|
const size_t ip_udp_len = 20 + 8 + TEST_PAYLOAD_LEN; |
|
uint8_t* pkt = calloc(1, ip_udp_len); |
|
build_ip_hdr(pkt, IPPROTO_UDP_UINT8, src_host, dst_host, ip_udp_len); |
|
// UDP header |
|
uint16_t sp = htobe16(src_port_host), dp = htobe16(dst_port_host); |
|
memcpy(pkt + 20, &sp, 2); // src port |
|
memcpy(pkt + 22, &dp, 2); // dst port |
|
uint16_t udp_len = htobe16(8 + TEST_PAYLOAD_LEN); |
|
memcpy(pkt + 24, &udp_len, 2); // length |
|
memset(pkt + 26, 0, 2); // checksum = 0 (no UDP csum) |
|
memset(pkt + 28, 0xAB, TEST_PAYLOAD_LEN); // payload |
|
compute_ip_checksum(pkt); |
|
return pkt; |
|
} |
|
|
|
static void test_port_alloc(void) { |
|
TEST("port_alloc_sequential"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
g.nat_port_start = 10000; g.nat_port_end = 10005; |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// Allocate 3 ports (different internal src ports) |
|
for (int i = 0; i < 3; i++) { |
|
uint8_t* pkt = make_udp_pkt(TEST_IP_SRC_HOST, 50000 + i, TEST_IP_DST_HOST, 53); |
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "egress ok"); |
|
// Check port was allocated from table index |
|
struct eim_nat_entry* e = &ctx.table[10000 + i]; |
|
ASSERT(e->state == EIM_NAT_ENTRY_ACTIVE, "entry active"); |
|
ASSERT_EQ(e->internal_port, htobe16(50000 + i), "internal port stored"); |
|
free(pkt); |
|
} |
|
ASSERT(ctx.next_port == 10003, "next_port advanced"); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("port_alloc_wraparound"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
g.nat_port_start = 10000; g.nat_port_end = 10001; // 2 ports |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// Fill first entry: egress with port not yet seen |
|
uint8_t* p1 = make_udp_pkt(TEST_IP_SRC_HOST, 50001, TEST_IP_DST_HOST, 53); |
|
int r = eim_nat_egress(&ctx, p1, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "first egress ok"); |
|
ASSERT(ctx.table[10000].state == EIM_NAT_ENTRY_ACTIVE, "port 10000 used"); |
|
free(p1); |
|
|
|
// Release port 10000 manually |
|
ctx.table[10000].state = EIM_NAT_ENTRY_FREE; |
|
|
|
// Now alloc should reuse port 10000 (next_port is at 10001, but it wraps around) |
|
// Actually next_port is 10001 after first alloc. port 10000 is free, but alloc starts from next_port. |
|
// Let me check the algorithm: it scans from next_port forward. If 10000 is free but 10001 is not current, |
|
// it will allocate 10001. But if we free 10000, the scan from 10001 will bypass it. |
|
// Actually eim_nat_alloc_port starts from ctx->next_port, not from port_start. |
|
// After first alloc, next_port=10001. Free 10000. |
|
// Now alloc starts from 10001 - it's free (we only allocated 10000, then freed it. next_port was incremented to 10001). |
|
// Wait, first alloc: port 10000 → next_port becomes 10001 (since 10001 <= port_end). |
|
// Then we free 10000. |
|
// Now alloc starts from 10001. It's free. So it allocates 10001. |
|
// Then next_port becomes 10002 → > 10001 → wraps to 10000. Now it allocates 10000 since it's free. |
|
|
|
// Allocate second - gets 10001 |
|
uint8_t* p2 = make_udp_pkt(TEST_IP_SRC_HOST, 50002, TEST_IP_DST_HOST, 53); |
|
r = eim_nat_egress(&ctx, p2, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "second egress ok"); |
|
ASSERT(ctx.table[10001].state == EIM_NAT_ENTRY_ACTIVE, "port 10001 used"); |
|
ASSERT_EQ(ctx.next_port, 10000, "next_port wrapped to 10000"); |
|
free(p2); |
|
|
|
// Third - wraps and gets 10000 (freed) |
|
uint8_t* p3 = make_udp_pkt(TEST_IP_SRC_HOST, 50003, TEST_IP_DST_HOST, 53); |
|
r = eim_nat_egress(&ctx, p3, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "third egress ok after wrap"); |
|
ASSERT(ctx.table[10000].state == EIM_NAT_ENTRY_ACTIVE, "port 10000 reused"); |
|
ASSERT_EQ(ctx.table[10000].internal_port, htobe16(50003), "new entry for reused port"); |
|
free(p3); |
|
|
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("port_exhaustion"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
g.nat_port_start = 10000; g.nat_port_end = 10001; // 2 ports |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// Fill both ports with different flows |
|
uint8_t* p1 = make_udp_pkt(TEST_IP_SRC_HOST, 50001, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
int r = eim_nat_egress(&ctx, p1, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "first ok"); free(p1); |
|
|
|
uint8_t* p2 = make_udp_pkt(0x0A000003, 50002, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
r = eim_nat_egress(&ctx, p2, 20 + 8 + TEST_PAYLOAD_LEN, 2, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "second ok"); free(p2); |
|
|
|
// Third with different (ip,port) → alloc fails |
|
uint8_t* p3 = make_udp_pkt(0x0A000004, 50003, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
r = eim_nat_egress(&ctx, p3, 20 + 8 + TEST_PAYLOAD_LEN, 3, get_mock_conn()); |
|
ASSERT_EQ(r, -1, "fails on exhaustion"); free(p3); |
|
|
|
// Same flow as first → reuses entry |
|
uint8_t* p4 = make_udp_pkt(TEST_IP_SRC_HOST, 50001, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
r = eim_nat_egress(&ctx, p4, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "same flow reuses entry"); free(p4); |
|
|
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 3: Egress NAT — UDP |
|
* ================================================================ */ |
|
static void test_egress_udp(void) { |
|
TEST("egress_udp_basic"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
uint8_t* pkt = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
// Save original dst IP/port (should not be changed by egress) |
|
uint32_t orig_dst_ip_net; memcpy(&orig_dst_ip_net, pkt + 16, 4); |
|
uint16_t orig_dst_port_net; memcpy(&orig_dst_port_net, pkt + 22, 2); |
|
|
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 0x5555, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "egress returns 0"); |
|
|
|
// Check src IP = gateway |
|
uint32_t new_src_ip_net; memcpy(&new_src_ip_net, pkt + 12, 4); |
|
ASSERT_EQ(be32toh(new_src_ip_net), TEST_GW_HOST, "src IP = gateway"); |
|
// Check dst IP unchanged |
|
uint32_t dst_ip_net; memcpy(&dst_ip_net, pkt + 16, 4); |
|
ASSERT_EQ(dst_ip_net, orig_dst_ip_net, "dst IP unchanged"); |
|
// Check src port changed |
|
uint16_t new_src_port_net; memcpy(&new_src_port_net, pkt + 20, 2); |
|
ASSERT(be16toh(new_src_port_net) == TEST_PORT_START, "src port = port_start"); |
|
// Check dst port unchanged |
|
uint16_t dst_port_net; memcpy(&dst_port_net, pkt + 22, 2); |
|
ASSERT_EQ(dst_port_net, orig_dst_port_net, "dst port unchanged"); |
|
// Check IP checksum valid |
|
ASSERT(verify_ip_checksum(pkt) == 1, "IP checksum valid"); |
|
// Check NAT entry |
|
struct eim_nat_entry* e = &ctx.table[TEST_PORT_START]; |
|
ASSERT_EQ(e->state, EIM_NAT_ENTRY_ACTIVE, "entry active"); |
|
ASSERT_EQ(e->internal_ip, TEST_IP_SRC_HOST, "internal_ip stored"); |
|
ASSERT_EQ(e->internal_port, htobe16(TEST_SRC_PORT), "internal_port stored"); |
|
ASSERT_EQ(e->proto, IPPROTO_UDP_UINT8, "proto=UDP"); |
|
ASSERT_EQ(e->src_node_id, 0x5555ULL, "src_node_id stored"); |
|
ASSERT(e->src_conn == get_mock_conn(), "src_conn stored"); |
|
|
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 4: Egress NAT — TCP |
|
* ================================================================ */ |
|
static uint8_t* make_tcp_pkt(uint32_t src_host, uint16_t src_port_host, uint32_t dst_host, uint16_t dst_port_host) { |
|
const size_t ip_tcp_len = 20 + 20 + TEST_PAYLOAD_LEN; // 20 TCP hdr min |
|
uint8_t* pkt = calloc(1, ip_tcp_len); |
|
build_ip_hdr(pkt, IPPROTO_TCP_UINT8, src_host, dst_host, ip_tcp_len); |
|
uint16_t sp = htobe16(src_port_host), dp = htobe16(dst_port_host); |
|
memcpy(pkt + 20, &sp, 2); |
|
memcpy(pkt + 22, &dp, 2); |
|
// seq, ack, offset+flags, window |
|
pkt[32] = 0x50; // offset=5 (20 bytes), flags=0 |
|
// checksum |
|
memset(pkt + 36, 0, 2); |
|
memset(pkt + 40, 0xCC, TEST_PAYLOAD_LEN); |
|
compute_ip_checksum(pkt); |
|
return pkt; |
|
} |
|
|
|
static void test_egress_tcp(void) { |
|
TEST("egress_tcp_basic"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
uint8_t* pkt = make_tcp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, 80); |
|
int r = eim_nat_egress(&ctx, pkt, 20 + 20 + TEST_PAYLOAD_LEN, 0x1111, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "egress TCP ok"); |
|
// Check src IP = gateway |
|
uint32_t new_src; memcpy(&new_src, pkt + 12, 4); |
|
ASSERT_EQ(be32toh(new_src), TEST_GW_HOST, "src IP=gw"); |
|
// Check src port |
|
uint16_t new_sport; memcpy(&new_sport, pkt + 20, 2); |
|
ASSERT_EQ(be16toh(new_sport), TEST_PORT_START, "src port=start"); |
|
// IP checksum valid |
|
ASSERT(verify_ip_checksum(pkt) == 1, "IP checksum valid"); |
|
// Entry proto = TCP |
|
ASSERT_EQ(ctx.table[TEST_PORT_START].proto, IPPROTO_TCP_UINT8, "proto=TCP"); |
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 5: Egress ICMP Echo |
|
* ================================================================ */ |
|
static uint8_t* make_icmp_echo_pkt(uint32_t src_host, uint32_t dst_host, uint8_t icmp_type, uint16_t id_host, uint16_t seq_host) { |
|
const size_t ip_icmp_len = 20 + 8 + TEST_PAYLOAD_LEN; |
|
uint8_t* pkt = calloc(1, ip_icmp_len); |
|
build_ip_hdr(pkt, IPPROTO_ICMP_UINT8, src_host, dst_host, ip_icmp_len); |
|
pkt[20] = icmp_type; // type |
|
pkt[21] = 0x00; // code |
|
// checksum = 0 for now |
|
memset(pkt + 22, 0, 2); |
|
uint16_t id_n = htobe16(id_host), seq_n = htobe16(seq_host); |
|
memcpy(pkt + 24, &id_n, 2); |
|
memcpy(pkt + 26, &seq_n, 2); |
|
memset(pkt + 28, 0xDD, TEST_PAYLOAD_LEN); |
|
compute_ip_checksum(pkt); |
|
return pkt; |
|
} |
|
|
|
static void test_egress_icmp(void) { |
|
TEST("egress_icmp_echo_request"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
uint16_t icmp_id = 0x1234; // host order |
|
uint8_t* pkt = make_icmp_echo_pkt(TEST_IP_SRC_HOST, TEST_IP_DST_HOST, 8, icmp_id, 1); |
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 0x9999, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "egress ICMP echo ok"); |
|
// ICMP ID should be overwritten with allocated port |
|
uint16_t new_id_net; memcpy(&new_id_net, pkt + 24, 2); |
|
ASSERT_EQ(be16toh(new_id_net), TEST_PORT_START, "ICMP ID = allocated port"); |
|
// IP checksum valid |
|
ASSERT(verify_ip_checksum(pkt) == 1, "IP checksum valid"); |
|
// Entry proto = ICMP |
|
ASSERT_EQ(ctx.table[TEST_PORT_START].proto, IPPROTO_ICMP_UINT8, "proto=ICMP"); |
|
ASSERT_EQ(ctx.table[TEST_PORT_START].internal_port, htobe16(icmp_id), "internal port = ICMP ID"); |
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("egress_icmp_error_no_rewrite"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// ICMP Dest Unreachable (type 3) — no echo, no id rewrite, no entry |
|
uint8_t* pkt = make_icmp_echo_pkt(TEST_IP_SRC_HOST, TEST_IP_DST_HOST, 3, 0x1234, 1); |
|
// Save original data for comparison |
|
uint8_t backup[20 + 8 + TEST_PAYLOAD_LEN]; |
|
memcpy(backup, pkt, sizeof(backup)); |
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 0x9999, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "returns 0 (not -1)"); |
|
// Check no entry created |
|
ASSERT(ctx.table[TEST_PORT_START].state == EIM_NAT_ENTRY_FREE, "no entry created for ICMP error"); |
|
// Packet should be unchanged (non-echo ICMP bypasses) |
|
ASSERT(memcmp(backup, pkt, sizeof(backup)) == 0, "packet unchanged"); |
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 6: Egress fragments |
|
* ================================================================ */ |
|
static void test_egress_fragments(void) { |
|
TEST("egress_fragment_mf_no_off"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
uint8_t* pkt = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
// Set MF=1, offset=0 |
|
pkt[6] = 0x20; pkt[7] = 0x00; |
|
// Recompute checksum with new frag field |
|
compute_ip_checksum(pkt); |
|
|
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 0x1111, get_mock_conn()); |
|
// MF bit set → bypassed (returns 0, no allocation) |
|
ASSERT_EQ(r, 0, "egress fragment MF=1 bypassed"); |
|
ASSERT(ctx.table[TEST_PORT_START].state == EIM_NAT_ENTRY_FREE, "no entry for fragment"); |
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("egress_fragment_nonzero_offset"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
uint8_t* pkt = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
// offset = 185 (in 8-byte units) → 0x00B9 network order |
|
pkt[6] = 0x00; pkt[7] = 0xB9; |
|
compute_ip_checksum(pkt); |
|
|
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 0x1111, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "egress fragment offset>0 bypassed"); |
|
ASSERT(ctx.table[TEST_PORT_START].state == EIM_NAT_ENTRY_FREE, "no entry"); |
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 7: Egress invalid packets |
|
* ================================================================ */ |
|
static void test_egress_invalid(void) { |
|
TEST("egress_too_short"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
uint8_t buf[10]; |
|
int r = eim_nat_egress(&ctx, buf, 10, 1, get_mock_conn()); |
|
ASSERT_EQ(r, -1, "returns -1"); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("egress_bad_ihl"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
uint8_t* pkt = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
pkt[0] = 0x43; // IHL=3 (invalid, <5) |
|
int r = eim_nat_egress(&ctx, pkt, 20 + 8 + TEST_PAYLOAD_LEN, 1, get_mock_conn()); |
|
ASSERT_EQ(r, -1, "returns -1 for bad IHL"); |
|
free(pkt); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("egress_not_tcp_udp_icmp"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
// Build IP with proto=0x63 (unknown) but pretend it's UDP format |
|
uint8_t pkt[20 + 8 + TEST_PAYLOAD_LEN]; |
|
build_ip_hdr(pkt, 0x63, TEST_IP_SRC_HOST, TEST_IP_DST_HOST, sizeof(pkt)); |
|
// Fill fake UDP header |
|
uint16_t sp = htobe16(TEST_SRC_PORT), dp = htobe16(TEST_DST_PORT); |
|
memcpy(pkt + 20, &sp, 2); memcpy(pkt + 22, &dp, 2); |
|
compute_ip_checksum(pkt); |
|
int r = eim_nat_egress(&ctx, pkt, sizeof(pkt), 1, get_mock_conn()); |
|
ASSERT_EQ(r, 0, "unknown proto bypassed (returns 0)"); |
|
ASSERT(ctx.table[TEST_PORT_START].state == EIM_NAT_ENTRY_FREE, "no entry"); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 8: Ingress NAT — UDP |
|
* ================================================================ */ |
|
static uint8_t* make_respond_pkt(uint32_t src_host, uint16_t src_port_host, uint32_t dst_host, uint16_t dst_port_host) { |
|
// Build a packet FROM internet TO gateway (this is the response after egress) |
|
return make_udp_pkt(src_host, src_port_host, dst_host, dst_port_host); |
|
} |
|
|
|
static void test_ingress_udp(void) { |
|
TEST("ingress_udp_normal"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// First: egress to create entry |
|
uint8_t* out = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
eim_nat_egress(&ctx, out, 20 + 8 + TEST_PAYLOAD_LEN, 0x1111, get_mock_conn()); |
|
free(out); |
|
|
|
// Save internal state |
|
uint32_t internal_ip = ctx.table[TEST_PORT_START].internal_ip; |
|
uint16_t internal_port_net = ctx.table[TEST_PORT_START].internal_port; |
|
|
|
// Build response: FROM 8.8.8.8:53 TO gateway:port_start |
|
uint8_t* resp = make_respond_pkt(TEST_IP_DST_HOST, TEST_DST_PORT, TEST_GW_HOST, TEST_PORT_START); |
|
struct eim_nat_entry* entry = NULL; |
|
int r = eim_nat_ingress(&ctx, resp, 20 + 8 + TEST_PAYLOAD_LEN, &entry); |
|
ASSERT_EQ(r, 0, "ingress ok"); |
|
ASSERT(entry != NULL, "entry returned"); |
|
ASSERT(entry == &ctx.table[TEST_PORT_START], "correct entry"); |
|
// Check dst IP → internal IP |
|
uint32_t new_dst_net; memcpy(&new_dst_net, resp + 16, 4); |
|
ASSERT_EQ(be32toh(new_dst_net), internal_ip, "dst IP = internal IP"); |
|
// Check dst port → internal port |
|
uint16_t new_dst_port_net; memcpy(&new_dst_port_net, resp + 22, 2); |
|
ASSERT_EQ(new_dst_port_net, internal_port_net, "dst port = internal port"); |
|
// Check src IP unchanged |
|
uint32_t src_net; memcpy(&src_net, resp + 12, 4); |
|
ASSERT_EQ(be32toh(src_net), TEST_IP_DST_HOST, "src IP unchanged"); |
|
ASSERT(verify_ip_checksum(resp) == 1, "IP checksum valid"); |
|
|
|
free(resp); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("ingress_no_entry"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
uint8_t* resp = make_respond_pkt(TEST_IP_DST_HOST, 53, TEST_GW_HOST, TEST_PORT_START + 500); |
|
int r = eim_nat_ingress(&ctx, resp, 20 + 8 + TEST_PAYLOAD_LEN, NULL); |
|
ASSERT_EQ(r, 0, "returns 0 (no entry → bypass)"); |
|
// Packet unchanged (port not in range anyway? Actually it IS in range but entry is FREE) |
|
// Wait: port_start+500 = 10500, which IS in range [10000,20000]. Entry state = FREE. |
|
// Code checks: if entry->state == FREE return 0 |
|
ASSERT(ctx.table[10500].state == EIM_NAT_ENTRY_FREE, "entry is free"); |
|
free(resp); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("ingress_not_for_gateway"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
// Packet dst = 8.8.8.8, not gateway → bypass |
|
uint8_t* resp = make_respond_pkt(TEST_IP_DST_HOST, 53, TEST_IP_DST_HOST, TEST_PORT_START); |
|
uint8_t backup[20 + 8 + TEST_PAYLOAD_LEN]; |
|
memcpy(backup, resp, sizeof(backup)); |
|
int r = eim_nat_ingress(&ctx, resp, 20 + 8 + TEST_PAYLOAD_LEN, NULL); |
|
ASSERT_EQ(r, 0, "bypass (dst not gateway)"); |
|
ASSERT(memcmp(backup, resp, sizeof(backup)) == 0, "packet unchanged"); |
|
free(resp); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 9: Ingress ICMP Echo Reply |
|
* ================================================================ */ |
|
static void test_ingress_icmp(void) { |
|
TEST("ingress_icmp_echo_reply"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
uint16_t icmp_id = 0x4321; |
|
// 1. Egress ICMP Echo Request → rewrites ID to allocated port |
|
uint8_t* req = make_icmp_echo_pkt(TEST_IP_SRC_HOST, TEST_IP_DST_HOST, 8, icmp_id, 1); |
|
eim_nat_egress(&ctx, req, 20 + 8 + TEST_PAYLOAD_LEN, 0x2222, get_mock_conn()); |
|
free(req); |
|
|
|
// 2. Build ICMP Echo Reply FROM internet TO gateway:port_start |
|
const size_t len = 20 + 8 + TEST_PAYLOAD_LEN; |
|
uint8_t* reply = calloc(1, len); |
|
build_ip_hdr(reply, IPPROTO_ICMP_UINT8, TEST_IP_DST_HOST, TEST_GW_HOST, len); |
|
reply[20] = 0; // Echo Reply |
|
reply[21] = 0; |
|
memset(reply + 22, 0, 2); // checksum |
|
uint16_t alloc_id_net = htobe16(TEST_PORT_START); // the port allocated by egress |
|
uint16_t seq = htobe16(1); |
|
memcpy(reply + 24, &alloc_id_net, 2); |
|
memcpy(reply + 26, &seq, 2); |
|
memset(reply + 28, 0xDD, TEST_PAYLOAD_LEN); |
|
compute_ip_checksum(reply); |
|
|
|
// 3. Ingress → should rewrite ID back to icmp_id |
|
struct eim_nat_entry* entry = NULL; |
|
int r = eim_nat_ingress(&ctx, reply, len, &entry); |
|
ASSERT_EQ(r, 0, "ingress icmp reply ok"); |
|
uint16_t new_id_net; memcpy(&new_id_net, reply + 24, 2); |
|
ASSERT_EQ(be16toh(new_id_net), icmp_id, "ICMP ID restored to original"); |
|
ASSERT(entry != NULL, "entry returned"); |
|
ASSERT(verify_ip_checksum(reply) == 1, "IP checksum valid"); |
|
free(reply); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 10: Port Forwarding (static entries) |
|
* ================================================================ */ |
|
static void test_port_forward(void) { |
|
TEST("add_forward_basic"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
int r = eim_nat_add_forward(&ctx, IPPROTO_TCP_UINT8, |
|
0x0A0000FE, htobe16(8080), // internal 10.0.0.254:8080 |
|
10050); |
|
ASSERT_EQ(r, 0, "add_forward ok"); |
|
ASSERT(ctx.table[10050].state == EIM_NAT_ENTRY_STATIC, "entry static"); |
|
ASSERT_EQ(ctx.table[10050].internal_ip, 0x0A0000FE, "internal_ip"); |
|
ASSERT_EQ(ctx.table[10050].internal_port, htobe16(8080), "internal_port"); |
|
ASSERT_EQ(ctx.table[10050].proto, IPPROTO_TCP_UINT8, "proto=TCP"); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("add_forward_duplicate"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
eim_nat_add_forward(&ctx, IPPROTO_TCP_UINT8, 0x0A0000FE, htobe16(8080), 10050); |
|
int r = eim_nat_add_forward(&ctx, IPPROTO_UDP_UINT8, 0x0A0000FF, htobe16(9090), 10050); |
|
ASSERT_EQ(r, -1, "duplicate rejects"); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
|
|
TEST("ingress_hits_static_entry"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// Static forward: external port 10050 → internal 10.0.0.254:8080 TCP |
|
eim_nat_add_forward(&ctx, IPPROTO_TCP_UINT8, 0x0A0000FE, htobe16(8080), 10050); |
|
|
|
// Ingress TCP packet to gateway:10050 |
|
uint8_t* resp = make_tcp_pkt(TEST_IP_DST_HOST, 443, TEST_GW_HOST, 10050); |
|
struct eim_nat_entry* entry = NULL; |
|
int r = eim_nat_ingress(&ctx, resp, 20 + 20 + TEST_PAYLOAD_LEN, &entry); |
|
ASSERT_EQ(r, 0, "ingress static ok"); |
|
ASSERT(entry != NULL, "entry returned"); |
|
ASSERT_EQ(entry->state, EIM_NAT_ENTRY_STATIC, "static entry"); |
|
// Check dst IP → 10.0.0.254 |
|
uint32_t dst_net; memcpy(&dst_net, resp + 16, 4); |
|
ASSERT_EQ(be32toh(dst_net), 0x0A0000FE, "dst IP = 10.0.0.254"); |
|
// Check dst port → 8080 |
|
uint16_t dst_port; memcpy(&dst_port, resp + 22, 2); |
|
ASSERT_EQ(dst_port, htobe16(8080), "dst port = 8080"); |
|
ASSERT(verify_ip_checksum(resp) == 1, "IP checksum valid"); |
|
free(resp); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Test 11: ICMP non-echo egress (bypass, no modification) |
|
* ================================================================ */ |
|
static void test_bypass_flows(void) { |
|
TEST("egress_flow_reuse"); |
|
{ |
|
struct global_config g = make_global_config(); |
|
struct eim_nat_ctx ctx; |
|
eim_nat_init_ctx(&ctx, &g); |
|
|
|
// Same flow (ip:port:proto) twice → same port |
|
uint8_t* p1 = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); |
|
eim_nat_egress(&ctx, p1, 20 + 8 + TEST_PAYLOAD_LEN, 0x1111, get_mock_conn()); |
|
free(p1); |
|
|
|
uint8_t* p2 = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, 0x08080404, TEST_DST_PORT); |
|
eim_nat_egress(&ctx, p2, 20 + 8 + TEST_PAYLOAD_LEN, 0x1111, get_mock_conn()); |
|
// Should reuse same port (matching internal_ip:port:proto) |
|
uint16_t sp; memcpy(&sp, p2 + 20, 2); |
|
ASSERT_EQ(be16toh(sp), TEST_PORT_START, "same port reused"); |
|
free(p2); |
|
eim_nat_destroy_ctx(&ctx); |
|
} |
|
PASS(); |
|
} |
|
|
|
/* ================================================================ |
|
* Main |
|
* ================================================================ */ |
|
int main(void) { |
|
debug_config_init(); |
|
debug_set_level(DEBUG_LEVEL_ERROR); |
|
|
|
test_init_destroy(); |
|
test_port_alloc(); |
|
test_egress_udp(); |
|
test_egress_tcp(); |
|
test_egress_icmp(); |
|
test_egress_fragments(); |
|
test_egress_invalid(); |
|
test_ingress_udp(); |
|
test_ingress_icmp(); |
|
test_port_forward(); |
|
test_bypass_flows(); |
|
|
|
printf("\n=== Results: %d run, %d passed, %d failed ===\n", |
|
stats.run, stats.passed, stats.failed); |
|
return stats.failed ? 1 : 0; |
|
}
|
|
|