From 71abf74bfc7092f58b5278c9666d681767828128 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 17 Oct 2026 17:48:55 +0200 Subject: [PATCH] tests: cover unknown NAT candidates and stalled ingress control --- tests/Makefile.am | 2 +- tests/test_nat_detection_faults.c | 68 +++++++++++++++++++++++++++++-- tests/test_sock_match.c | 3 ++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index 45baf7be..cbf3872e 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -451,7 +451,7 @@ test_nat_detection_faults_SOURCES = test_nat_detection_faults.c $(test_clock_sou test_nat_detection_faults_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_nat_detection_faults_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_nat_detection_faults_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=socket_sendto \ - -Wl,--wrap=nat_detection_record_observation -Wl,--wrap=nat_detection_receive_ingress + -Wl,--wrap=nat_detection_record_observation -Wl,--wrap=nat_detection_receive_ingress -Wl,--wrap=etcp_route_send endif test_stun_nat_SOURCES = test_stun_nat.c $(test_clock_sources) diff --git a/tests/test_nat_detection_faults.c b/tests/test_nat_detection_faults.c index b2c8a641..5eaab589 100644 --- a/tests/test_nat_detection_faults.c +++ b/tests/test_nat_detection_faults.c @@ -2,10 +2,11 @@ * Подмена наблюдения отдельно проверяет применение подписанных mapping-отчётов. */ #include "nat_fixture.h" #include "test_clock.h" +#include "routing_layer/etcp_router.h" enum fault_mode { FAULT_NONE, FAULT_NO_PONG, FAULT_FILTERED, FAULT_LATE_PRE, FAULT_CONTACT, FAULT_DEPENDENT, FAULT_UNSTABLE, FAULT_NO_REPORT, FAULT_INGRESS_BLOCKED, FAULT_NO_ANCHOR, - FAULT_LOCAL_INGRESS, FAULT_LOCAL_OBSERVER, FAULT_LOCAL_HELPER_MAPPING }; + FAULT_LOCAL_INGRESS, FAULT_LOCAL_OBSERVER, FAULT_LOCAL_HELPER_MAPPING, FAULT_CONTROL_STALL }; static struct { struct nat_fixture *fixture; @@ -26,6 +27,17 @@ void __real_nat_detection_record_observation(struct NAT_DETECTION *, struct ETCP const struct sockaddr_storage *, const uint8_t *, size_t); void __real_nat_detection_receive_ingress(struct NAT_DETECTION *, struct ETCP_SOCKET *, const uint8_t *, const struct sockaddr_storage *, const uint8_t *, size_t); +int __real_etcp_route_send(struct UTUN_INSTANCE *, uint64_t, uint64_t, struct ll_entry *, int, int); + +/* Помощник отправляет пробы, но его управляющие результаты до наблюдателя не доходят. */ +int __wrap_etcp_route_send(struct UTUN_INSTANCE *instance, uint64_t group, uint64_t destination, + struct ll_entry *entry, int force, int mode) { + if (fault.fixture && fault.mode == FAULT_CONTROL_STALL && instance == fault.helper && + destination == fault.observer->node_id && entry->len && entry->dgram[0] == ETCP_RT_ID_NAT_DETECTION) { + fault.dropped++; queue_entry_free(entry); return 0; + } + return __real_etcp_route_send(instance, group, destination, entry, force, mode); +} static void ignored_ping(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) { (void)success; (void)rtt; (void)arg; (void)nonce; (void)data; (void)length; @@ -88,7 +100,7 @@ ssize_t __wrap_socket_sendto(socket_t fd, const void *packet, size_t length, con &source->local_addr, 500, ignored_ping, NULL, NULL, 0, 0)); DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture: real outgoing PING contaminates PRE source IP"); } - if (fault.mode == FAULT_INGRESS_BLOCKED || + if (fault.mode == FAULT_INGRESS_BLOCKED || fault.mode == FAULT_CONTROL_STALL || ((fault.mode == FAULT_FILTERED || fault.mode == FAULT_LATE_PRE || fault.mode == FAULT_NO_ANCHOR) && !fault.contacted)) { if (fault.mode == FAULT_LATE_PRE && !fault.delayed_length && !fault.injected) { memcpy(fault.delayed, packet, length); fault.delayed_length = length; fault.delayed_fd = fd; @@ -180,6 +192,7 @@ static void run_case(enum fault_mode mode, uint8_t mapping, uint8_t inbound) { if (mode == FAULT_LATE_PRE || mode == FAULT_CONTACT) assert(fault.injected); if (mode == FAULT_DEPENDENT || mode == FAULT_UNSTABLE) assert(fault.altered); if (mode == FAULT_LOCAL_OBSERVER) assert(fault.injected && status.helper_reselections && !fault.observations); + if (mode == FAULT_CONTROL_STALL) assert(fault.dropped && fault.observations >= 2 && status.rounds == rounds + 1); fault.fixture = NULL; nat_fixture_cleanup(&fixture); DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT fault mode=%d", mode); } @@ -241,6 +254,54 @@ static void run_lan_observation(void) { DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS LAN observation preserves internet mapping and inbound profile"); } +/* Одно соединение даёт NAT-кандидат с типом UNKNOWN; он подписывается и распространяется отдельно от адреса интерфейса. */ +static void run_unknown_address(void) { + struct nat_fixture fixture; nat_fixture_init(&fixture); + struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL); + struct UTUN_INSTANCE *peer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL); + nat_detection_set_automatic(owner->nat_det, 0); nat_detection_set_automatic(peer->nat_det, 0); + struct ETCP_CONN *connection = nat_fixture_connect(&fixture, owner, "udp", peer); + struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp"); + struct sockaddr_storage observed = socket->local_addr; + assert(inet_pton(AF_INET, "127.0.0.9", &((struct sockaddr_in *)&observed)->sin_addr) == 1); + nat_detection_observed_by_peer(owner->nat_det, connection->links, &observed); + assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN); + assert(socket->address_profile.source == ADDRESS_SOURCE_ETCP && route_ping_addr_equal(&socket->nat_addr, &observed, 1)); + struct TOPO_NODE *node = topo_node_registry_find(owner->topo_groups, owner->node_id); + assert(node && !topo_node_verify(node)); + sqlite3_stmt *statement = NULL; + assert(sqlite3_prepare_v2(owner->topo_sqlite_db, + "SELECT addr_type,nat_mapping,nat_inbound,nat_source,address FROM node_addresses" + " WHERE node_id=? AND socket_id=? AND address_kind=?", -1, &statement, NULL) == SQLITE_OK); + sqlite3_bind_int64(statement, 1, (sqlite3_int64)owner->node_id); + sqlite3_bind_int(statement, 2, socket->sock_id); sqlite3_bind_int(statement, 3, TOPO_ADDR_NAT); + assert(sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int(statement, 0) == ADDR_TYPE_NAT); + assert(!sqlite3_column_int(statement, 1) && !sqlite3_column_int(statement, 2)); + assert(sqlite3_column_int(statement, 3) == ADDRESS_SOURCE_ETCP); + assert(sqlite3_column_bytes(statement, 4) == 4); + assert(!memcmp(sqlite3_column_blob(statement, 4), &((struct sockaddr_in *)&observed)->sin_addr, 4)); + sqlite3_finalize(statement); + uint64_t end = get_time_tb() + 30000; int received = 0; + while (get_time_tb() < end && !received) { + test_clock_poll(fixture.ua, 1000); + node = topo_node_registry_find(peer->topo_groups, owner->node_id); + for (struct TOPO_ADDR4 *a = node ? node->v4_addrs : NULL; a; a = a->next) + if (a->type == TOPO_ADDR_NAT && a->socket_id == socket->sock_id && + !memcmp(a->addr, &((struct sockaddr_in *)&observed)->sin_addr, 4)) received = 1; + } + assert(received && !topo_node_verify(node)); + struct nat_check_status status; + assert(!nat_detection_get_status(owner->nat_det, socket, &status)); uint32_t rounds = status.rounds; + assert(!nat_detection_request_check_socket(owner->nat_det, socket, peer->node_id)); + end = get_time_tb() + 400000; + do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, socket, &status)); } + while (get_time_tb() < end && (status.rounds == rounds || status.phase || status.cycling)); + assert(!status.phase && !status.cycling && status.reason == NAT_REASON_NO_HELPER); + assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN && route_ping_addr_equal(&socket->nat_addr, &observed, 1)); + nat_fixture_cleanup(&fixture); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT type=UNKNOWN keeps one observed candidate in signed NODEINFO and SQLite"); +} + /* У наблюдателя первый UDP listener недоступен, но управление по TCP и второй IP работают. */ static void run_endpoint_retry(void) { struct nat_fixture fixture; nat_fixture_init(&fixture); @@ -283,8 +344,9 @@ int main(void) { run_case(FAULT_INGRESS_BLOCKED, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN); run_case(FAULT_NO_ANCHOR, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN); run_case(FAULT_LOCAL_OBSERVER, ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN); + run_case(FAULT_CONTROL_STALL, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN); run_alias("127.0.0.3", FAULT_NONE); run_alias("127.0.0.2", FAULT_NONE); run_alias("127.0.0.9", FAULT_LOCAL_INGRESS); run_alias("127.0.0.9", FAULT_LOCAL_HELPER_MAPPING); - run_lan_observation(); run_endpoint_retry(); + run_lan_observation(); run_unknown_address(); run_endpoint_retry(); test_clock_finish(); return 0; } diff --git a/tests/test_sock_match.c b/tests/test_sock_match.c index 63acd27a..f9567ef6 100644 --- a/tests/test_sock_match.c +++ b/tests/test_sock_match.c @@ -12,6 +12,9 @@ int main(void) { const uint32_t public_ip = htonl(UINT32_C(0xc633640b)); const uint32_t private_ip = htonl(UINT32_C(0x0a400102)); struct address_profile unknown = {0}; + struct address_profile observed = {.source = ADDRESS_SOURCE_ETCP}; + assert(address_profile_valid(&observed)); + assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &observed, 1) == SOCK_TC_PROBE); struct address_profile open = {.mapping = ADDRESS_MAP_EIM, .inbound = ADDRESS_IN_OPEN, .source = ADDRESS_SOURCE_ETCP}; struct address_profile contact = open; contact.inbound = ADDRESS_IN_CONTACT; struct address_profile dependent = open; dependent.mapping = ADDRESS_MAP_DEPENDENT; dependent.inbound = ADDRESS_IN_UNKNOWN;