diff --git a/src/config_parser_doc.md b/src/config_parser_doc.md index 17e4102d..21247cdd 100644 --- a/src/config_parser_doc.md +++ b/src/config_parser_doc.md @@ -71,7 +71,7 @@ free_config(cfg); | `[firewall]` | `parse_firewall_rule()` | `allow=IPv4[:port]` или `allow=all`; порт 0–65535, 0 разрешает весь IP; CIDR запрещён | | `[control]` | `parse_control()` | `ip`, `port`, `allow` (подсеть) | | `[allowed_keys]` | inline | `allow_all`, `key` (64 hex) | -| `[nat]` | `parse_nat()` | `tun_ifname`, `tun_ip`, `nat_via`, `port_start`, `port_end`, `forward` (proto:ip:in_port:ext_port) | +| `[nat]` | `parse_nat()` | `tun_ifname`, `tun_ip`, `nat_via`, `port_start`, `port_end`, `forward` (`proto:node_id_hex:IPv4:in_port:ext_port`) | | `[tcp_proxy_client]` | `parse_tcp_proxy_client()` | `enabled`, `tun_name`, `tun_ip`, `mtu`, `via_node`, `socks_*`, `http_proxy_*`, `forward` (port -> ip:port) | | `[tcp_proxy_server]` | inline | `tcp_recv_buf` | | `[ntp]` | inline | `enabled`, `server`, `interval` | diff --git a/tests/test_nat_engine.c b/tests/test_nat_engine.c index a33ab3e4..1745b057 100644 --- a/tests/test_nat_engine.c +++ b/tests/test_nat_engine.c @@ -973,6 +973,74 @@ static void test_node_mapping_isolation(void) { PASS(); } +/* Пробросы вне/внутри динамического пула работают до первого исходящего пакета и сохраняют владельца. */ +static void test_static_forward_owners(void) { + const uint16_t external_ports[] = {80, TEST_PORT_START, 65535}; + for (size_t i = 0; i < sizeof(external_ports) / sizeof(external_ports[0]); i++) { + TEST("static_forward_owner_and_port_range"); + struct global_config g = make_global_config(); + struct eim_nat_ctx ctx; + ASSERT_EQ(eim_nat_init_ctx(&ctx, &g), 0, "forward context initialized"); + uint16_t external_port = external_ports[i]; + ASSERT_EQ(eim_nat_add_forward(&ctx, IPPROTO_UDP_UINT8, TEST_IP_SRC_HOST, htobe16(TEST_SRC_PORT), external_port, 1), + 0, "forward with explicit owner accepted"); + const size_t len = 28 + TEST_PAYLOAD_LEN; + uint8_t* pkt = make_udp_pkt(TEST_IP_DST_HOST, TEST_DST_PORT, TEST_GW_HOST, external_port); + struct eim_nat_entry* entry = NULL; + int r = eim_nat_ingress(&ctx, pkt, len, &entry); + free(pkt); + ASSERT_EQ(r, 0, "unsolicited forward accepted"); + ASSERT(entry && entry->src_node_id == 1, "unsolicited packet has target node"); + for (uint64_t node = 1; node <= 2; node++) { + pkt = make_udp_pkt(TEST_IP_SRC_HOST, TEST_SRC_PORT, TEST_IP_DST_HOST, TEST_DST_PORT); + r = eim_nat_egress(&ctx, pkt, len, node, NULL); + uint16_t port; memcpy(&port, pkt + 20, 2); + free(pkt); + ASSERT_EQ(r, 0, "forward response accepted"); + ASSERT(node == 1 ? be16toh(port) == external_port : be16toh(port) != external_port, + "only forward owner reuses its static port"); + } + ASSERT_EQ(ctx.table[external_port].state, EIM_NAT_ENTRY_STATIC, "static mapping preserved"); + ASSERT_EQ(eim_nat_add_forward(&ctx, IPPROTO_UDP_UINT8, TEST_IP_SRC_HOST, htobe16(TEST_SRC_PORT), 81, 1), + -1, "duplicate internal endpoint rejected"); + ASSERT_EQ(eim_nat_add_forward(&ctx, IPPROTO_UDP_UINT8, TEST_IP_SRC_HOST, htobe16(8080), 81, 0), + -1, "forward without target node rejected"); + eim_nat_destroy_ctx(&ctx); + PASS(); + } + TEST("static_forward_config_and_rollback"); + const char* text = "[nat]\nforward=udp:1234:10.0.0.2:40000:80\n"; + struct utun_config* config = parse_config_from_buf(text, strlen(text), "nat-forward-valid"); + ASSERT(config && config->global.nat_forward_count == 1, "forward config parsed"); + struct eim_nat_ctx ctx; + ASSERT_EQ(eim_nat_init_ctx(&ctx, &config->global), 0, "forward config initialized"); + ASSERT_EQ(ctx.table[80].src_node_id, 0x1234, "HEX target loaded from config"); + eim_nat_destroy_ctx(&ctx); + config->global.nat_forward_count = 2; + config->global.nat_forwards[1] = config->global.nat_forwards[0]; + int r = eim_nat_init_ctx(&ctx, &config->global); + free_config(config); + ASSERT_EQ(r, -1, "duplicate config fails initialization"); + ASSERT(!ctx.initialized && !ctx.table, "failed init releases NAT table"); + PASS(); + TEST("static_forward_invalid_config"); + const char* invalid[] = { + "forward=udp:10.0.0.2:40000:80", "forward=udp:0:10.0.0.2:40000:80", + "forward=udp::10.0.0.2:40000:80", "forward=udp:1234:10.0.0.2:40000:80:extra", + "forward=udp:10000000000000000:10.0.0.2:40000:80", "forward=udp:1234:10.0.0.2:65536:80", + "forward=udp:1234:10.0.0.2:40000:0", "forward=udp:1234:10.0.0.2:40000:-1", + "forward=udp:1234:10.0.0.2:40000:80x", "port_start=65536", "port_end=-1", "nat_via=1234x" + }; + for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { + char buf[160]; snprintf(buf, sizeof(buf), "[nat]\n%s\n", invalid[i]); + config = parse_config_from_buf(buf, strlen(buf), "nat-forward-invalid"); + int rejected = !config; + if (config) free_config(config); + ASSERT(rejected, "invalid NAT config rejected"); + } + PASS(); +} + /* ================================================================ * Main * ================================================================ */ @@ -996,6 +1064,7 @@ int main(void) { test_udp_checksum_encoding(0); test_udp_checksum_encoding(1); test_node_mapping_isolation(); + test_static_forward_owners(); printf("\n=== Results: %d run, %d passed, %d failed ===\n", stats.run, stats.passed, stats.failed); diff --git a/tests/test_nat_transport.c b/tests/test_nat_transport.c index 08f72233..fd589e53 100644 --- a/tests/test_nat_transport.c +++ b/tests/test_nat_transport.c @@ -496,6 +496,30 @@ static int test_send_failure_ownership(int provider) { } #endif +/* Первый входящий пакет по статическому пробросу ниже динамического диапазона доходит до заданного узла. */ +static int test_static_forward_delivery(void) { + if (eim_nat_add_forward(&inst_provider->nat, IPPROTO_UDP_UINT8, 0x0a0000ce, htobe16(8081), 8080, g_client_node_id)) return 0; + size_t len; + uint8_t* packet = build_udp_pkt(0x08080808, 53, inst_provider->nat.gateway_ip, 8080, &len); + test_state.capture_count = 0; test_state.captured_len = 0; + int r = tun_inject_packet(inst_provider->nat_tr.nat_tun, packet, len); + free(packet); + if (r) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Static forward injection failed"); return 0; } + for (int i = 0; i < 500 && !test_timed_out && !test_state.capture_count; i++) { + uasync_poll(ua, 10); capture_tun_read(inst_client->nat_tr.nat_tun); + } + uint32_t dst = 0; uint16_t port = 0; + if (test_state.captured_len >= 28) { + memcpy(&dst, test_state.captured + 16, 4); memcpy(&port, test_state.captured + 22, 2); + } + int ok = test_state.captured_len == len && dst == htobe32(0x0a0000ce) && port == htobe16(8081) && + verify_ip_checksum(test_state.captured); + if (ok) DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS static forward: :8080 -> node=%016llx 10.0.0.206:8081", (unsigned long long)g_client_node_id); + else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL static forward: bytes=%zu destination=%08x port=%u", + test_state.captured_len, be32toh(dst), be16toh(port)); + return ok; +} + static int test_full_roundtrip(void) { /* Полный путь через client socketpair → ETCP → provider TUN и обратно. */ DEBUG_INFO(DEBUG_CATEGORY_NAT, "=== test_full_roundtrip ==="); @@ -634,6 +658,7 @@ int main(void) { #endif // Реальная доставка после инъекций проверяет восстановление обеих веток без перезапуска NAT. total++; if (test_full_roundtrip()) passed++; else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL: full_roundtrip"); + total++; if (test_static_forward_delivery()) passed++; else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "FAIL: static_forward"); printf("\n=== NAT Transport Tests: %d/%d passed ===\n", passed, total); test_result = (passed == total) ? 0 : 1;