diff --git a/lib/debug_config.h b/lib/debug_config.h index 622e4b1e..3f8e3ca0 100644 --- a/lib/debug_config.h +++ b/lib/debug_config.h @@ -55,7 +55,8 @@ typedef int debug_category_t; #define DEBUG_CATEGORY_TRAFFIC 17 // Traffic flow (src/dst node IDs) #define DEBUG_CATEGORY_DEBUG 18 // Current debugging #define DEBUG_CATEGORY_GENERAL 19 // Messages for user (common log) -#define DEBUG_CATEGORY_COUNT 20 // Total number of categories +#define DEBUG_CATEGORY_NAT 20 // EIM NAT module +#define DEBUG_CATEGORY_COUNT 21 // Total number of categories #define DEBUG_CATEGORY_ALL (-1) // special value for all categories /* Debug configuration structure */ diff --git a/src/Makefile.am b/src/Makefile.am index 9f0484cf..9e8a366f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -26,7 +26,9 @@ utun_CORE_SOURCES = \ packet_dump.c \ etcp_api.c \ control_server.c \ - firewall.c + firewall.c \ + eim_nat.c \ + nat_transport.c # Platform-specific TUN libs (Windows only) utun_TUN_LIBS = @TUN_LIBS@ diff --git a/src/config_parser.c b/src/config_parser.c index 555cfc50..129eb6e2 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -13,6 +13,7 @@ #include #include #include +#include #endif #include "../lib/mem.h" @@ -42,7 +43,8 @@ typedef enum { SECTION_DEBUG, SECTION_FIREWALL, SECTION_CONTROL, - SECTION_ALLOWED_KEYS + SECTION_ALLOWED_KEYS, + SECTION_NAT } section_type_t; static char* trim(char *str) { @@ -414,6 +416,77 @@ static int parse_control(const char *key, const char *value, struct global_confi return 0; } +static int parse_nat(const char *key, const char *value, struct global_config *global) { + if (strcmp(key, "tun_ifname") == 0) { + strncpy(global->nat_tun_ifname, value, sizeof(global->nat_tun_ifname) - 1); + return 0; + } + if (strcmp(key, "tun_ip") == 0) { + uint8_t netmask; + parse_ip_with_netmask(value, &global->nat_tun_ip, &netmask); + return 0; + } + if (strcmp(key, "nat_via") == 0) { + global->nat_via_node_id = strtoull(value, NULL, 16); + return 0; + } + if (strcmp(key, "port_start") == 0) { + global->nat_port_start = (uint16_t)atoi(value); + return 0; + } + if (strcmp(key, "port_end") == 0) { + global->nat_port_end = (uint16_t)atoi(value); + return 0; + } + if (strcmp(key, "forward") == 0) { + if (global->nat_forward_count >= MAX_NAT_FORWARDS) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Too many NAT forward rules (max %d)", MAX_NAT_FORWARDS); + return -1; + } + // Format: proto:internal_ip:internal_port:external_port + // Example: tcp:192.168.1.100:8080:8080 + char buf[128]; + strncpy(buf, value, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + trim(buf); + + char* proto_str = strtok(buf, ":"); + char* ip_str = strtok(NULL, ":"); + char* int_port = strtok(NULL, ":"); + char* ext_port = strtok(NULL, ":"); + if (!proto_str || !ip_str || !int_port || !ext_port) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward format: %s (expected proto:ip:internal_port:external_port)", value); + return -1; + } + + uint8_t proto; + if (strcasecmp(proto_str, "tcp") == 0) proto = IPPROTO_TCP; + else if (strcasecmp(proto_str, "udp") == 0) proto = IPPROTO_UDP; + else { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward proto: %s (tcp/udp)", proto_str); + return -1; + } + + struct in_addr addr; + if (inet_pton(AF_INET, ip_str, &addr) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward IP: %s", ip_str); + return -1; + } + + int idx = global->nat_forward_count; + global->nat_forwards[idx].proto = proto; + global->nat_forwards[idx].internal_ip_host = ntohl(addr.s_addr); + global->nat_forwards[idx].internal_port_net = htons((uint16_t)atoi(int_port)); + global->nat_forwards[idx].external_port = (uint16_t)atoi(ext_port); + global->nat_forward_count++; + + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NAT forward: %s %s:%s -> :%s", + proto_str, ip_str, int_port, ext_port); + return 0; + } + return 0; +} + static int parse_server(const char *key, const char *value, struct CFG_SERVER *srv) { if (strcmp(key, "addr") == 0) { return parse_address_and_port(value, &srv->ip); @@ -519,6 +592,7 @@ static section_type_t parse_section_header(const char *line, char *name, size_t if (strcasecmp(section, "firewall") == 0) return SECTION_FIREWALL; if (strcasecmp(section, "control") == 0) return SECTION_CONTROL; if (strcasecmp(section, "allowed_keys") == 0) return SECTION_ALLOWED_KEYS; + if (strcasecmp(section, "nat") == 0) return SECTION_NAT; char *colon = strchr(section, ':'); if (!colon) return SECTION_UNKNOWN; @@ -674,6 +748,12 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename) } } break; + case SECTION_NAT: + cfg->global.nat_enabled = 1; + if (parse_nat(key, value, &cfg->global) < 0) { + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT key '%s'", filename, line_num, key); + } + break; default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break; diff --git a/src/config_parser.h b/src/config_parser.h index 57f60f3f..f429bf8d 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -121,6 +121,22 @@ struct global_config { struct CFG_ALLOWED_KEY *allowed_keys; int allowed_keys_count; int allowed_keys_allow_all; + + // NAT configuration ([nat] section) + int nat_enabled; + char nat_tun_ifname[16]; + struct IP nat_tun_ip; + uint64_t nat_via_node_id; // 0 = this node is provider; !=0 = client, use this provider + uint16_t nat_port_start; + uint16_t nat_port_end; + #define MAX_NAT_FORWARDS 64 + struct { + uint8_t proto; + uint32_t internal_ip_host; + uint16_t internal_port_net; + uint16_t external_port; + } nat_forwards[MAX_NAT_FORWARDS]; + int nat_forward_count; }; struct utun_config { diff --git a/src/eim_nat.c b/src/eim_nat.c new file mode 100644 index 00000000..f356cf64 --- /dev/null +++ b/src/eim_nat.c @@ -0,0 +1,306 @@ +#include "eim_nat.h" +#include "config_parser.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include +#include + +// 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; +} diff --git a/src/eim_nat.h b/src/eim_nat.h new file mode 100644 index 00000000..9c984677 --- /dev/null +++ b/src/eim_nat.h @@ -0,0 +1,54 @@ +#ifndef EIM_NAT_H +#define EIM_NAT_H + +#include +#include + +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 diff --git a/src/etcp_api.h b/src/etcp_api.h index 4526a3d2..8c6c43c3 100644 --- a/src/etcp_api.h +++ b/src/etcp_api.h @@ -23,6 +23,7 @@ // ETCP packet IDs #define ETCP_ID_DATA 0x00 // Пакет для передачи адресату #define ETCP_ID_ROUTE_ENTRY 0x01 // Элемент роутинг-таблицы +#define ETCP_ID_NAT 0x02 // NAT трафик между узлами // Forward declarations struct ETCP_CONN; diff --git a/src/nat_transport.c b/src/nat_transport.c new file mode 100644 index 00000000..4217a3bf --- /dev/null +++ b/src/nat_transport.c @@ -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 + +#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"); +} diff --git a/src/nat_transport.h b/src/nat_transport.h new file mode 100644 index 00000000..88ee5d2a --- /dev/null +++ b/src/nat_transport.h @@ -0,0 +1,23 @@ +// nat_transport.h — NAT transport layer (TUN + ETCP protocol handling) +#ifndef NAT_TRANSPORT_H +#define NAT_TRANSPORT_H + +#include + +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 diff --git a/src/route_bgp.c b/src/route_bgp.c index f3c3fddd..6f5d4d09 100644 --- a/src/route_bgp.c +++ b/src/route_bgp.c @@ -456,6 +456,21 @@ struct NODEINFO_Q* route_bgp_get_node(struct ROUTE_BGP* bgp, uint64_t node_id) { return e ? (struct NODEINFO_Q*)e : NULL; } +struct ETCP_CONN* route_bgp_find_conn_for_node(struct ROUTE_BGP* bgp, uint64_t node_id) { + if (!bgp) return NULL; + struct NODEINFO_Q* nq = route_bgp_get_node(bgp, node_id); + if (!nq || !nq->paths || !nq->paths->head) return NULL; + struct ll_entry* e = nq->paths->head; + struct NODEINFO_PATH* best = NULL; + uint8_t best_hops = 255; + while (e) { + struct NODEINFO_PATH* path = (struct NODEINFO_PATH*)e; + if (path->conn && path->hop_count < best_hops) { best = path; best_hops = path->hop_count; } + e = e->next; + } + return best ? best->conn : NULL; +} + int route_bgp_add_path(struct NODEINFO_Q* nq, struct ETCP_CONN* conn, uint64_t* hop_list, uint8_t hop_count) { if (!nq || !conn || hop_count > MAX_HOPS || !hop_list) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "add_path: invalid args"); diff --git a/src/route_bgp.h b/src/route_bgp.h index 7a37a816..ccda1355 100644 --- a/src/route_bgp.h +++ b/src/route_bgp.h @@ -158,6 +158,17 @@ void route_bgp_send_withdraw(struct ROUTE_BGP* bgp, uint64_t node_id); */ struct NODEINFO_Q* route_bgp_get_node(struct ROUTE_BGP* bgp, uint64_t node_id); +/** + * @brief Поиск оптимального ETCP соединения для указанного node_id. + * + * Перебирает paths узла, выбирает путь с минимальным hop_count. + * + * @param bgp указатель на ROUTE_BGP + * @param node_id целевой узел + * @return оптимальный ETCP_CONN* или NULL + */ +struct ETCP_CONN* route_bgp_find_conn_for_node(struct ROUTE_BGP* bgp, uint64_t node_id); + /** * @brief Добавляет путь (conn) в paths узла. * diff --git a/src/routing.c b/src/routing.c index 2124c0fa..18c9a31c 100644 --- a/src/routing.c +++ b/src/routing.c @@ -52,7 +52,7 @@ static uint32_t extract_dst_ip(uint8_t* data, size_t len) { } // entry format: -static void route_pkt(struct UTUN_INSTANCE* instance, struct ll_entry* entry, uint64_t src_node_id) { +void route_pkt(struct UTUN_INSTANCE* instance, struct ll_entry* entry, uint64_t src_node_id) { DEBUG_TRACE(DEBUG_CATEGORY_ROUTING, ""); if (!instance || !entry) { DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "route_pkt: invalid arguments: instance=%p entry=%p entry->len=%zu", diff --git a/src/routing.h b/src/routing.h index f17fcc62..b1da002d 100644 --- a/src/routing.h +++ b/src/routing.h @@ -8,6 +8,7 @@ // Forward declarations struct ETCP_CONN; struct UTUN_INSTANCE; +struct ll_entry; /** * @brief Initialize routing module for instance @@ -43,4 +44,9 @@ void routing_del_conn(struct ETCP_CONN* etcp); */ void routing_set_tun(struct UTUN_INSTANCE* instance); +/** + * @brief Route a single packet (exposed for NAT module interception) + */ +void route_pkt(struct UTUN_INSTANCE* instance, struct ll_entry* entry, uint64_t src_node_id); + #endif // ROUTING_H diff --git a/src/tun_if.c b/src/tun_if.c index 2225807b..26d2f40b 100644 --- a/src/tun_if.c +++ b/src/tun_if.c @@ -204,6 +204,64 @@ fail: return NULL; } +// =================================================================== +// Init NAT TUN (separate TUN interface for NAT traffic) +// =================================================================== +struct tun_if* tun_init_nat(struct UASYNC* ua, const char* ifname, const char* ip_addr_str, int mtu, int test_mode) +{ + if (!ua || !ifname || !ip_addr_str) return NULL; + if (mtu <= 0) mtu = TUN_MTU_DEFAULT; + + struct tun_if* tun = u_calloc(1, sizeof(struct tun_if)); + if (!tun) return NULL; + + tun->ua = ua; + tun->test_mode = test_mode; + tun->fd = -1; + strncpy(tun->ifname, ifname, sizeof(tun->ifname) - 1); + + char ip_str[64]; + snprintf(ip_str, sizeof(ip_str), "%s/32", ip_addr_str); + + if (!test_mode) { + if (tun_platform_init(tun, tun->ifname, ip_str, mtu) != 0) { + u_free(tun); + return NULL; + } + } + + tun->pool = memory_pool_init(sizeof(struct ll_entry)); + if (!tun->pool) goto fail2; + + tun->output_queue = queue_new(ua, 0, "NAT TUN output"); + tun->input_queue = queue_new(ua, 0, "NAT TUN input"); + if (!tun->output_queue || !tun->input_queue) goto fail2; + + queue_set_callback(tun->input_queue, tun_input_queue_callback, tun); + + if (!test_mode) { + int poll_fd = tun_platform_get_poll_fd(tun); + if (poll_fd >= 0) { +#ifndef _WIN32 + tun->socket_id = uasync_add_socket(ua, poll_fd, tun_read_callback, NULL, NULL, tun); + if (!tun->socket_id) goto fail2; +#endif + } + } + + DEBUG_INFO(DEBUG_CATEGORY_TUN, "NAT TUN %s initialized (%s mode)", tun->ifname, + test_mode ? "TEST" : "REAL"); + return tun; + +fail2: + if (tun->output_queue) queue_free(tun->output_queue); + if (tun->input_queue) queue_free(tun->input_queue); + if (tun->pool) memory_pool_destroy(tun->pool); + if (!test_mode) tun_platform_cleanup(tun); + u_free(tun); + return NULL; +} + // =================================================================== // Остальные функции (без изменений) // =================================================================== diff --git a/src/tun_if.h b/src/tun_if.h index 61fc9383..ed6996a3 100644 --- a/src/tun_if.h +++ b/src/tun_if.h @@ -75,6 +75,17 @@ struct tun_if { */ struct tun_if* tun_init(struct UASYNC* ua, struct utun_config* config); +/** + * @brief Инициализация TUN интерфейса с явными параметрами (для NAT TUN) + * @param ua экземпляр uasync + * @param ifname имя интерфейса + * @param ip_addr IP-адрес (строка, например "10.0.1.1") + * @param mtu MTU интерфейса + * @param test_mode тестовый режим + * @return указатель на tun_if или NULL при ошибке + */ +struct tun_if* tun_init_nat(struct UASYNC* ua, const char* ifname, const char* ip_addr, int mtu, int test_mode); + /** * @brief Закрытие и освобождение всех ресурсов */ diff --git a/src/utun_instance.c b/src/utun_instance.c index 2b0c9e6a..1d39156c 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -297,6 +297,11 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { // Cleanup firewall fw_free(&instance->fw); + // Cleanup NAT + if (instance->nat_tr.initialized) { + nat_transport_destroy(instance); + } + // Cleanup config if (instance->config) { DEBUG_INFO(DEBUG_CATEGORY_MEMORY, "[INSTANCE_DESTROY] Freeing configuration"); @@ -361,6 +366,14 @@ int utun_instance_init(struct UTUN_INSTANCE *instance) { routing_set_tun(instance); DEBUG_INFO(DEBUG_CATEGORY_ROUTING, "TUN interface registered in routing module"); } + + // Initialize NAT (after routing_set_tun, before connections) + if (instance->config->global.nat_enabled) { + int nat_ret = nat_transport_init(instance); + if (nat_ret != 0) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT transport init failed (non-fatal)"); + } + } // Note: TUN socket is already registered in tun_init() diff --git a/src/utun_instance.h b/src/utun_instance.h index 8508056e..2c317540 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -9,6 +9,8 @@ #include "etcp_api.h" #include "config_parser.h" #include "firewall.h" +#include "eim_nat.h" +#include "nat_transport.h" // Forward declarations struct utun_config; @@ -84,6 +86,10 @@ struct UTUN_INSTANCE { // Firewall struct firewall_ctx fw; + // EIM NAT + struct eim_nat_ctx nat; + struct nat_transport_ctx nat_tr; + // Socket initialization status: 0=OK, 1=partial (some sockets failed), -1=error (none created) int socket_init_status; }; diff --git a/tests/Makefile.am b/tests/Makefile.am index 45567371..d230c22d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -26,6 +26,9 @@ check_PROGRAMS = \ test_etcp_ping \ test_route_ping \ test_nat_detection \ + test_nat_engine \ + test_nat_transport \ + test_nat_stress \ bench_timeout_heap \ bench_uasync_timeouts @@ -91,6 +94,8 @@ ETCP_FULL_OBJS = \ $(top_builddir)/src/utun-tun_route.o \ $(top_builddir)/src/utun-packet_dump.o \ $(top_builddir)/src/utun-firewall.o \ + $(top_builddir)/src/utun-eim_nat.o \ + $(top_builddir)/src/utun-nat_transport.o \ $(top_builddir)/src/utun-control_server.o \ $(TUN_PLATFORM_OBJ) \ $(top_builddir)/src/utun-utun_instance.o \ @@ -186,6 +191,18 @@ test_nat_detection_SOURCES = test_nat_detection.c test_nat_detection_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib -I$(top_srcdir)/tinycrypt/lib/include -I$(top_srcdir)/tinycrypt/lib/source test_nat_detection_LDADD = $(ETCP_FULL_OBJS) $(SECURE_CHANNEL_OBJS) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_nat_engine_SOURCES = test_nat_engine.c +test_nat_engine_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_nat_engine_LDADD = $(top_builddir)/src/utun-eim_nat.o $(top_builddir)/src/utun-config_parser.o $(COMMON_LIBS) + +test_nat_transport_SOURCES = test_nat_transport.c +test_nat_transport_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib -I$(top_srcdir)/tinycrypt/lib/include -I$(top_srcdir)/tinycrypt/lib/source +test_nat_transport_LDADD = $(ETCP_FULL_OBJS) $(SECURE_CHANNEL_OBJS) $(CRYPTO_LIBS) $(COMMON_LIBS) + +test_nat_stress_SOURCES = test_nat_stress.c +test_nat_stress_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_nat_stress_LDADD = $(top_builddir)/src/utun-eim_nat.o $(top_builddir)/src/utun-config_parser.o $(COMMON_LIBS) + test_ll_queue_SOURCES = test_ll_queue.c test_ll_queue_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_ll_queue_LDADD = $(COMMON_LIBS) diff --git a/tests/test_nat_engine.c b/tests/test_nat_engine.c new file mode 100644 index 00000000..553b2c9b --- /dev/null +++ b/tests/test_nat_engine.c @@ -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 +#include +#include +#include +#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; +} diff --git a/tests/test_nat_stress.c b/tests/test_nat_stress.c new file mode 100644 index 00000000..8c1c4b56 --- /dev/null +++ b/tests/test_nat_stress.c @@ -0,0 +1,494 @@ +/** + * @file test_nat_stress.c + * @brief Стресс-тесты NAT engine: множество сессий, заполнение таблицы, многопоточная нагрузка. + * + * Тестирует только eim_nat.c/h (чистый engine), без TUN/ETCP. + * Измеряет время операций и проверяет корректность при предельных нагрузках. + */ + +#include +#include +#include +#include +#include +#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; +} diff --git a/tests/test_nat_transport.c b/tests/test_nat_transport.c new file mode 100644 index 00000000..887824d7 --- /dev/null +++ b/tests/test_nat_transport.c @@ -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 +#include +#include +#include +#include +#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; +} diff --git a/utun.conf.sample b/utun.conf.sample index 67fd354a..0dc96f8c 100644 --- a/utun.conf.sample +++ b/utun.conf.sample @@ -50,3 +50,28 @@ allow=all #allow=192.168.1.100 #allow=10.0.0.50:443 #allow=8.8.8.8 + +# --- NAT (Full Cone / EIM) --- +# Предоставляет доступ в интернет через этот узел другим узлам utun. +# Наличие секции [nat] включает NAT на этом узле. +# Две роли: +# 1. Provider (нет nat_via) — шлюз в интернет, выделяет порты для клиентов +# 2. Client (nat_via=) — отправляет свой трафик через указанный узел-провайдер + +# === Provider example === +#[nat] +#enabled=0 +#tun_ifname=tun_nat # имя NAT TUN интерфейса (по умолчанию tun_nat) +#tun_ip=100.64.0.1/24 # IP адрес NAT шлюза +#port_start=20000 # начало диапазона портов для NAT +#port_end=29999 # конец диапазона (размер = количество одновременных сессий) +## Port forwarding (статический проброс портов): +## forward=proto:internal_ip:internal_port:external_port +#forward=tcp:192.168.1.100:22:2222 +#forward=tcp:192.168.1.100:443:443 + +# === Client example === +#[nat] +#tun_ifname=tun_nat_client +#tun_ip=100.64.1.1/24 # IP клиентского NAT TUN +#nat_via=0xABCD000000000001 # node_id провайдера (у кого запрашивать NAT)