Browse Source
- New ETCP protocol ETCP_ID_NAT (0x02) for NAT traffic between nodes - eim_nat.c/h: Pure NAT engine (table, port allocation, checksum updates, egress/ingress) - nat_transport.c/h: Transport layer (NAT TUN, ETCP binding, client/provider roles) - route_bgp_find_conn_for_node(): shared utility for optimal connection lookup - tun_init_nat(): dedicated TUN creation for NAT - Config: [nat] section with tun_ip, port_start/end, nat_via, forward rules - Fix: csum_update_ip/transport use htonl() halves (correct on LE systems) - Fix: IP_FRAG_MF_MASK changed to 0x2000 (after ntohs, MF is bit 13) - Tests: 26 engine unit + 4 transport integration + 7 stress (29 total)congestion
22 changed files with 2780 additions and 4 deletions
@ -0,0 +1,306 @@
|
||||
#include "eim_nat.h" |
||||
#include "config_parser.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
|
||||
// IP header offsets
|
||||
#define IP_IHL_OFFSET 0 |
||||
#define IP_PROTO_OFFSET 9 |
||||
#define IP_CHECKSUM_OFFSET 10 |
||||
#define IP_SRC_ADDR_OFFSET 12 |
||||
#define IP_DST_ADDR_OFFSET 16 |
||||
#define IP_HDR_MIN_SIZE 20 |
||||
|
||||
#define IP_FRAG_MF_MASK 0x2000 |
||||
#define IP_FRAG_OFF_MASK 0x1FFF |
||||
|
||||
#define TCP_SRC_PORT_OFFSET 0 |
||||
#define TCP_DST_PORT_OFFSET 2 |
||||
#define TCP_CHECKSUM_OFFSET 16 |
||||
#define UDP_SRC_PORT_OFFSET 0 |
||||
#define UDP_DST_PORT_OFFSET 2 |
||||
#define UDP_CHECKSUM_OFFSET 6 |
||||
#define ICMP_TYPE_OFFSET 0 |
||||
#define ICMP_ID_OFFSET 4 |
||||
#define ICMP_CHECKSUM_OFFSET 2 |
||||
#define ICMP_ECHO_REQUEST 8 |
||||
#define ICMP_ECHO_REPLY 0 |
||||
|
||||
static ip_str_t ip_host_to_str(uint32_t ip_host) { |
||||
struct in_addr a; a.s_addr = htonl(ip_host); |
||||
return ip_to_str(&a, AF_INET); |
||||
} |
||||
|
||||
static uint16_t csum_update_n(uint16_t old_csum, const uint16_t* old_vals, |
||||
const uint16_t* new_vals, int n) { |
||||
uint32_t sum = (uint32_t)(uint16_t)(~old_csum); |
||||
for (int i = 0; i < n; i++) { sum -= old_vals[i]; sum += new_vals[i]; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum = (sum & 0xFFFF) + (sum >> 16); |
||||
return (uint16_t)(~sum); |
||||
} |
||||
|
||||
static void csum_update_ip(uint8_t* ip_data, uint32_t old_ip_host, uint32_t new_ip_host) { |
||||
uint16_t old_csum; memcpy(&old_csum, ip_data + IP_CHECKSUM_OFFSET, 2); |
||||
uint32_t old_ip_net = htonl(old_ip_host), new_ip_net = htonl(new_ip_host); |
||||
uint16_t old_w[2] = {(uint16_t)(old_ip_net & 0xFFFF), (uint16_t)(old_ip_net >> 16)}; |
||||
uint16_t new_w[2] = {(uint16_t)(new_ip_net & 0xFFFF), (uint16_t)(new_ip_net >> 16)}; |
||||
uint16_t new_csum = csum_update_n(old_csum, old_w, new_w, 2); |
||||
memcpy(ip_data + IP_CHECKSUM_OFFSET, &new_csum, 2); |
||||
} |
||||
|
||||
static void csum_update_transport(uint8_t* transport, int csum_off, |
||||
uint32_t old_ip_host, uint32_t new_ip_host, |
||||
uint16_t old_port_net, uint16_t new_port_net) { |
||||
uint16_t old_csum; memcpy(&old_csum, transport + csum_off, 2); |
||||
uint32_t old_ip_net = htonl(old_ip_host), new_ip_net = htonl(new_ip_host); |
||||
uint16_t old_w[3] = {(uint16_t)(old_ip_net & 0xFFFF), (uint16_t)(old_ip_net >> 16), old_port_net}; |
||||
uint16_t new_w[3] = {(uint16_t)(new_ip_net & 0xFFFF), (uint16_t)(new_ip_net >> 16), new_port_net}; |
||||
uint16_t new_csum = csum_update_n(old_csum, old_w, new_w, 3); |
||||
memcpy(transport + csum_off, &new_csum, 2); |
||||
} |
||||
|
||||
static void csum_update_icmp(uint8_t* icmp, uint16_t old_word, uint16_t new_word) { |
||||
uint16_t old_csum; memcpy(&old_csum, icmp + ICMP_CHECKSUM_OFFSET, 2); |
||||
uint16_t new_csum = csum_update_n(old_csum, &old_word, &new_word, 1); |
||||
memcpy(icmp + ICMP_CHECKSUM_OFFSET, &new_csum, 2); |
||||
} |
||||
|
||||
static struct eim_nat_entry* eim_nat_find_egress(struct eim_nat_ctx* ctx, |
||||
uint8_t proto, uint32_t ip_host, uint16_t port_net) { |
||||
for (uint16_t i = ctx->port_start; i <= ctx->port_end; i++) { |
||||
struct eim_nat_entry* e = &ctx->table[i]; |
||||
if (e->state != EIM_NAT_ENTRY_FREE && e->proto == proto && |
||||
e->internal_ip == ip_host && e->internal_port == port_net) return e; |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
static uint16_t eim_nat_alloc_port(struct eim_nat_ctx* ctx) { |
||||
uint16_t start = ctx->next_port; |
||||
do { |
||||
if (ctx->table[ctx->next_port].state == EIM_NAT_ENTRY_FREE) { |
||||
uint16_t port = ctx->next_port; |
||||
ctx->next_port++; |
||||
if (ctx->next_port > ctx->port_end) ctx->next_port = ctx->port_start; |
||||
return port; |
||||
} |
||||
ctx->next_port++; |
||||
if (ctx->next_port > ctx->port_end) ctx->next_port = ctx->port_start; |
||||
} while (ctx->next_port != start); |
||||
return 0; |
||||
} |
||||
|
||||
// ==================== Public API ====================
|
||||
|
||||
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len, |
||||
uint64_t src_node_id, struct ETCP_CONN* src_conn) { |
||||
if (ip_len < IP_HDR_MIN_SIZE) return -1; |
||||
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F; |
||||
if (ihl < 5) return -1; |
||||
uint16_t ip_hdr_len = ihl * 4; |
||||
if (ip_len < ip_hdr_len) return -1; |
||||
|
||||
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6)); |
||||
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0; |
||||
|
||||
uint8_t proto = ip_data[IP_PROTO_OFFSET]; |
||||
uint32_t src_ip_net, src_ip_host; |
||||
memcpy(&src_ip_net, ip_data + IP_SRC_ADDR_OFFSET, 4); |
||||
src_ip_host = ntohl(src_ip_net); |
||||
|
||||
uint8_t* transport = ip_data + ip_hdr_len; |
||||
size_t tlen = ip_len - ip_hdr_len; |
||||
uint16_t src_port_net = 0; |
||||
int is_tcp = (proto == IPPROTO_TCP_UINT8 && tlen >= 20); |
||||
int is_udp = (proto == IPPROTO_UDP_UINT8 && tlen >= 8); |
||||
int is_icmp_echo = 0; |
||||
|
||||
if (is_tcp) memcpy(&src_port_net, transport + TCP_SRC_PORT_OFFSET, 2); |
||||
else if (is_udp) memcpy(&src_port_net, transport + UDP_SRC_PORT_OFFSET, 2); |
||||
else if (proto == IPPROTO_ICMP_UINT8 && tlen >= 8) { |
||||
uint8_t icmp_type = transport[ICMP_TYPE_OFFSET]; |
||||
if (icmp_type == ICMP_ECHO_REQUEST || icmp_type == ICMP_ECHO_REPLY) { |
||||
is_icmp_echo = 1; memcpy(&src_port_net, transport + ICMP_ID_OFFSET, 2); |
||||
} |
||||
} |
||||
if (!is_tcp && !is_udp && !is_icmp_echo) return 0; |
||||
|
||||
struct eim_nat_entry* entry = eim_nat_find_egress(ctx, proto, src_ip_host, src_port_net); |
||||
uint16_t ext_port_host; |
||||
if (entry) { |
||||
ext_port_host = (uint16_t)(entry - ctx->table); |
||||
} else { |
||||
ext_port_host = eim_nat_alloc_port(ctx); |
||||
if (ext_port_host == 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT port range exhausted (%s:%u proto=%u)", |
||||
ip_host_to_str(src_ip_host).str, ntohs(src_port_net), proto); |
||||
return -1; |
||||
} |
||||
entry = &ctx->table[ext_port_host]; |
||||
entry->internal_ip = src_ip_host; |
||||
entry->internal_port = src_port_net; |
||||
entry->proto = proto; |
||||
entry->state = EIM_NAT_ENTRY_ACTIVE; |
||||
entry->src_node_id = src_node_id; |
||||
entry->src_conn = src_conn; |
||||
entry->last_seen = 0; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT map: %s:%u -> %s:%u (proto=%u) from node %016llx", |
||||
ip_host_to_str(src_ip_host).str, ntohs(src_port_net), |
||||
ip_host_to_str(ctx->gateway_ip).str, ext_port_host, proto, |
||||
(unsigned long long)src_node_id); |
||||
} |
||||
|
||||
uint32_t gw_ip_net = htonl(ctx->gateway_ip); |
||||
memcpy(ip_data + IP_SRC_ADDR_OFFSET, &gw_ip_net, 4); |
||||
csum_update_ip(ip_data, src_ip_host, ctx->gateway_ip); |
||||
|
||||
uint16_t new_port_net = htons(ext_port_host); |
||||
if (is_tcp) { |
||||
memcpy(transport + TCP_SRC_PORT_OFFSET, &new_port_net, 2); |
||||
csum_update_transport(transport, TCP_CHECKSUM_OFFSET, src_ip_host, ctx->gateway_ip, src_port_net, new_port_net); |
||||
} else if (is_udp) { |
||||
memcpy(transport + UDP_SRC_PORT_OFFSET, &new_port_net, 2); |
||||
csum_update_transport(transport, UDP_CHECKSUM_OFFSET, src_ip_host, ctx->gateway_ip, src_port_net, new_port_net); |
||||
} else if (is_icmp_echo) { |
||||
memcpy(transport + ICMP_ID_OFFSET, &new_port_net, 2); |
||||
csum_update_icmp(transport, src_port_net, new_port_net); |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len, |
||||
struct eim_nat_entry** out_entry) { |
||||
if (ip_len < IP_HDR_MIN_SIZE) return -1; |
||||
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F; |
||||
if (ihl < 5) return -1; |
||||
uint16_t ip_hdr_len = ihl * 4; |
||||
if (ip_len < ip_hdr_len) return -1; |
||||
|
||||
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6)); |
||||
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0; |
||||
|
||||
uint8_t proto = ip_data[IP_PROTO_OFFSET]; |
||||
uint32_t dst_ip_net; |
||||
memcpy(&dst_ip_net, ip_data + IP_DST_ADDR_OFFSET, 4); |
||||
if (dst_ip_net != htonl(ctx->gateway_ip)) return 0; |
||||
|
||||
uint8_t* transport = ip_data + ip_hdr_len; |
||||
size_t tlen = ip_len - ip_hdr_len; |
||||
uint16_t dst_port_net = 0; |
||||
int is_tcp = (proto == IPPROTO_TCP_UINT8 && tlen >= 20); |
||||
int is_udp = (proto == IPPROTO_UDP_UINT8 && tlen >= 8); |
||||
int is_icmp_echo = 0; |
||||
|
||||
if (is_tcp) memcpy(&dst_port_net, transport + TCP_DST_PORT_OFFSET, 2); |
||||
else if (is_udp) memcpy(&dst_port_net, transport + UDP_DST_PORT_OFFSET, 2); |
||||
else if (proto == IPPROTO_ICMP_UINT8 && tlen >= 8) { |
||||
uint8_t icmp_type = transport[ICMP_TYPE_OFFSET]; |
||||
if (icmp_type == ICMP_ECHO_REQUEST || icmp_type == ICMP_ECHO_REPLY) { |
||||
is_icmp_echo = 1; memcpy(&dst_port_net, transport + ICMP_ID_OFFSET, 2); |
||||
} |
||||
} |
||||
if (!is_tcp && !is_udp && !is_icmp_echo) return 0; |
||||
|
||||
uint16_t ext_port_host = ntohs(dst_port_net); |
||||
if (ext_port_host < ctx->port_start || ext_port_host > ctx->port_end) return 0; |
||||
|
||||
struct eim_nat_entry* entry = &ctx->table[ext_port_host]; |
||||
if (entry->state == EIM_NAT_ENTRY_FREE) return 0; |
||||
if (entry->proto != proto) return 0; |
||||
|
||||
uint32_t dst_ip_host = ntohl(dst_ip_net); |
||||
uint32_t internal_ip_net = htonl(entry->internal_ip); |
||||
memcpy(ip_data + IP_DST_ADDR_OFFSET, &internal_ip_net, 4); |
||||
csum_update_ip(ip_data, dst_ip_host, entry->internal_ip); |
||||
|
||||
uint16_t internal_port_net = entry->internal_port; |
||||
if (is_tcp) { |
||||
memcpy(transport + TCP_DST_PORT_OFFSET, &internal_port_net, 2); |
||||
csum_update_transport(transport, TCP_CHECKSUM_OFFSET, dst_ip_host, entry->internal_ip, dst_port_net, internal_port_net); |
||||
} else if (is_udp) { |
||||
memcpy(transport + UDP_DST_PORT_OFFSET, &internal_port_net, 2); |
||||
csum_update_transport(transport, UDP_CHECKSUM_OFFSET, dst_ip_host, entry->internal_ip, dst_port_net, internal_port_net); |
||||
} else if (is_icmp_echo) { |
||||
memcpy(transport + ICMP_ID_OFFSET, &internal_port_net, 2); |
||||
csum_update_icmp(transport, dst_port_net, internal_port_net); |
||||
} |
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT unmap: %s:%u <- %s:%u (proto=%u) back to node %016llx", |
||||
ip_host_to_str(entry->internal_ip).str, ntohs(entry->internal_port), |
||||
ip_host_to_str(ctx->gateway_ip).str, ext_port_host, proto, |
||||
(unsigned long long)entry->src_node_id); |
||||
|
||||
if (out_entry) *out_entry = entry; |
||||
return 0; |
||||
} |
||||
|
||||
// ==================== Init / Destroy ====================
|
||||
|
||||
int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g) { |
||||
if (!ctx || !g) return -1; |
||||
memset(ctx, 0, sizeof(*ctx)); |
||||
|
||||
if (!g->nat_enabled) return 0; |
||||
|
||||
if (g->nat_tun_ip.family == AF_INET) { |
||||
ctx->gateway_ip = ntohl(g->nat_tun_ip.addr.v4.s_addr); |
||||
} else if (g->tun_ip.family == AF_INET) { |
||||
ctx->gateway_ip = ntohl(g->tun_ip.addr.v4.s_addr); |
||||
} else { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No valid IP for NAT gateway"); |
||||
return -1; |
||||
} |
||||
|
||||
ctx->port_start = g->nat_port_start; |
||||
ctx->port_end = g->nat_port_end; |
||||
if (ctx->port_start == 0 || ctx->port_end == 0 || ctx->port_start >= ctx->port_end) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT port range: %u-%u", ctx->port_start, ctx->port_end); |
||||
return -1; |
||||
} |
||||
ctx->next_port = ctx->port_start; |
||||
|
||||
ctx->table_size = EIM_NAT_TABLE_SIZE; |
||||
ctx->table = u_calloc(ctx->table_size, sizeof(struct eim_nat_entry)); |
||||
if (!ctx->table) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT table"); return -1; } |
||||
|
||||
for (int i = 0; i < g->nat_forward_count; i++) { |
||||
uint16_t ext = g->nat_forwards[i].external_port; |
||||
if (ext >= ctx->table_size) continue; |
||||
struct eim_nat_entry* e = &ctx->table[ext]; |
||||
e->internal_ip = g->nat_forwards[i].internal_ip_host; |
||||
e->internal_port = g->nat_forwards[i].internal_port_net; |
||||
e->proto = g->nat_forwards[i].proto; |
||||
e->state = EIM_NAT_ENTRY_STATIC; |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: %s:%u <- :%u (proto=%u)", |
||||
ip_host_to_str(e->internal_ip).str, ntohs(e->internal_port), ext, e->proto); |
||||
} |
||||
|
||||
ctx->initialized = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u", |
||||
ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end); |
||||
return 0; |
||||
} |
||||
|
||||
void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx) { |
||||
if (!ctx || !ctx->initialized) return; |
||||
u_free(ctx->table); |
||||
memset(ctx, 0, sizeof(*ctx)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine destroyed"); |
||||
} |
||||
|
||||
int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto, |
||||
uint32_t internal_ip_host, uint16_t internal_port_net, |
||||
uint16_t external_port) { |
||||
if (!ctx || !ctx->initialized || external_port >= ctx->table_size) return -1; |
||||
if (ctx->table[external_port].state != EIM_NAT_ENTRY_FREE) return -1; |
||||
struct eim_nat_entry* e = &ctx->table[external_port]; |
||||
e->internal_ip = internal_ip_host; |
||||
e->internal_port = internal_port_net; |
||||
e->proto = proto; |
||||
e->state = EIM_NAT_ENTRY_STATIC; |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: %s:%u <- :%u (proto=%u)", |
||||
ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,54 @@
|
||||
#ifndef EIM_NAT_H |
||||
#define EIM_NAT_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
struct ETCP_CONN; |
||||
|
||||
#define EIM_NAT_TABLE_SIZE 65536 |
||||
|
||||
#define EIM_NAT_ENTRY_FREE 0 |
||||
#define EIM_NAT_ENTRY_ACTIVE 1 |
||||
#define EIM_NAT_ENTRY_STATIC 2 |
||||
|
||||
#define IPPROTO_TCP_UINT8 6 |
||||
#define IPPROTO_UDP_UINT8 17 |
||||
#define IPPROTO_ICMP_UINT8 1 |
||||
|
||||
struct eim_nat_entry { |
||||
uint32_t internal_ip; |
||||
uint16_t internal_port; |
||||
uint8_t proto; |
||||
uint8_t state; |
||||
uint64_t src_node_id; // which node sent the original packet
|
||||
uint64_t last_seen; |
||||
struct ETCP_CONN* src_conn; // cached connection for sendback
|
||||
}; |
||||
|
||||
// Pure NAT engine state (no transport/TUN/ETCP fields)
|
||||
struct eim_nat_ctx { |
||||
uint32_t gateway_ip; |
||||
uint16_t port_start; |
||||
uint16_t port_end; |
||||
uint16_t next_port; |
||||
struct eim_nat_entry* table; |
||||
size_t table_size; |
||||
int initialized; |
||||
}; |
||||
|
||||
struct global_config; |
||||
|
||||
int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g); |
||||
void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx); |
||||
|
||||
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len, |
||||
uint64_t src_node_id, struct ETCP_CONN* src_conn); |
||||
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len, |
||||
struct eim_nat_entry** out_entry); |
||||
|
||||
int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto, |
||||
uint32_t internal_ip_host, uint16_t internal_port_net, |
||||
uint16_t external_port); |
||||
|
||||
#endif |
||||
@ -0,0 +1,234 @@
|
||||
#include "nat_transport.h" |
||||
#include "eim_nat.h" |
||||
#include "etcp.h" |
||||
#include "utun_instance.h" |
||||
#include "config_parser.h" |
||||
#include "tun_if.h" |
||||
#include "etcp_api.h" |
||||
#include "route_bgp.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/ll_queue.h" |
||||
#include <string.h> |
||||
|
||||
#define NAT_HDR_SIZE 17 // cmd(1) + src_node_id(8) + dst_node_id(8)
|
||||
|
||||
static ip_str_t ip_host_to_str(uint32_t ip_host) { |
||||
struct in_addr a; a.s_addr = htonl(ip_host); |
||||
return ip_to_str(&a, AF_INET); |
||||
} |
||||
|
||||
// ==================== Callbacks ====================
|
||||
|
||||
// CLIENT: NAT TUN output → encapsulate in ETCP_ID_NAT → send to provider
|
||||
static void nat_transport_client_tun_out_cb(struct ll_queue* q, void* arg) { |
||||
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; |
||||
if (!inst) { queue_resume_callback(q); return; } |
||||
struct nat_transport_ctx* tr = &inst->nat_tr; |
||||
struct eim_nat_ctx* ctx = &inst->nat; |
||||
|
||||
struct ll_entry* pkt = queue_data_get(q); |
||||
if (!pkt) { queue_resume_callback(q); return; } |
||||
|
||||
if (!pkt->dgram || pkt->len < 2) { queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); return; } |
||||
|
||||
if (!tr->nat_via_conn) { |
||||
tr->nat_via_conn = route_bgp_find_conn_for_node(inst->bgp, tr->nat_via_node_id); |
||||
if (!tr->nat_via_conn) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "No connection to NAT provider %016llx, dropping", |
||||
(unsigned long long)tr->nat_via_node_id); |
||||
queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
size_t ip_len = pkt->len - 1; |
||||
size_t total_len = NAT_HDR_SIZE + ip_len; |
||||
uint8_t* new_dgram = u_malloc(total_len); |
||||
if (!new_dgram) { queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); return; } |
||||
|
||||
new_dgram[0] = ETCP_ID_NAT; |
||||
memcpy(new_dgram + 1, &tr->self_node_id, 8); |
||||
memcpy(new_dgram + 9, &tr->nat_via_node_id, 8); |
||||
memcpy(new_dgram + 17, pkt->dgram + 1, ip_len); |
||||
|
||||
struct ll_entry* new_entry = queue_entry_new(0); |
||||
if (!new_entry) { u_free(new_dgram); queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); return; } |
||||
new_entry->dgram = new_dgram; |
||||
new_entry->len = total_len; |
||||
|
||||
queue_dgram_free(pkt); queue_entry_free(pkt); |
||||
queue_resume_callback(q); |
||||
|
||||
int ret = etcp_send(tr->nat_via_conn, new_entry); |
||||
if (ret != 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "etcp_send to provider failed"); |
||||
queue_entry_free(new_entry); queue_dgram_free(new_entry); |
||||
} |
||||
} |
||||
|
||||
// PROVIDER: NAT TUN output (internet response) → ingress NAT → encapsulate ETCP_ID_NAT → send back
|
||||
static void nat_transport_provider_tun_out_cb(struct ll_queue* q, void* arg) { |
||||
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; |
||||
if (!inst) { queue_resume_callback(q); return; } |
||||
struct nat_transport_ctx* tr = &inst->nat_tr; |
||||
struct eim_nat_ctx* ctx = &inst->nat; |
||||
|
||||
struct ll_entry* pkt = queue_data_get(q); |
||||
if (!pkt) { queue_resume_callback(q); return; } |
||||
if (!pkt->dgram || pkt->len < 2) { queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); return; } |
||||
|
||||
struct eim_nat_entry* entry = NULL; |
||||
int ret = eim_nat_ingress(ctx, pkt->dgram + 1, pkt->len - 1, &entry); |
||||
if (ret != 0 || !entry || !entry->src_conn) { |
||||
if (ret == 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "Ingress NAT: no matching entry, dropping"); |
||||
queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); |
||||
return; |
||||
} |
||||
|
||||
size_t ip_len = pkt->len - 1; |
||||
size_t total_len = NAT_HDR_SIZE + ip_len; |
||||
uint8_t* new_dgram = u_malloc(total_len); |
||||
if (!new_dgram) { queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); return; } |
||||
|
||||
new_dgram[0] = ETCP_ID_NAT; |
||||
memcpy(new_dgram + 1, &tr->self_node_id, 8); |
||||
memcpy(new_dgram + 9, &entry->src_node_id, 8); |
||||
memcpy(new_dgram + 17, pkt->dgram + 1, ip_len); |
||||
|
||||
struct ll_entry* new_entry = queue_entry_new(0); |
||||
if (!new_entry) { u_free(new_dgram); queue_dgram_free(pkt); queue_entry_free(pkt); queue_resume_callback(q); return; } |
||||
new_entry->dgram = new_dgram; |
||||
new_entry->len = total_len; |
||||
|
||||
queue_dgram_free(pkt); queue_entry_free(pkt); |
||||
queue_resume_callback(q); |
||||
|
||||
int send_ret = etcp_send(entry->src_conn, new_entry); |
||||
if (send_ret != 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "etcp_send back to node %016llx failed", |
||||
(unsigned long long)entry->src_node_id); |
||||
queue_entry_free(new_entry); queue_dgram_free(new_entry); |
||||
} |
||||
} |
||||
|
||||
// ETCP_ID_NAT receive: CLIENT gets response, PROVIDER gets request
|
||||
static void nat_transport_etcp_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { |
||||
if (!conn || !entry || !entry->dgram || entry->len < NAT_HDR_SIZE) { |
||||
if (entry) { queue_entry_free(entry); queue_dgram_free(entry); } |
||||
return; |
||||
} |
||||
struct UTUN_INSTANCE* inst = conn->instance; |
||||
if (!inst) { queue_entry_free(entry); queue_dgram_free(entry); return; } |
||||
struct nat_transport_ctx* tr = &inst->nat_tr; |
||||
struct eim_nat_ctx* ctx = &inst->nat; |
||||
if (!ctx->initialized) { queue_entry_free(entry); queue_dgram_free(entry); return; } |
||||
|
||||
uint64_t src_node_id, dst_node_id; |
||||
memcpy(&src_node_id, entry->dgram + 1, 8); |
||||
memcpy(&dst_node_id, entry->dgram + 9, 8); |
||||
uint8_t* ip_data = entry->dgram + NAT_HDR_SIZE; |
||||
size_t ip_len = entry->len - NAT_HDR_SIZE; |
||||
|
||||
if (tr->nat_via_node_id != 0) { |
||||
// CLIENT: response from provider → write to NAT TUN
|
||||
if (tr->nat_tun) { |
||||
tun_write(tr->nat_tun, entry->dgram + NAT_HDR_SIZE - 1, ip_len + 1); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT client: received %zu bytes from provider", ip_len); |
||||
} |
||||
} else { |
||||
// PROVIDER: request from client → egress NAT → write to NAT TUN
|
||||
int ret = eim_nat_egress(ctx, ip_data, ip_len, src_node_id, conn); |
||||
if (ret < 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "Egress NAT failed"); |
||||
} else if (ret == 0 && tr->nat_tun) { |
||||
tun_write(tr->nat_tun, entry->dgram + NAT_HDR_SIZE - 1, ip_len + 1); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT provider: sent %zu bytes to internet", ip_len); |
||||
} |
||||
} |
||||
|
||||
queue_entry_free(entry); queue_dgram_free(entry); |
||||
} |
||||
|
||||
// ==================== Init / Destroy ====================
|
||||
|
||||
int nat_transport_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->config) return -1; |
||||
struct nat_transport_ctx* tr = &inst->nat_tr; |
||||
struct global_config* g = &inst->config->global; |
||||
|
||||
memset(tr, 0, sizeof(*tr)); |
||||
if (!g->nat_enabled) return 0; |
||||
|
||||
tr->self_node_id = inst->node_id; |
||||
tr->nat_via_node_id = g->nat_via_node_id; |
||||
|
||||
// Initialize NAT engine (table, forwards, gateway_ip) — only provider needs full init.
|
||||
// Client also gets minimal init so ctx->initialized==1 for callback check.
|
||||
if (tr->nat_via_node_id == 0) { |
||||
if (eim_nat_init_ctx(&inst->nat, g) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to init NAT engine"); |
||||
return -1; |
||||
} |
||||
} else { |
||||
// Client: minimal init — mark as initialized without allocating table
|
||||
inst->nat.initialized = 1; |
||||
} |
||||
|
||||
// Create NAT TUN
|
||||
const char* tun_name = g->nat_tun_ifname[0] ? g->nat_tun_ifname : "tun_nat"; |
||||
char ip_str[64] = ""; |
||||
if (g->nat_tun_ip.family == AF_INET) { |
||||
snprintf(ip_str, sizeof(ip_str), "%s", ip_to_str(&g->nat_tun_ip.addr.v4, AF_INET).str); |
||||
} else if (g->tun_ip.family == AF_INET) { |
||||
snprintf(ip_str, sizeof(ip_str), "%s", ip_to_str(&g->tun_ip.addr.v4, AF_INET).str); |
||||
} else { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No NAT TUN IP"); |
||||
eim_nat_destroy_ctx(&inst->nat); |
||||
return -1; |
||||
} |
||||
tr->nat_tun = tun_init_nat(inst->ua, tun_name, ip_str, g->mtu, g->tun_test_mode); |
||||
if (!tr->nat_tun) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to create NAT TUN %s ip=%s", tun_name, ip_str); |
||||
eim_nat_destroy_ctx(&inst->nat); |
||||
return -1; |
||||
} |
||||
|
||||
// Bind ETCP_ID_NAT
|
||||
if (etcp_bind(inst, ETCP_ID_NAT, nat_transport_etcp_recv_cb) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to bind ETCP_ID_NAT"); |
||||
tun_close(tr->nat_tun); tr->nat_tun = NULL; |
||||
eim_nat_destroy_ctx(&inst->nat); |
||||
return -1; |
||||
} |
||||
|
||||
// Role-specific TUN callback
|
||||
struct eim_nat_ctx* ctx = &inst->nat; |
||||
if (tr->nat_via_node_id != 0) { |
||||
if (tr->nat_tun->output_queue) queue_set_callback(tr->nat_tun->output_queue, nat_transport_client_tun_out_cb, inst); |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT client via node %016llx, TUN=%s gw=%s ports=%u-%u", |
||||
(unsigned long long)tr->nat_via_node_id, tun_name, |
||||
ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end); |
||||
} else { |
||||
if (tr->nat_tun->output_queue) queue_set_callback(tr->nat_tun->output_queue, nat_transport_provider_tun_out_cb, inst); |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT provider TUN=%s gw=%s ports=%u-%u", |
||||
tun_name, ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end); |
||||
} |
||||
|
||||
tr->initialized = 1; |
||||
return 0; |
||||
} |
||||
|
||||
void nat_transport_destroy(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->nat_tr.initialized) return; |
||||
struct nat_transport_ctx* tr = &inst->nat_tr; |
||||
|
||||
etcp_unbind(inst, ETCP_ID_NAT); |
||||
|
||||
if (tr->nat_tun) { tun_close(tr->nat_tun); tr->nat_tun = NULL; } |
||||
|
||||
eim_nat_destroy_ctx(&inst->nat); |
||||
|
||||
memset(tr, 0, sizeof(*tr)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT transport destroyed"); |
||||
} |
||||
@ -0,0 +1,23 @@
|
||||
// nat_transport.h — NAT transport layer (TUN + ETCP protocol handling)
|
||||
#ifndef NAT_TRANSPORT_H |
||||
#define NAT_TRANSPORT_H |
||||
|
||||
#include <stdint.h> |
||||
|
||||
struct tun_if; |
||||
struct ETCP_CONN; |
||||
|
||||
struct nat_transport_ctx { |
||||
uint64_t self_node_id; |
||||
uint64_t nat_via_node_id; |
||||
struct tun_if* nat_tun; |
||||
struct ETCP_CONN* nat_via_conn; |
||||
int initialized; |
||||
}; |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
int nat_transport_init(struct UTUN_INSTANCE* inst); |
||||
void nat_transport_destroy(struct UTUN_INSTANCE* inst); |
||||
|
||||
#endif |
||||
@ -0,0 +1,791 @@
|
||||
/**
|
||||
* @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 <arpa/inet.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 htonl/htons + 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 = htons(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 s_net = htonl(src_host), d_net = htonl(dst_host); |
||||
memcpy(buf + 12, &s_net, 4); |
||||
memcpy(buf + 16, &d_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 = htonl(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 = htons(src_port_host), dp = htons(dst_port_host); |
||||
memcpy(pkt + 20, &sp, 2); // src port
|
||||
memcpy(pkt + 22, &dp, 2); // dst port
|
||||
uint16_t udp_len = htons(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, htons(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, htons(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(ntohl(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(ntohs(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, htons(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 = htons(src_port_host), dp = htons(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(ntohl(new_src), TEST_GW_HOST, "src IP=gw"); |
||||
// Check src port
|
||||
uint16_t new_sport; memcpy(&new_sport, pkt + 20, 2); |
||||
ASSERT_EQ(ntohs(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 = htons(id_host), seq_n = htons(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(ntohs(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, htons(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 = htons(TEST_SRC_PORT), dp = htons(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(ntohl(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(ntohl(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 = htons(TEST_PORT_START); // the port allocated by egress
|
||||
uint16_t seq = htons(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(ntohs(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, htons(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, htons(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, htons(8080), 10050); |
||||
int r = eim_nat_add_forward(&ctx, IPPROTO_UDP_UINT8, 0x0A0000FF, htons(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, htons(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(ntohl(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, htons(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(ntohs(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; |
||||
} |
||||
@ -0,0 +1,494 @@
|
||||
/**
|
||||
* @file test_nat_stress.c |
||||
* @brief Стресс-тесты NAT engine: множество сессий, заполнение таблицы, многопоточная нагрузка. |
||||
* |
||||
* Тестирует только eim_nat.c/h (чистый engine), без TUN/ETCP. |
||||
* Измеряет время операций и проверяет корректность при предельных нагрузках. |
||||
*/ |
||||
|
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <arpa/inet.h> |
||||
#include <sys/time.h> |
||||
#include "../lib/debug_config.h" |
||||
#include "../src/eim_nat.h" |
||||
#include "../src/etcp.h" |
||||
#include "../src/config_parser.h" |
||||
|
||||
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) |
||||
|
||||
static double now_ms(void) { |
||||
struct timeval tv; gettimeofday(&tv, NULL); |
||||
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0; |
||||
} |
||||
|
||||
static uint8_t* make_udp_pkt(uint32_t src_host, uint16_t src_port_host, uint32_t dst_host, uint16_t dst_port_host, size_t* out_len) { |
||||
const size_t len = 20 + 8 + 14; |
||||
uint8_t* pkt = calloc(1, len); |
||||
pkt[0] = 0x45; pkt[1] = 0x00; |
||||
uint16_t tot = htons((uint16_t)len); memcpy(pkt + 2, &tot, 2); |
||||
pkt[4] = 0x12; pkt[5] = 0x34; |
||||
memset(pkt + 6, 0, 2); |
||||
pkt[8] = 64; pkt[9] = IPPROTO_UDP_UINT8; |
||||
memset(pkt + 10, 0, 2); |
||||
uint32_t sn = htonl(src_host), dn = htonl(dst_host); |
||||
memcpy(pkt + 12, &sn, 4); memcpy(pkt + 16, &dn, 4); |
||||
uint16_t sp = htons(src_port_host), dp = htons(dst_port_host); |
||||
memcpy(pkt + 20, &sp, 2); memcpy(pkt + 22, &dp, 2); |
||||
uint16_t ul = htons(8 + 14); memcpy(pkt + 24, &ul, 2); |
||||
memset(pkt + 26, 0, 2); |
||||
memset(pkt + 28, 0xAB, 14); |
||||
uint32_t sum = 0; |
||||
for (int i = 0; i < 10; i++) { uint16_t w; memcpy(&w, pkt + i*2, 2); sum += w; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum += (sum >> 16); |
||||
uint16_t cs = (uint16_t)(~sum); |
||||
memcpy(pkt + 10, &cs, 2); |
||||
*out_len = len; |
||||
return pkt; |
||||
} |
||||
|
||||
static int verify_ip_checksum(const 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; |
||||
} |
||||
|
||||
static struct ETCP_CONN mock_conn; |
||||
static struct ETCP_CONN* get_mock_conn(void) { |
||||
static int once = 0; |
||||
if (!once) { memset(&mock_conn, 0, sizeof(mock_conn)); mock_conn.peer_node_id = 1; once = 1; } |
||||
return &mock_conn; |
||||
} |
||||
|
||||
static struct global_config make_config(uint16_t port_start, uint16_t port_end) { |
||||
struct global_config g; |
||||
memset(&g, 0, sizeof(g)); |
||||
g.nat_enabled = 1; |
||||
g.nat_port_start = port_start; |
||||
g.nat_port_end = port_end; |
||||
g.nat_tun_ip.family = AF_INET; |
||||
g.nat_tun_ip.addr.v4.s_addr = htonl(0x0A000001); // 10.0.0.1
|
||||
return g; |
||||
} |
||||
|
||||
// ==================== Stress tests ====================
|
||||
|
||||
static void stress_many_sessions(void) { |
||||
/* 10000 sequential egress operations */ |
||||
TEST("stress_many_sessions_10k"); |
||||
{ |
||||
struct global_config g = make_config(10000, 20000); // 10001 ports
|
||||
struct eim_nat_ctx ctx; |
||||
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "init"); |
||||
|
||||
double t0 = now_ms(); |
||||
const int N = 10000; |
||||
for (int i = 0; i < N; i++) { |
||||
uint16_t sport = 50000 + (uint16_t)i; |
||||
size_t len; |
||||
uint8_t* pkt = make_udp_pkt(0x0A000002, sport, 0x08080808, 53, &len); |
||||
int r = eim_nat_egress(&ctx, pkt, len, (uint64_t)(i+1), get_mock_conn()); |
||||
ASSERT_EQ(r, 0, "egress"); |
||||
free(pkt); |
||||
} |
||||
double t1 = now_ms(); |
||||
|
||||
ASSERT_EQ(ctx.table[10000].state, EIM_NAT_ENTRY_ACTIVE, "first active"); |
||||
ASSERT_EQ(ctx.table[10000+N-1].state, EIM_NAT_ENTRY_ACTIVE, "last active"); |
||||
ASSERT_EQ(ctx.table[10000+N].state, EIM_NAT_ENTRY_FREE, "next free"); |
||||
|
||||
printf("%d egress in %.1fms (%.0f/sec) ", N, t1 - t0, N / ((t1 - t0) / 1000.0)); |
||||
eim_nat_destroy_ctx(&ctx); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
static void stress_table_full_and_lookup(void) { |
||||
/* Fill entire port range, then ingress lookup */ |
||||
TEST("stress_table_full_lookup"); |
||||
{ |
||||
int n_ports = 1000; |
||||
struct global_config g = make_config(1000, 1000 + n_ports - 1); |
||||
struct eim_nat_ctx ctx; |
||||
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "init"); |
||||
|
||||
// Fill all ports
|
||||
for (int i = 0; i < n_ports; i++) { |
||||
uint16_t sport = 40000 + (uint16_t)i; |
||||
size_t len; |
||||
uint8_t* pkt = make_udp_pkt(0x0A000002, sport, 0x08080808, 53, &len); |
||||
eim_nat_egress(&ctx, pkt, len, (uint64_t)i, get_mock_conn()); |
||||
free(pkt); |
||||
} |
||||
ASSERT_EQ(ctx.next_port, 1000, "wrapped to start"); |
||||
|
||||
// Verify exhaustion
|
||||
{ |
||||
size_t len; |
||||
uint8_t* pkt = make_udp_pkt(0x0A000002, 40000 + n_ports, 0x08080808, 53, &len); |
||||
int r = eim_nat_egress(&ctx, pkt, len, 9999, get_mock_conn()); |
||||
ASSERT_EQ(r, -1, "exhaustion"); |
||||
free(pkt); |
||||
} |
||||
|
||||
// Ingress lookup: verify ALL entries work correctly
|
||||
double t0 = now_ms(); |
||||
for (int i = 0; i < n_ports; i++) { |
||||
uint16_t ext_port = 1000 + i; |
||||
size_t len; |
||||
uint8_t* resp = make_udp_pkt(0x08080808, 53, 0x0A000001, ext_port, &len); |
||||
struct eim_nat_entry* entry = NULL; |
||||
int r = eim_nat_ingress(&ctx, resp, len, &entry); |
||||
ASSERT_EQ(r, 0, "ingress"); |
||||
ASSERT(entry != NULL, "entry non-null"); |
||||
ASSERT_EQ(entry - ctx.table, ext_port, "entry index"); |
||||
ASSERT_EQ(entry->internal_ip, 0x0A000002, "internal IP"); |
||||
uint16_t expected_port_net = htons(40000 + i); |
||||
ASSERT_EQ(entry->internal_port, expected_port_net, "internal port"); |
||||
ASSERT(verify_ip_checksum(resp), "IP checksum"); |
||||
free(resp); |
||||
} |
||||
double t1 = now_ms(); |
||||
printf("%d ingress lookups in %.1fms (%.0f/sec) ", n_ports, t1 - t0, n_ports / ((t1 - t0) / 1000.0)); |
||||
|
||||
eim_nat_destroy_ctx(&ctx); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
static void stress_egress_ingress_cycle(void) { |
||||
/* Repeated allocate-free cycles: egress → ingress → free entry → repeat */ |
||||
TEST("stress_cyclic_allocate_free"); |
||||
{ |
||||
struct global_config g = make_config(1000, 1001); // 2 ports
|
||||
struct eim_nat_ctx ctx; |
||||
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "init"); |
||||
|
||||
const int CYCLES = 5000; |
||||
size_t len; |
||||
for (int cycle = 0; cycle < CYCLES; cycle++) { |
||||
uint8_t* pkt = make_udp_pkt(0x0A000002, 40000, 0x08080808, 53, &len); |
||||
int r = eim_nat_egress(&ctx, pkt, len, 0x1111, get_mock_conn()); |
||||
ASSERT_EQ(r, 0, "egress"); |
||||
free(pkt); |
||||
|
||||
// Ingress with response
|
||||
uint16_t ext_port = ctx.next_port == 1000 ? 1001 : 1000; // find the allocated port
|
||||
// Actually the alloc function uses next_port. First alloc gets 1000, next_port becomes 1001.
|
||||
// But the cycle reuses the same internal ip:port, so it finds existing entry in eim_nat_find_egress.
|
||||
// Let's use a different approach: free the entry, then re-egress.
|
||||
// Actually, same (ip:port:proto) always reuses same port. Let's clean entry manually.
|
||||
struct eim_nat_entry* e = &ctx.table[1000]; |
||||
if (e->state != EIM_NAT_ENTRY_ACTIVE) { FAIL("entry not active"); eim_nat_destroy_ctx(&ctx); return; } |
||||
|
||||
uint8_t* resp = make_udp_pkt(0x08080808, 53, 0x0A000001, 1000, &len); |
||||
struct eim_nat_entry* entry = NULL; |
||||
r = eim_nat_ingress(&ctx, resp, len, &entry); |
||||
ASSERT_EQ(r, 0, "ingress"); |
||||
ASSERT(entry == e, "same entry"); |
||||
ASSERT(verify_ip_checksum(resp), "IP checksum after ingress"); |
||||
free(resp); |
||||
|
||||
// Free entry for next cycle
|
||||
memset(e, 0, sizeof(*e)); |
||||
ctx.next_port = 1000; |
||||
} |
||||
|
||||
eim_nat_destroy_ctx(&ctx); |
||||
printf("%d cycles OK ", CYCLES); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
static void stress_concurrent_different_flows(void) { |
||||
/* Many different internal IP:port combos, verify each gets unique external port */ |
||||
TEST("stress_unique_port_mapping"); |
||||
{ |
||||
struct global_config g = make_config(20000, 20099); // 100 ports
|
||||
struct eim_nat_ctx ctx; |
||||
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "init"); |
||||
|
||||
const int N = 100; |
||||
for (int i = 0; i < N; i++) { |
||||
uint32_t int_ip = 0x0A000002 + (uint32_t)(i / 10); // 10 IPs, 10 ports each
|
||||
uint16_t int_port = 40000 + (uint16_t)(i % 10); |
||||
size_t len; |
||||
uint8_t* pkt = make_udp_pkt(int_ip, int_port, 0x08080808, 53, &len); |
||||
int r = eim_nat_egress(&ctx, pkt, len, (uint64_t)i, get_mock_conn()); |
||||
ASSERT_EQ(r, 0, "egress"); |
||||
free(pkt); |
||||
} |
||||
|
||||
// Verify all ports are used
|
||||
for (int i = 0; i < N; i++) { |
||||
ASSERT(ctx.table[20000 + i].state == EIM_NAT_ENTRY_ACTIVE, "port active"); |
||||
} |
||||
|
||||
// Verify unique (internal_ip, internal_port) → unique external_port
|
||||
for (int i = 0; i < N; i++) { |
||||
struct eim_nat_entry* ei = &ctx.table[20000 + i]; |
||||
for (int j = i + 1; j < N; j++) { |
||||
struct eim_nat_entry* ej = &ctx.table[20000 + j]; |
||||
// Different entries should NOT map the same (internal_ip, internal_port)
|
||||
if (ei->internal_ip == ej->internal_ip && ei->internal_port == ej->internal_port) { |
||||
FAIL("duplicate flow mapping"); |
||||
eim_nat_destroy_ctx(&ctx); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Verify ingress: each flow responds correctly
|
||||
for (int i = 0; i < N; i++) { |
||||
uint16_t ext_port = 20000 + i; |
||||
size_t len; |
||||
uint8_t* resp = make_udp_pkt(0x08080808, 53, 0x0A000001, ext_port, &len); |
||||
struct eim_nat_entry* entry = NULL; |
||||
int r = eim_nat_ingress(&ctx, resp, len, &entry); |
||||
ASSERT_EQ(r, 0, "ingress"); |
||||
ASSERT(entry == &ctx.table[ext_port], "entry match"); |
||||
ASSERT(verify_ip_checksum(resp), "IP checksum"); |
||||
free(resp); |
||||
} |
||||
|
||||
eim_nat_destroy_ctx(&ctx); |
||||
printf("%d unique flows OK ", N); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
static void stress_tcp_mixed_with_udp(void) { |
||||
/* Mix TCP and UDP flows on same NAT */ |
||||
TEST("stress_tcp_udp_mixed"); |
||||
{ |
||||
struct global_config g = make_config(30000, 30099); |
||||
struct eim_nat_ctx ctx; |
||||
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "init"); |
||||
|
||||
// 50 UDP flows + 50 TCP flows
|
||||
for (int i = 0; i < 50; i++) { |
||||
uint16_t sport = 40000 + i; |
||||
size_t len; |
||||
uint8_t* pkt = make_udp_pkt(0x0A000002, sport, 0x08080808, 53, &len); |
||||
eim_nat_egress(&ctx, pkt, len, i, get_mock_conn()); |
||||
free(pkt); |
||||
} |
||||
for (int i = 0; i < 50; i++) { |
||||
uint16_t sport = 40000 + i; |
||||
const size_t tcp_len = 20 + 20 + 14; |
||||
uint8_t* pkt = calloc(1, tcp_len); |
||||
pkt[0] = 0x45; pkt[1] = 0x00; |
||||
uint16_t tot = htons((uint16_t)tcp_len); memcpy(pkt + 2, &tot, 2); |
||||
pkt[4] = 0x12; pkt[5] = 0x34; |
||||
memset(pkt + 6, 0, 2); |
||||
pkt[8] = 64; pkt[9] = IPPROTO_TCP_UINT8; |
||||
memset(pkt + 10, 0, 2); |
||||
uint32_t sn = htonl(0x0A000002), dn = htonl(0x08080808); |
||||
memcpy(pkt + 12, &sn, 4); memcpy(pkt + 16, &dn, 4); |
||||
uint16_t sp = htons(sport), dp = htons(80); |
||||
memcpy(pkt + 20, &sp, 2); memcpy(pkt + 22, &dp, 2); |
||||
pkt[32] = 0x50; |
||||
memset(pkt + 36, 0, 2); |
||||
memset(pkt + 40, 0xCC, 14); |
||||
uint32_t sum = 0; |
||||
for (int k = 0; k < 10; k++) { uint16_t w; memcpy(&w, pkt + k*2, 2); sum += w; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum += (sum >> 16); |
||||
uint16_t cs = (uint16_t)(~sum); memcpy(pkt + 10, &cs, 2); |
||||
eim_nat_egress(&ctx, pkt, tcp_len, 100 + i, get_mock_conn()); |
||||
free(pkt); |
||||
} |
||||
|
||||
// Verify: 100 entries total, same internal_port with different proto OK
|
||||
int udp_count = 0, tcp_count = 0; |
||||
for (uint16_t p = 30000; p <= 30099; p++) { |
||||
if (ctx.table[p].state == EIM_NAT_ENTRY_ACTIVE) { |
||||
if (ctx.table[p].proto == IPPROTO_UDP_UINT8) udp_count++; |
||||
else if (ctx.table[p].proto == IPPROTO_TCP_UINT8) tcp_count++; |
||||
} |
||||
} |
||||
ASSERT_EQ(udp_count, 50, "50 UDP entries"); |
||||
ASSERT_EQ(tcp_count, 50, "50 TCP entries"); |
||||
|
||||
// Ingress TCP: use same source internal_ip:port but want response
|
||||
// The TCP entry for internal port 40000 is at some external port. Let's find it.
|
||||
uint16_t tcp_ext_port = 0; |
||||
for (uint16_t p = 30000; p <= 30099; p++) { |
||||
if (ctx.table[p].state == EIM_NAT_ENTRY_ACTIVE && ctx.table[p].proto == IPPROTO_TCP_UINT8) { |
||||
tcp_ext_port = p; break; |
||||
} |
||||
} |
||||
ASSERT(tcp_ext_port > 0, "found TCP port"); |
||||
|
||||
// Send ingress TCP to that port
|
||||
{ |
||||
const size_t tcp_len = 20 + 20 + 14; |
||||
uint8_t* resp = calloc(1, tcp_len); |
||||
resp[0] = 0x45; resp[1] = 0x00; |
||||
uint16_t tot = htons((uint16_t)tcp_len); memcpy(resp + 2, &tot, 2); |
||||
resp[4] = 0x12; resp[5] = 0x34; |
||||
memset(resp + 6, 0, 2); |
||||
resp[8] = 64; resp[9] = IPPROTO_TCP_UINT8; |
||||
memset(resp + 10, 0, 2); |
||||
uint32_t sn = htonl(0x08080808), dn = htonl(0x0A000001); |
||||
memcpy(resp + 12, &sn, 4); memcpy(resp + 16, &dn, 4); |
||||
uint16_t sp = htons(80), dp = htons(tcp_ext_port); |
||||
memcpy(resp + 20, &sp, 2); memcpy(resp + 22, &dp, 2); |
||||
resp[32] = 0x50; |
||||
memset(resp + 36, 0, 2); |
||||
memset(resp + 40, 0xCC, 14); |
||||
uint32_t sum = 0; |
||||
for (int k = 0; k < 10; k++) { uint16_t w; memcpy(&w, resp + k*2, 2); sum += w; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum += (sum >> 16); |
||||
uint16_t cs = (uint16_t)(~sum); memcpy(resp + 10, &cs, 2); |
||||
struct eim_nat_entry* entry = NULL; |
||||
int r = eim_nat_ingress(&ctx, resp, tcp_len, &entry); |
||||
ASSERT_EQ(r, 0, "TCP ingress"); |
||||
ASSERT(entry != NULL, "TCP entry non-null"); |
||||
ASSERT_EQ(entry->proto, IPPROTO_TCP_UINT8, "proto=TCP"); |
||||
ASSERT(verify_ip_checksum(resp), "IP checksum"); |
||||
free(resp); |
||||
} |
||||
|
||||
eim_nat_destroy_ctx(&ctx); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
static void stress_icmp_mixed(void) { |
||||
/* ICMP Echo + UDP mix in same NAT */ |
||||
TEST("stress_icmp_udp_mixed"); |
||||
{ |
||||
struct global_config g = make_config(40000, 40099); |
||||
struct eim_nat_ctx ctx; |
||||
ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "init"); |
||||
|
||||
// 10 ICMP Echo + 10 UDP flows
|
||||
for (int i = 0; i < 10; i++) { |
||||
uint16_t icmp_id = 0x1000 + i; |
||||
const size_t len = 20 + 8 + 14; |
||||
uint8_t* pkt = calloc(1, len); |
||||
pkt[0] = 0x45; pkt[1] = 0x00; |
||||
uint16_t tot = htons((uint16_t)len); memcpy(pkt + 2, &tot, 2); |
||||
pkt[4] = 0x12; pkt[5] = 0x34; |
||||
memset(pkt + 6, 0, 2); |
||||
pkt[8] = 64; pkt[9] = IPPROTO_ICMP_UINT8; |
||||
memset(pkt + 10, 0, 2); |
||||
uint32_t sn = htonl(0x0A000002), dn = htonl(0x08080808); |
||||
memcpy(pkt + 12, &sn, 4); memcpy(pkt + 16, &dn, 4); |
||||
pkt[20] = 8; pkt[21] = 0; |
||||
memset(pkt + 22, 0, 2); |
||||
uint16_t id_n = htons(icmp_id), seq_n = htons(1); |
||||
memcpy(pkt + 24, &id_n, 2); memcpy(pkt + 26, &seq_n, 2); |
||||
memset(pkt + 28, 0xDD, 14); |
||||
uint32_t sum = 0; |
||||
for (int k = 0; k < 10; k++) { uint16_t w; memcpy(&w, pkt + k*2, 2); sum += w; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum += (sum >> 16); |
||||
uint16_t cs = (uint16_t)(~sum); memcpy(pkt + 10, &cs, 2); |
||||
eim_nat_egress(&ctx, pkt, len, i, get_mock_conn()); |
||||
free(pkt); |
||||
} |
||||
for (int i = 0; i < 10; i++) { |
||||
size_t len; |
||||
uint8_t* pkt = make_udp_pkt(0x0A000002, 50000 + i, 0x08080808, 53, &len); |
||||
eim_nat_egress(&ctx, pkt, len, 100 + i, get_mock_conn()); |
||||
free(pkt); |
||||
} |
||||
|
||||
int icmp_ct = 0, udp_ct = 0; |
||||
for (uint16_t p = 40000; p <= 40099; p++) { |
||||
if (ctx.table[p].state == EIM_NAT_ENTRY_ACTIVE) { |
||||
if (ctx.table[p].proto == IPPROTO_ICMP_UINT8) icmp_ct++; |
||||
else if (ctx.table[p].proto == IPPROTO_UDP_UINT8) udp_ct++; |
||||
} |
||||
} |
||||
ASSERT_EQ(icmp_ct, 10, "10 ICMP"); |
||||
ASSERT_EQ(udp_ct, 10, "10 UDP"); |
||||
|
||||
// ICMP ingress: reply to first ICMP entry
|
||||
uint16_t icmp_ext = 0; |
||||
for (uint16_t p = 40000; p <= 40099; p++) { |
||||
if (ctx.table[p].state == EIM_NAT_ENTRY_ACTIVE && ctx.table[p].proto == IPPROTO_ICMP_UINT8) { icmp_ext = p; break; } |
||||
} |
||||
{ |
||||
const size_t len = 20 + 8 + 14; |
||||
uint8_t* reply = calloc(1, len); |
||||
reply[0] = 0x45; reply[1] = 0x00; |
||||
uint16_t tot = htons((uint16_t)len); memcpy(reply + 2, &tot, 2); |
||||
reply[4] = 0x12; reply[5] = 0x34; |
||||
memset(reply + 6, 0, 2); |
||||
reply[8] = 64; reply[9] = IPPROTO_ICMP_UINT8; |
||||
memset(reply + 10, 0, 2); |
||||
uint32_t sn = htonl(0x08080808), dn = htonl(0x0A000001); |
||||
memcpy(reply + 12, &sn, 4); memcpy(reply + 16, &dn, 4); |
||||
reply[20] = 0; reply[21] = 0; |
||||
memset(reply + 22, 0, 2); |
||||
uint16_t ext_id_net = htons(icmp_ext), seq_n = htons(1); |
||||
memcpy(reply + 24, &ext_id_net, 2); memcpy(reply + 26, &seq_n, 2); |
||||
memset(reply + 28, 0xDD, 14); |
||||
uint32_t sum = 0; |
||||
for (int k = 0; k < 10; k++) { uint16_t w; memcpy(&w, reply + k*2, 2); sum += w; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum += (sum >> 16); |
||||
uint16_t cs = (uint16_t)(~sum); memcpy(reply + 10, &cs, 2); |
||||
struct eim_nat_entry* entry = NULL; |
||||
int r = eim_nat_ingress(&ctx, reply, len, &entry); |
||||
ASSERT_EQ(r, 0, "ICMP ingress"); |
||||
ASSERT(entry != NULL, "ICMP entry"); |
||||
ASSERT(verify_ip_checksum(reply), "IP checksum"); |
||||
free(reply); |
||||
} |
||||
|
||||
eim_nat_destroy_ctx(&ctx); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
static void stress_large_table_init_free(void) { |
||||
/* Verify 65536-entry table alloc/free is fast and doesn't leak */ |
||||
TEST("stress_table_alloc_free_65k"); |
||||
{ |
||||
struct global_config g = make_config(10000, 20000); |
||||
double t0 = now_ms(); |
||||
for (int i = 0; i < 100; i++) { |
||||
struct eim_nat_ctx ctx; |
||||
int r = eim_nat_init_ctx(&ctx, &g); |
||||
ASSERT_EQ(r, 0, "init"); |
||||
eim_nat_destroy_ctx(&ctx); |
||||
} |
||||
double t1 = now_ms(); |
||||
printf("100 alloc/free in %.1fms (%.0f/sec) ", t1 - t0, 100.0 / ((t1 - t0) / 1000.0)); |
||||
} |
||||
PASS(); |
||||
} |
||||
|
||||
// ==================== Main ====================
|
||||
int main(void) { |
||||
debug_config_init(); |
||||
debug_set_level(DEBUG_LEVEL_ERROR); |
||||
|
||||
stress_many_sessions(); |
||||
stress_table_full_and_lookup(); |
||||
stress_egress_ingress_cycle(); |
||||
stress_concurrent_different_flows(); |
||||
stress_tcp_mixed_with_udp(); |
||||
stress_icmp_mixed(); |
||||
stress_large_table_init_free(); |
||||
|
||||
printf("\n=== Stress Results: %d run, %d passed, %d failed ===\n", |
||||
stats.run, stats.passed, stats.failed); |
||||
return stats.failed ? 1 : 0; |
||||
} |
||||
@ -0,0 +1,612 @@
|
||||
/**
|
||||
* @file test_nat_transport.c |
||||
* @brief Интеграционный тест NAT transport layer (nat_transport.c/h) |
||||
* |
||||
* Создаёт два UTUN_INSTANCE (provider + client), инициализирует NAT транспорт |
||||
* и тестирует полный цикл: client → provider (egress) → provider TUN (internet) |
||||
* → provider (ingress) → client (response). |
||||
* |
||||
* Из-за особенностей test-mode TUN (tun_write → output_queue), на клиентской |
||||
* стороне для приёма response используется capture-callback, предотвращая loop. |
||||
*/ |
||||
|
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <unistd.h> |
||||
#include <arpa/inet.h> |
||||
#include "test_utils.h" |
||||
#include "../src/etcp.h" |
||||
#include "../src/etcp_connections.h" |
||||
#include "../src/config_parser.h" |
||||
#include "../src/config_updater.h" |
||||
#include "../src/utun_instance.h" |
||||
#include "../src/routing.h" |
||||
#include "../src/route_bgp.h" |
||||
#include "../src/tun_if.h" |
||||
#include "../src/nat_transport.h" |
||||
#include "../src/eim_nat.h" |
||||
#include "../src/etcp_api.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/ll_queue.h" |
||||
|
||||
#define TEST_TIMEOUT_MS 15000 |
||||
#define NODE_ID_PROVIDER 0xAAAA000000000001ULL |
||||
#define NODE_ID_CLIENT 0xBBBB000000000001ULL |
||||
#define NAT_HDR_SIZE 17 // cmd(1) + src_node_id(8) + dst_node_id(8)
|
||||
|
||||
static struct UTUN_INSTANCE* inst_provider = NULL; |
||||
static struct UTUN_INSTANCE* inst_client = NULL; |
||||
static struct UASYNC* ua = NULL; |
||||
static int test_timed_out = 0; |
||||
static void* test_timeout_id = NULL; |
||||
|
||||
static char temp_dir[] = "/tmp/utun_nat_transport_XXXXXX"; |
||||
static char config_provider[256]; |
||||
static char config_client[256]; |
||||
|
||||
/* Test result tracking */ |
||||
static struct { |
||||
int done; |
||||
uint8_t captured[1500]; |
||||
size_t captured_len; |
||||
int capture_count; |
||||
|
||||
int provider_egress_entry; |
||||
uint64_t egress_src_node_id; |
||||
uint32_t egress_internal_ip; |
||||
uint16_t egress_internal_port_net; |
||||
uint16_t egress_external_port; |
||||
uint8_t egress_proto; |
||||
int provider_ingress_done; |
||||
int client_response_done; |
||||
} test_state; |
||||
|
||||
static int write_config(const char* path, const char* content) { |
||||
FILE* f = fopen(path, "w"); |
||||
if (!f) return -1; |
||||
fprintf(f, "%s", content); |
||||
fclose(f); |
||||
return 0; |
||||
} |
||||
|
||||
static char* get_pubkey_from_config(const char* path) { |
||||
struct utun_config* cfg = parse_config(path); |
||||
if (!cfg) return NULL; |
||||
char* pub = strdup(cfg->global.my_public_key_hex); |
||||
free_config(cfg); |
||||
return pub; |
||||
} |
||||
|
||||
static int create_temp_configs(void) { |
||||
if (test_mkdtemp(temp_dir) != 0) { |
||||
fprintf(stderr, "Failed to create temp directory\n"); |
||||
return -1; |
||||
} |
||||
snprintf(config_provider, sizeof(config_provider), "%s/provider.conf", temp_dir); |
||||
snprintf(config_client, sizeof(config_client), "%s/client.conf", temp_dir); |
||||
|
||||
const char* tpl_provider = |
||||
"[global]\n" |
||||
"my_node_id=0xAAAA000000000001\n" |
||||
"tun_ip=10.100.0.1/24\n" |
||||
"tun_ifname=tun_provider\n" |
||||
"tun_test_mode=1\n" |
||||
"\n" |
||||
"[server:prov]\n" |
||||
"addr=127.0.0.1:39101\n" |
||||
"type=public\n" |
||||
"\n" |
||||
"[allowed_keys]\n" |
||||
"allow_all=1\n" |
||||
"\n" |
||||
"[nat]\n" |
||||
"enabled=1\n" |
||||
"tun_ifname=tun_nat\n" |
||||
"tun_ip=100.64.0.1/24\n" |
||||
"port_start=20000\n" |
||||
"port_end=20099\n"; |
||||
if (write_config(config_provider, tpl_provider) != 0) return -1; |
||||
if (config_ensure_keys_and_node_id(config_provider) != 0) return -1; |
||||
char* pub_prov = get_pubkey_from_config(config_provider); |
||||
if (!pub_prov) return -1; |
||||
|
||||
char tpl_client_full[4096]; |
||||
snprintf(tpl_client_full, sizeof(tpl_client_full), |
||||
"[global]\n" |
||||
"my_node_id=0xBBBB000000000001\n" |
||||
"tun_ip=10.200.0.1/24\n" |
||||
"tun_ifname=tun_client\n" |
||||
"tun_test_mode=1\n" |
||||
"\n" |
||||
"[server:cl]\n" |
||||
"addr=127.0.0.1:39102\n" |
||||
"type=public\n" |
||||
"\n" |
||||
"[client:to_prov]\n" |
||||
"keepalive=1\n" |
||||
"peer_public_key=%s\n" |
||||
"link=cl:127.0.0.1:39101\n" |
||||
"\n" |
||||
"[nat]\n" |
||||
"enabled=1\n" |
||||
"tun_ifname=tun_nat_client\n" |
||||
"tun_ip=100.64.1.1/24\n" |
||||
"nat_via=0xAAAA000000000001\n", |
||||
pub_prov); |
||||
free(pub_prov); |
||||
if (write_config(config_client, tpl_client_full) != 0) return -1; |
||||
if (config_ensure_keys_and_node_id(config_client) != 0) return -1; |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
static void cleanup_temp_configs(void) { |
||||
test_unlink(config_provider); |
||||
test_unlink(config_client); |
||||
test_rmdir(temp_dir); |
||||
} |
||||
|
||||
static void test_timeout_cb(void* arg) { |
||||
(void)arg; |
||||
test_timed_out = 1; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "test_nat_transport: timeout"); |
||||
} |
||||
|
||||
static struct ETCP_LINK* first_initialized_link(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->connections) return NULL; |
||||
struct ETCP_LINK* link = inst->connections->links; |
||||
while (link) { |
||||
if (link->initialized) return link; |
||||
link = link->next; |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
// ==================== Capture callback for client TUN ====================
|
||||
static void capture_tun_out_cb(struct ll_queue* q, void* arg) { |
||||
(void)arg; |
||||
struct ll_entry* pkt = queue_data_get(q); |
||||
if (pkt && pkt->dgram && pkt->len > 1) { |
||||
size_t copy = pkt->len - 1; |
||||
if (copy > sizeof(test_state.captured) - 1) copy = sizeof(test_state.captured) - 1; |
||||
memcpy(test_state.captured, pkt->dgram + 1, copy); |
||||
test_state.captured_len = copy; |
||||
test_state.capture_count++; |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "capture_tun: %zu bytes, count=%d", copy, test_state.capture_count); |
||||
} |
||||
queue_dgram_free(pkt); |
||||
queue_entry_free(pkt); |
||||
queue_resume_callback(q); |
||||
} |
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
/* Build raw UDP/IP packet helper */ |
||||
static uint8_t* build_udp_pkt(uint32_t src_host, uint16_t src_port_host, |
||||
uint32_t dst_host, uint16_t dst_port_host, |
||||
size_t* out_len) { |
||||
const size_t len = 20 + 8 + 14; |
||||
uint8_t* pkt = calloc(1, len); |
||||
|
||||
pkt[0] = 0x45; pkt[1] = 0x00; |
||||
uint16_t tot = htons((uint16_t)len); |
||||
memcpy(pkt + 2, &tot, 2); |
||||
pkt[4] = 0x12; pkt[5] = 0x34; |
||||
memset(pkt + 6, 0, 2); |
||||
pkt[8] = 64; |
||||
pkt[9] = IPPROTO_UDP_UINT8; |
||||
memset(pkt + 10, 0, 2); |
||||
uint32_t sn = htonl(src_host), dn = htonl(dst_host); |
||||
memcpy(pkt + 12, &sn, 4); |
||||
memcpy(pkt + 16, &dn, 4); |
||||
|
||||
uint16_t sp = htons(src_port_host), dp = htons(dst_port_host); |
||||
memcpy(pkt + 20, &sp, 2); |
||||
memcpy(pkt + 22, &dp, 2); |
||||
uint16_t ul = htons(8 + 14); |
||||
memcpy(pkt + 24, &ul, 2); |
||||
memset(pkt + 26, 0, 2); |
||||
memset(pkt + 28, 0xAB, 14); |
||||
|
||||
// Compute IP checksum
|
||||
uint32_t sum = 0; |
||||
for (int i = 0; i < 10; i++) { uint16_t w; memcpy(&w, pkt + i*2, 2); sum += w; } |
||||
sum = (sum & 0xFFFF) + (sum >> 16); |
||||
sum += (sum >> 16); |
||||
uint16_t cs = (uint16_t)(~sum); |
||||
memcpy(pkt + 10, &cs, 2); |
||||
|
||||
*out_len = len; |
||||
return pkt; |
||||
} |
||||
|
||||
/* Verify IP checksum */ |
||||
static int verify_ip_checksum(const 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; |
||||
} |
||||
|
||||
static int test_init_destroy(void) { |
||||
/* Already done in main: provider and client NAT transport initialized.
|
||||
Here we just verify that ctx fields are populated. */ |
||||
int ok = 1; |
||||
|
||||
// Check provider
|
||||
if (!inst_provider->nat_tr.initialized) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Provider NAT transport not initialized"); |
||||
ok = 0; |
||||
} |
||||
if (!inst_provider->nat.initialized) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Provider NAT engine not initialized"); |
||||
ok = 0; |
||||
} |
||||
if (inst_provider->nat.gateway_ip == 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Provider gateway_ip = 0"); |
||||
ok = 0; |
||||
} |
||||
if (!inst_provider->nat_tr.nat_tun) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Provider NAT TUN not created"); |
||||
ok = 0; |
||||
} |
||||
if (inst_provider->nat_tr.nat_via_node_id != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Provider nat_via_node_id should be 0"); |
||||
ok = 0; |
||||
} |
||||
|
||||
// Check client
|
||||
if (!inst_client->nat_tr.initialized) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Client NAT transport not initialized"); |
||||
ok = 0; |
||||
} |
||||
if (inst_client->nat_tr.nat_via_node_id != NODE_ID_PROVIDER) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Client nat_via_node_id mismatch: 0x%016llx", |
||||
(unsigned long long)inst_client->nat_tr.nat_via_node_id); |
||||
ok = 0; |
||||
} |
||||
|
||||
return ok; |
||||
} |
||||
|
||||
static int test_provider_egress(void) { |
||||
/* Inject a raw IP/UDP packet directly into provider via ETCP */ |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "=== test_provider_egress ==="); |
||||
|
||||
// Get the connection from client to provider
|
||||
struct ETCP_CONN* client_conn = inst_client->connections; |
||||
if (!client_conn) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No client connections"); |
||||
return 0; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Client conn peer_node_id=0x%016llx", (unsigned long long)client_conn->peer_node_id); |
||||
|
||||
// But we need to send FROM client TO provider. The client's nat_via_conn
|
||||
// should be resolved via route_bgp_find_conn_for_node.
|
||||
// Since we haven't triggered a TUN out yet, we need to force resolve.
|
||||
// Simpler: use the already-established connection from client side.
|
||||
struct ETCP_CONN* via_conn = route_bgp_find_conn_for_node(inst_client->bgp, NODE_ID_PROVIDER); |
||||
if (!via_conn) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No route to provider"); |
||||
return 0; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "via_conn peer=0x%016llx", (unsigned long long)via_conn->peer_node_id); |
||||
|
||||
// Build ETCP_ID_NAT packet
|
||||
size_t ip_len; |
||||
uint8_t* raw_ip = build_udp_pkt(0x0A0000FE, 40000, 0x08080808, 53, &ip_len); |
||||
size_t total = NAT_HDR_SIZE + ip_len; |
||||
uint8_t* dgram = u_malloc(total); |
||||
dgram[0] = ETCP_ID_NAT; |
||||
memcpy(dgram + 1, &inst_client->nat_tr.self_node_id, 8); |
||||
memcpy(dgram + 9, &inst_provider->nat_tr.self_node_id, 8); |
||||
memcpy(dgram + 17, raw_ip, ip_len); |
||||
free(raw_ip); |
||||
|
||||
struct ll_entry* entry = queue_entry_new(0); |
||||
entry->dgram = dgram; |
||||
entry->len = total; |
||||
|
||||
int ret = etcp_send(via_conn, entry); |
||||
if (ret != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "etcp_send failed"); |
||||
queue_entry_free(entry); |
||||
queue_dgram_free(entry); |
||||
return 0; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "ETCP_ID_NAT sent from client to provider"); |
||||
|
||||
// Poll to let provider process
|
||||
int cycles = 0; |
||||
while (cycles < 200 && !test_timed_out) { |
||||
uasync_poll(ua, 5); |
||||
cycles++; |
||||
// Stop when we see a NAT entry
|
||||
if (inst_provider->nat.table[inst_provider->nat.port_start].state != EIM_NAT_ENTRY_FREE) |
||||
break; |
||||
} |
||||
|
||||
// Verify NAT table on provider
|
||||
struct eim_nat_entry* e = &inst_provider->nat.table[inst_provider->nat.port_start]; |
||||
if (e->state != EIM_NAT_ENTRY_ACTIVE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Provider: no NAT entry after egress (state=%d)", (int)e->state); |
||||
return 0; |
||||
} |
||||
|
||||
test_state.provider_egress_entry = 1; |
||||
test_state.egress_src_node_id = e->src_node_id; |
||||
test_state.egress_internal_ip = e->internal_ip; |
||||
test_state.egress_internal_port_net = e->internal_port; |
||||
test_state.egress_external_port = inst_provider->nat.port_start; |
||||
test_state.egress_proto = e->proto; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Provider egress: internal=%08x:%u proto=%u ext_port=%u node=%016llx", |
||||
e->internal_ip, ntohs(e->internal_port), e->proto, |
||||
inst_provider->nat.port_start, (unsigned long long)e->src_node_id); |
||||
|
||||
if (e->src_node_id != NODE_ID_CLIENT) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "src_node_id mismatch: %016llx vs %016llx", |
||||
(unsigned long long)e->src_node_id, NODE_ID_CLIENT); |
||||
return 0; |
||||
} |
||||
if (e->internal_ip != 0x0A0000FE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "internal_ip mismatch: %08x", e->internal_ip); |
||||
return 0; |
||||
} |
||||
if (e->internal_port != htons(40000)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "internal_port mismatch: %u vs 40000", ntohs(e->internal_port)); |
||||
return 0; |
||||
} |
||||
if (e->proto != IPPROTO_UDP_UINT8) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "proto mismatch: %u", e->proto); |
||||
return 0; |
||||
} |
||||
|
||||
return 1; |
||||
} |
||||
|
||||
static int test_provider_ingress_response(void) { |
||||
/* Inject internet response into provider's TUN output_queue.
|
||||
The provider's TUN output callback will do ingress NAT and send to client. */ |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "=== test_provider_ingress_response ==="); |
||||
|
||||
if (!test_state.provider_egress_entry) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Skip: no egress entry from previous test"); |
||||
return 0; |
||||
} |
||||
|
||||
// Use actual gateway IP from provider's NAT engine
|
||||
uint32_t gw_ip_host = inst_provider->nat.gateway_ip; |
||||
size_t ip_len; |
||||
uint8_t* resp_ip = build_udp_pkt(0x08080808, 53, |
||||
gw_ip_host, test_state.egress_external_port, |
||||
&ip_len); |
||||
|
||||
// Inject into provider's NAT TUN output_queue (simulating internet response)
|
||||
struct tun_if* tun = inst_provider->nat_tr.nat_tun; |
||||
if (!tun) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No NAT TUN"); free(resp_ip); return 0; } |
||||
|
||||
// Before injecting, set capture callback on client's TUN to prevent loop
|
||||
struct tun_if* client_tun = inst_client->nat_tr.nat_tun; |
||||
if (client_tun && client_tun->output_queue) { |
||||
queue_set_callback(client_tun->output_queue, capture_tun_out_cb, NULL); |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Client TUN callback set to capture"); |
||||
} |
||||
|
||||
memset(&test_state.captured, 0, sizeof(test_state.captured)); |
||||
test_state.captured_len = 0; |
||||
test_state.capture_count = 0; |
||||
|
||||
int r = tun_inject_packet(tun, resp_ip, ip_len); |
||||
free(resp_ip); |
||||
if (r != 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "tun_inject_packet failed"); return 0; } |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Injected response into provider TUN output_queue"); |
||||
|
||||
// Poll extensively to let the full chain complete
|
||||
int cycles = 0; |
||||
while (cycles < 500 && !test_timed_out && test_state.capture_count == 0) { |
||||
uasync_poll(ua, 10); |
||||
cycles++; |
||||
} |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "After poll: capture_count=%d, captured_len=%zu", |
||||
test_state.capture_count, test_state.captured_len); |
||||
|
||||
if (test_state.capture_count == 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No response captured on client after %d cycles", cycles); |
||||
return 0; |
||||
} |
||||
|
||||
// Restore original callback
|
||||
if (client_tun && client_tun->output_queue) { |
||||
queue_set_callback(client_tun->output_queue, NULL, NULL); |
||||
} |
||||
|
||||
// Verify the captured response
|
||||
if (test_state.captured_len < 20) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Captured packet too short: %zu", test_state.captured_len); |
||||
return 0; |
||||
} |
||||
if (!verify_ip_checksum(test_state.captured)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Captured IP checksum invalid"); |
||||
return 0; |
||||
} |
||||
// Check dst IP = original internal IP
|
||||
uint32_t dst_net; memcpy(&dst_net, test_state.captured + 16, 4); |
||||
uint32_t expected_dst = htonl(0x0A0000FE); // 10.0.0.254
|
||||
if (dst_net != expected_dst) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Response dst IP: %08x, expected %08x", ntohl(dst_net), ntohl(expected_dst)); |
||||
return 0; |
||||
} |
||||
// Check dst port = original internal port
|
||||
uint16_t dst_port_net; memcpy(&dst_port_net, test_state.captured + 22, 2); |
||||
if (dst_port_net != htons(40000)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Response dst port: %u, expected 40000", ntohs(dst_port_net)); |
||||
return 0; |
||||
} |
||||
|
||||
test_state.client_response_done = 1; |
||||
return 1; |
||||
} |
||||
|
||||
static int test_full_roundtrip(void) { |
||||
/* Combined egress + ingress via manual injection.
|
||||
Cannot do auto-roundtrip because tun_write in test mode puts to output_queue |
||||
instead of real TUN, causing egressed packets to loop back. */ |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "=== test_full_roundtrip ==="); |
||||
|
||||
struct tun_if* client_tun = inst_client->nat_tr.nat_tun; |
||||
if (!client_tun) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No client TUN"); return 0; } |
||||
|
||||
// Clean NAT table from previous test
|
||||
for (uint16_t p = inst_provider->nat.port_start; p <= inst_provider->nat.port_end; p++) |
||||
memset(&inst_provider->nat.table[p], 0, sizeof(struct eim_nat_entry)); |
||||
inst_provider->nat.next_port = inst_provider->nat.port_start; |
||||
|
||||
// === Step 1: Send ETCP_ID_NAT from client to provider (egress) ===
|
||||
struct ETCP_CONN* via_conn = route_bgp_find_conn_for_node(inst_client->bgp, NODE_ID_PROVIDER); |
||||
if (!via_conn) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No route to provider"); return 0; } |
||||
|
||||
size_t ip_len; |
||||
uint8_t* raw_ip = build_udp_pkt(0x0A0000CD, 44444, 0x08080808, 80, &ip_len); |
||||
size_t total = NAT_HDR_SIZE + ip_len; |
||||
uint8_t* dgram = u_malloc(total); |
||||
dgram[0] = ETCP_ID_NAT; |
||||
memcpy(dgram + 1, &inst_client->nat_tr.self_node_id, 8); |
||||
memcpy(dgram + 9, &inst_provider->nat_tr.self_node_id, 8); |
||||
memcpy(dgram + 17, raw_ip, ip_len); |
||||
free(raw_ip); |
||||
|
||||
struct ll_entry* entry = queue_entry_new(0); |
||||
entry->dgram = dgram; |
||||
entry->len = total; |
||||
int ret = etcp_send(via_conn, entry); |
||||
if (ret != 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "etcp_send failed"); queue_entry_free(entry); queue_dgram_free(entry); return 0; } |
||||
|
||||
// Poll for provider to process egress
|
||||
int cycles = 0; |
||||
while (cycles < 200 && !test_timed_out) { |
||||
uasync_poll(ua, 5); cycles++; |
||||
if (inst_provider->nat.table[inst_provider->nat.port_start].state != EIM_NAT_ENTRY_FREE) break; |
||||
} |
||||
|
||||
// Verify NAT entry
|
||||
struct eim_nat_entry* e = &inst_provider->nat.table[inst_provider->nat.port_start]; |
||||
if (e->state != EIM_NAT_ENTRY_ACTIVE || e->internal_ip != 0x0A0000CD || e->internal_port != htons(44444)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Egress entry not found: ip=%08x port=%u state=%d", |
||||
e->internal_ip, ntohs(e->internal_port), e->state); |
||||
return 0; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Egress entry ok: %08x:%u ext=%u proto=%u", |
||||
e->internal_ip, ntohs(e->internal_port), inst_provider->nat.port_start, e->proto); |
||||
|
||||
// === Step 2: Manually inject internet response into provider's TUN ===
|
||||
if (client_tun->output_queue) queue_set_callback(client_tun->output_queue, capture_tun_out_cb, NULL); |
||||
memset(&test_state.captured, 0, sizeof(test_state.captured)); |
||||
test_state.captured_len = 0; |
||||
test_state.capture_count = 0; |
||||
|
||||
uint32_t gw_ip_host = inst_provider->nat.gateway_ip; |
||||
size_t rip_len; |
||||
uint8_t* resp_ip = build_udp_pkt(0x08080808, 80, gw_ip_host, inst_provider->nat.port_start, &rip_len); |
||||
if (tun_inject_packet(inst_provider->nat_tr.nat_tun, resp_ip, rip_len) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "tun_inject response failed"); |
||||
free(resp_ip); return 0; |
||||
} |
||||
free(resp_ip); |
||||
|
||||
cycles = 0; |
||||
while (cycles < 500 && !test_timed_out && test_state.capture_count == 0) { uasync_poll(ua, 10); cycles++; } |
||||
|
||||
if (client_tun->output_queue) queue_set_callback(client_tun->output_queue, NULL, NULL); |
||||
|
||||
if (test_state.capture_count == 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "No response in roundtrip"); return 0; } |
||||
|
||||
// Verify captured response
|
||||
if (!verify_ip_checksum(test_state.captured)) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Captured IP checksum invalid"); return 0; } |
||||
uint32_t dst_net; memcpy(&dst_net, test_state.captured + 16, 4); |
||||
if (dst_net != htonl(0x0A0000CD)) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Roundtrip dst IP mismatch"); return 0; } |
||||
uint16_t dp_net; memcpy(&dp_net, test_state.captured + 22, 2); |
||||
if (dp_net != htons(44444)) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Roundtrip dst port mismatch"); return 0; } |
||||
|
||||
return 1; |
||||
} |
||||
|
||||
// ==================== Main ====================
|
||||
int main(void) { |
||||
int test_result = 1; |
||||
debug_config_init(); |
||||
debug_set_level(DEBUG_LEVEL_INFO); |
||||
debug_set_categories(DEBUG_CATEGORY_NAT | DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING); |
||||
utun_instance_set_tun_init_enabled(0); |
||||
|
||||
if (create_temp_configs() != 0) { |
||||
fprintf(stderr, "Failed to create temp configs\n"); |
||||
return 1; |
||||
} |
||||
|
||||
ua = uasync_create(); |
||||
if (!ua) { cleanup_temp_configs(); return 1; } |
||||
|
||||
inst_provider = utun_instance_create(ua, config_provider); |
||||
inst_client = utun_instance_create(ua, config_client); |
||||
if (!inst_provider || !inst_client) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to create instances"); |
||||
goto cleanup; |
||||
} |
||||
|
||||
if (utun_instance_init(inst_provider) != 0 || utun_instance_init(inst_client) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to init instances"); |
||||
goto cleanup; |
||||
} |
||||
|
||||
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS, NULL, test_timeout_cb, "test_nat_transport"); |
||||
|
||||
// Wait for ETCP link between client and provider
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Waiting for ETCP link..."); |
||||
while (!test_timed_out) { |
||||
if (first_initialized_link(inst_client)) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Link initialized"); |
||||
break; |
||||
} |
||||
uasync_poll(ua, 10); |
||||
} |
||||
if (test_timed_out) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Link timeout"); goto cleanup; } |
||||
|
||||
// Wait for BGP exchange (provider learns about client)
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Waiting for BGP..."); |
||||
int bgp_cycles = 0; |
||||
while (!test_timed_out && bgp_cycles < 500) { |
||||
if (inst_provider->bgp && route_bgp_get_node(inst_provider->bgp, NODE_ID_CLIENT) && |
||||
inst_client->bgp && route_bgp_get_node(inst_client->bgp, NODE_ID_PROVIDER)) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "BGP exchanged"); |
||||
break; |
||||
} |
||||
uasync_poll(ua, 10); |
||||
bgp_cycles++; |
||||
} |
||||
if (test_timed_out) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "BGP timeout"); goto cleanup; } |
||||
|
||||
// Run tests
|
||||
int passed = 0, total = 0; |
||||
|
||||
total++; if (test_init_destroy()) passed++; else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL: init_destroy"); |
||||
total++; if (test_provider_egress()) passed++; else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL: provider_egress"); |
||||
total++; if (test_provider_ingress_response()) passed++; else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL: provider_ingress"); |
||||
total++; if (test_full_roundtrip()) passed++; else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL: full_roundtrip"); |
||||
|
||||
printf("\n=== NAT Transport Tests: %d/%d passed ===\n", passed, total); |
||||
test_result = (passed == total) ? 0 : 1; |
||||
|
||||
cleanup: |
||||
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id); |
||||
if (inst_provider) utun_instance_destroy(inst_provider); |
||||
if (inst_client) utun_instance_destroy(inst_client); |
||||
if (ua) uasync_destroy(ua, 0); |
||||
cleanup_temp_configs(); |
||||
return test_result; |
||||
} |
||||
Loading…
Reference in new issue