From f433413c49507a3772a2410dd7c61013b78fcbb4 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 12 Oct 2026 01:01:11 +0200 Subject: [PATCH] Test deduplicated STUN groups and fallback promotion through real ETCP --- src/transport_layer/stun_nat.c | 11 ++- tests/test_stun_nat.c | 126 +++++++++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/src/transport_layer/stun_nat.c b/src/transport_layer/stun_nat.c index 03d7b835..e9a3dd94 100644 --- a/src/transport_layer/stun_nat.c +++ b/src/transport_layer/stun_nat.c @@ -163,7 +163,10 @@ static void finish_probe(struct stun_probe* p) { else if (r->server_count < 2 || r->mapping == STUN_MAP_UNKNOWN || r->filtering == STUN_FILTER_UNKNOWN) r->status = STUN_NAT_PARTIAL; else r->status = STUN_NAT_COMPLETE; } - if (r->status == STUN_NAT_COMPLETE || r->status == STUN_NAT_PARTIAL) p->cached = *r; + if (r->mapping_reason == STUN_REASON_UNSTABLE || r->filtering_reason == STUN_REASON_UNSTABLE) + memset(&p->cached, 0, sizeof(p->cached)); + if (r->status == STUN_NAT_COMPLETE || (r->status == STUN_NAT_PARTIAL && + (r->mapping != STUN_MAP_UNKNOWN || r->filtering != STUN_FILTER_UNKNOWN))) p->cached = *r; p->due = r->status == STUN_NAT_COMPLETE ? r->measured_at_tb + STUN_REFRESH_TB : r->measured_at_tb + STUN_RETRY_TB; if (r->status == STUN_NAT_SKIPPED) p->due = UINT64_MAX; DEBUG_INFO(DEBUG_CATEGORY_NAT, @@ -591,7 +594,9 @@ static int source_snapshot(const struct ETCP_SOCKET* sock, struct stun_source* s source->key.netif_index = sock->netif_index; source->skipped = sock->is_tcp || sock->probe_only || sock->socks_udp || sock->only_local; const struct sockaddr_storage* addr = &sock->local_addr; - if (addr->ss_family != AF_INET || !((const struct sockaddr_in*)addr)->sin_addr.s_addr) addr = &sock->local_addr; + if ((addr->ss_family == AF_INET && !((const struct sockaddr_in*)addr)->sin_addr.s_addr) || + (addr->ss_family == AF_INET6 && !memcmp(&((const struct sockaddr_in6*)addr)->sin6_addr, &in6addr_any, 16)) || + (addr->ss_family != AF_INET && addr->ss_family != AF_INET6)) addr = &sock->interface_addr; if (addr->ss_family == AF_INET6) { source->key.family = 6; memcpy(source->key.address, &((const struct sockaddr_in6*)addr)->sin6_addr, 16); @@ -826,7 +831,7 @@ int stun_nat_get_socket_result(const struct UTUN_INSTANCE* inst, const struct ET struct stun_source source; if (source_snapshot(socket, &source)) return -1; for (const struct stun_probe* p = inst->stun_nat->probes; p; p = p->next) { - if (!key_equal(&p->source.key, &source.key) || !policy_equal(&p->source, &source)) continue; + if (!key_equal(&p->source.key, &source.key) || (!p->conflict && !policy_equal(&p->source, &source))) continue; uint64_t now = get_time_tb(); int fresh = p->cached.measured_at_tb && now >= p->cached.measured_at_tb && now - p->cached.measured_at_tb < STUN_TTL_TB; *result = fresh ? p->cached : p->result; diff --git a/tests/test_stun_nat.c b/tests/test_stun_nat.c index 506faa17..23c32299 100644 --- a/tests/test_stun_nat.c +++ b/tests/test_stun_nat.c @@ -4,6 +4,7 @@ #include "../src/transport_layer/crc32.h" #include "../lib/debug_config.h" #include "test_clock.h" +#include "nat_fixture.h" #include #include #include @@ -26,7 +27,7 @@ struct fixture { struct endpoint endpoints[6]; enum stun_nat_mapping mapping; enum stun_nat_filtering filtering; - int rfc, ignored_change, error_change, drop_alternates, drop_primary, drop_first, tamper, unstable, direct; + int rfc, ignored_change, error_change, drop_alternates, drop_primary, drop_first, tamper, unstable, direct, multiple_clients; uint8_t first_id[12], late[80]; size_t late_len; struct sockaddr_in test_client; @@ -74,10 +75,12 @@ static void server_read(socket_t fd, void* arg) { assert(get32(request + len - 4) == (crc32_calc(request, (size_t)len - 8) ^ 0x5354554eu)); unsigned flags = len == 36 ? get32(request + 24) : 0; e->requests++; - assert(client.sin_addr.s_addr == ((struct sockaddr_in*)&f->source.local_addr)->sin_addr.s_addr); - assert(client.sin_port != ((struct sockaddr_in*)&f->source.local_addr)->sin_port); - if (!f->test_client.sin_family) f->test_client = client; - assert(client.sin_port == f->test_client.sin_port); + if (!f->multiple_clients) { + assert(client.sin_addr.s_addr == ((struct sockaddr_in*)&f->source.local_addr)->sin_addr.s_addr); + assert(client.sin_port != ((struct sockaddr_in*)&f->source.local_addr)->sin_port); + if (!f->test_client.sin_family) f->test_client = client; + assert(client.sin_port == f->test_client.sin_port); + } if (!e->index && !flags) f->primary_bindings++; if (f->drop_primary && !e->index) return; if (f->drop_alternates && e->index >= 1 && e->index <= 3) return; @@ -144,7 +147,7 @@ static void server_read(socket_t fd, void* arg) { /* Создаёт четыре RFC5780 endpoint и два независимых обычных сервера в том же event loop. */ static void fixture_init(struct fixture* f) { - f->ua = uasync_create(); + if (!f->ua) f->ua = uasync_create(); assert(f->ua); f->inst.ua = f->ua; f->source.fd = socket_create_udp(AF_INET); @@ -159,6 +162,7 @@ static void fixture_init(struct fixture* f) { f->source.instance = &f->inst; f->source.address_profile.mapping = ADDRESS_MAP_DEPENDENT; f->source.address_profile.inbound = ADDRESS_IN_CONTACT; + f->source.address_profile.source = ADDRESS_SOURCE_ETCP; f->source.address_profile.evidence_id = 1; f->source.address_profile.checked_at = 1; f->source.nat_addr = f->source.local_addr; @@ -277,6 +281,114 @@ static void run_case(const char* name, struct fixture* f, enum stun_nat_mapping fixture_destroy(f); } +/* Настоящий дополнительный рабочий порт; ссылка source остаётся собственностью теста. */ +static struct ETCP_SOCKET extra_socket(struct fixture* f, const char* name, const char* ip) { + struct ETCP_SOCKET socket = f->source; + socket.next = NULL; socket.generation++; + snprintf(socket.name, sizeof(socket.name), "%s", name); + socket.fd = socket_create_udp(AF_INET); assert(socket.fd != SOCKET_INVALID); + struct sockaddr_in local = {0}; local.sin_family = AF_INET; + assert(inet_pton(AF_INET, ip, &local.sin_addr) == 1); + assert(!bind(socket.fd, (struct sockaddr*)&local, sizeof(local))); + socklen_t length = sizeof(socket.local_addr); + assert(!getsockname(socket.fd, (struct sockaddr*)&socket.local_addr, &length)); + socket.interface_addr = socket.local_addr; + return socket; +} + +/* Дедупликация по IP/netif, смена представителя, независимые пути и неоднозначная политика. */ +static void test_groups(void) { + struct fixture f = {.mapping = STUN_MAP_EIM}; fixture_init(&f); + struct ETCP_SOCKET duplicate = extra_socket(&f, "duplicate", "127.0.0.1"); + f.source.next = &duplicate; f.original = f.source; + fixture_start(&f, 0, 0, 0); + struct stun_nat_result first = wait_result(&f), second; + assert(!stun_nat_get_socket_result(&f.inst, &duplicate, &second)); + assert(first.members == 2 && first.evidence_id == second.evidence_id && first.generation == second.generation); + assert(first.measured_at_us == second.measured_at_us && first.local.ss_family == AF_INET); + unsigned requests = f.endpoints[0].requests; + f.inst.etcp_sockets = &duplicate; test_clock_run_for(f.ua, 30001); + assert(!stun_nat_get_socket_result(&f.inst, &duplicate, &second)); + assert(second.members == 1 && second.evidence_id == first.evidence_id && f.endpoints[0].requests == requests); + f.inst.etcp_sockets = &f.source; f.multiple_clients = 1; + duplicate.netif_index = 1; test_clock_run_for(f.ua, 30001); + uint64_t end = get_time_tb() + 3000000; + do { assert(!stun_nat_get_socket_result(&f.inst, &duplicate, &second)); + if (second.status != STUN_NAT_PENDING) break; test_clock_poll(f.ua, 10000); + } while (get_time_tb() < end); + assert(second.key.netif_index == 1 && second.evidence_id != first.evidence_id); + duplicate.netif_index = 0; + if (!socket_bind_to_device(duplicate.fd, "lo")) { + test_clock_run_for(f.ua, 30001); + assert(!stun_nat_get_socket_result(&f.inst, &f.source, &second)); + assert(second.status == STUN_NAT_SKIPPED && second.mapping_reason == STUN_REASON_ROUTE); + assert(!stun_nat_get_socket_result(&f.inst, &duplicate, &second)); + assert(second.mapping_reason == STUN_REASON_ROUTE); + } else DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN routing conflict subtest unavailable: bind-to-device denied"); + socket_close_wrapper(duplicate.fd); f.source.next = NULL; f.original = f.source; + fixture_destroy(&f); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS STUN groups: duplicate ports, representative removal, separate netif, policy conflict"); +} + +static void wait_source(struct nat_fixture* nf, struct ETCP_SOCKET* socket, uint8_t source) { + uint64_t end = get_time_tb() + 3000000; + while (get_time_tb() < end) { + if (socket->address_profile.source == source) return; + test_clock_poll(nf->ua, 10000); + } + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT source wait failed socket=%s source=%s expected=%s", socket->name, + address_profile_source_name(socket->address_profile.source), address_profile_source_name(source)); + abort(); +} + +/* Настоящее ядро: нет узлов -> один наблюдатель -> независимый третий узел. + * Сервер сообщает translated EIM, реальный рабочий loopback endpoint — UNCHANGED. */ +static void test_fallback(void) { + struct nat_fixture nf; nat_fixture_init(&nf); + struct fixture server = {.ua = nf.ua, .rfc = 1, .mapping = STUN_MAP_EIM, + .filtering = STUN_FILTER_EIF, .multiple_clients = 1}; fixture_init(&server); + char config[512]; + snprintf(config, sizeof(config), "[stun]\nenabled=yes\nserver=127.0.0.1:%u\nserver=127.0.0.3:%u\n" + "[server:duplicate]\naddr=127.0.0.1:0\ntype=public\n", ntohs(server.endpoints[0].addr.sin_port), + ntohs(server.endpoints[4].addr.sin_port)); + struct UTUN_INSTANCE* owner = nat_fixture_node(&nf, "127.0.0.1", NULL, config); + struct ETCP_SOCKET* socket = test_etcp_bound_socket(owner, "udp"); + struct ETCP_SOCKET* duplicate = test_etcp_bound_socket(owner, "duplicate"); + wait_source(&nf, socket, ADDRESS_SOURCE_STUN); wait_source(&nf, duplicate, ADDRESS_SOURCE_STUN); + assert(socket->address_profile.mapping == ADDRESS_MAP_EIM && socket->address_profile.inbound == ADDRESS_IN_OPEN); + assert(socket->address_profile.evidence_id == duplicate->address_profile.evidence_id); + assert(!socket->nat_addr.ss_family && !duplicate->nat_addr.ss_family); + struct nat_check_status status; assert(!nat_detection_get_status(owner->nat_det, socket, &status)); + assert(status.reason == NAT_REASON_NO_OBSERVER && status.fallback_allowed); + struct TOPO_NODE* snapshot = topo_node_registry_find(owner->topo_groups, owner->node_id); + assert(snapshot && !topo_node_verify(snapshot)); + for (const struct TOPO_ADDR4* address = snapshot->v4_addrs; address; address = address->next) + assert(address->type != TOPO_ADDR_NAT); /* тестовый внешний порт не опубликован */ + struct UTUN_INSTANCE* observer = nat_fixture_node(&nf, "127.0.0.5", NULL, NULL); + nat_fixture_connect(&nf, owner, "udp", observer); nat_detection_request_check_all(owner->nat_det); + uint64_t end = get_time_tb() + 300000; + do { test_clock_poll(nf.ua, 10000); nat_detection_get_status(owner->nat_det, socket, &status); } + while (get_time_tb() < end && status.reason != NAT_REASON_NO_HELPER); + assert(status.reason == NAT_REASON_NO_HELPER && socket->address_profile.source == ADDRESS_SOURCE_STUN); + struct UTUN_INSTANCE* helper = nat_fixture_node(&nf, "127.0.0.6", NULL, NULL); + nat_fixture_connect(&nf, observer, "udp", helper); nat_detection_request_check_all(owner->nat_det); + wait_source(&nf, socket, ADDRESS_SOURCE_ETCP); wait_source(&nf, duplicate, ADDRESS_SOURCE_ETCP); + assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_OPEN); + uint64_t evidence = socket->address_profile.evidence_id; + struct stun_nat_result prior, next; + assert(!stun_nat_get_socket_result(owner, socket, &prior)); stun_nat_recheck(owner); + end = get_time_tb() + 3000000; + do { test_clock_poll(nf.ua, 10000); assert(!stun_nat_get_socket_result(owner, socket, &next)); } + while (get_time_tb() < end && next.status == STUN_NAT_PENDING); + assert(next.mapping == STUN_MAP_EIM && next.evidence_id != prior.evidence_id); + assert(socket->address_profile.source == ADDRESS_SOURCE_ETCP && socket->address_profile.evidence_id == evidence); + for (unsigned i = 0; i < 6; i++) { + assert(!uasync_remove_socket_t(nf.ua, server.endpoints[i].fd)); socket_close_wrapper(server.endpoints[i].fd); + } + socket_close_wrapper(server.source.fd); nat_fixture_cleanup(&nf); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS STUN fallback: no peers, no independent helper, ETCP priority, unchanged real endpoint"); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); @@ -284,6 +396,7 @@ int main(void) { if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG); test_clock_init(10000); test_config(); + test_groups(); struct fixture ordinary = { .mapping = STUN_MAP_EIM }; run_case("ordinary equal", &ordinary, STUN_MAP_MATCH, STUN_FILTER_UNKNOWN); struct fixture dependent = { .mapping = STUN_MAP_ADM }; @@ -351,6 +464,7 @@ int main(void) { result = wait_result(&changed); assert(result.mapping == STUN_MAP_MATCH); fixture_destroy(&changed); + test_fallback(); test_clock_finish(); puts("PASS STUN independent sockets, RFC5780, fallback, lifecycle and ETCP isolation"); return 0;