diff --git a/tests/Makefile.am b/tests/Makefile.am index aa9331dd..b3c53d9e 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -445,6 +445,14 @@ test_nat_detection_SOURCES = test_nat_detection.c test_nat_detection_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_nat_detection_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +if OS_LINUX +check_PROGRAMS += test_nat_detection_faults +test_nat_detection_faults_SOURCES = test_nat_detection_faults.c $(test_clock_sources) +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 +endif + test_stun_nat_SOURCES = test_stun_nat.c $(test_clock_sources) test_stun_nat_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_stun_nat_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_nat_detection_faults.c b/tests/test_nat_detection_faults.c new file mode 100644 index 00000000..f50e86cf --- /dev/null +++ b/tests/test_nat_detection_faults.c @@ -0,0 +1,152 @@ +/* Настоящие UDP/router-пакеты; сетевые потери и задержка вводятся на границе sendto. + * Подмена наблюдения отдельно проверяет применение подписанных mapping-отчётов. */ +#include "nat_fixture.h" +#include "test_clock.h" + +enum fault_mode { FAULT_NONE, FAULT_NO_PONG, FAULT_FILTERED, FAULT_LATE_PRE, FAULT_CONTACT, + FAULT_DEPENDENT, FAULT_UNSTABLE, FAULT_NO_REPORT }; + +static struct { + struct nat_fixture *fixture; + struct UTUN_INSTANCE *owner, *observer, *helper; + struct ETCP_SOCKET *socket; + enum fault_mode mode; + unsigned dropped, altered, direct_received, observations; + int contacted, injected; + socket_t delayed_fd; + uint8_t delayed[1600]; + size_t delayed_length; + struct sockaddr_storage delayed_target; + socklen_t delayed_target_length; +} fault; + +ssize_t __real_socket_sendto(socket_t, const void *, size_t, const struct sockaddr *, socklen_t); +void __real_nat_detection_record_observation(struct NAT_DETECTION *, struct ETCP_SOCKET *, const uint8_t *, + const struct sockaddr_storage *, const uint8_t *, size_t); + +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; +} + +/* Расшифровываем только PING/PONG с публичным хвостом, используя настоящие ключи участников стенда. */ +static size_t decode_ping(struct ETCP_SOCKET *socket, const uint8_t *packet, size_t length, uint8_t plain[1600]) { + if (length <= SC_PUBKEY_ENC_SIZE || length > 1600) return 0; + const uint8_t *salt = packet + length - SC_PUBKEY_ENC_SIZE; + for (unsigned i = 0; i < fault.fixture->node_count; i++) { + struct UTUN_INSTANCE *peer = fault.fixture->nodes[i]; + uint8_t obfuscated[SC_PUBKEY_SIZE]; + sc_obfuscate_pubkey(salt, peer->my_keys.public_key, socket->instance->my_keys.public_key, obfuscated); + if (memcmp(obfuscated, salt + SC_PUBKEY_ENC_SALT_SIZE, sizeof(obfuscated))) continue; + sc_context_t crypto; assert(sc_init_ctx(&crypto, &socket->instance->my_keys) == SC_OK); + assert(sc_set_peer_public_key(&crypto, peer->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK); + size_t size = 0; assert(sc_decrypt(&crypto, packet, length - SC_PUBKEY_ENC_SIZE, plain, &size) == SC_OK); + return size >= 23 && (plain[3] == ETCP_PING || plain[3] == ETCP_PONG) ? size : 0; + } + return 0; +} + +ssize_t __wrap_socket_sendto(socket_t fd, const void *packet, size_t length, const struct sockaddr *target, socklen_t target_length) { + if (!fault.fixture) return __real_socket_sendto(fd, packet, length, target, target_length); + struct ETCP_SOCKET *source = NULL; + for (unsigned i = 0; i < fault.fixture->node_count && !source; i++) + for (struct ETCP_SOCKET *socket = fault.fixture->nodes[i]->etcp_sockets; socket; socket = socket->next) + if (socket->fd == fd) { source = socket; break; } + uint8_t plain[1600]; + size_t size = source ? decode_ping(source, packet, length, plain) : 0; + if (!size) return __real_socket_sendto(fd, packet, length, target, target_length); + struct sockaddr_storage destination = {0}; assert(target_length <= sizeof(destination)); + memcpy(&destination, target, target_length); + int to_owner = route_ping_addr_equal(&destination, &fault.socket->local_addr, 1); + if (fault.mode == FAULT_NO_PONG && to_owner && plain[3] == ETCP_PONG) { + fault.dropped++; return length; + } + if (source == fault.socket && plain[3] == ETCP_PING && (plain[4] & ETCP_PING_FLAG_OBSERVE) && + !route_ping_addr_equal(&destination, &test_etcp_bound_socket(fault.observer, "udp")->local_addr, 0)) { + fault.contacted = 1; + ssize_t result = __real_socket_sendto(fd, packet, length, target, target_length); + if (fault.delayed_length) { + assert(__real_socket_sendto(fault.delayed_fd, fault.delayed, fault.delayed_length, + (const struct sockaddr *)&fault.delayed_target, fault.delayed_target_length) == (ssize_t)fault.delayed_length); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture: delayed PRE delivered after outgoing contact"); + fault.delayed_length = 0; fault.injected = 1; + } + return result; + } + if (to_owner && plain[3] == ETCP_PING && (plain[4] & ETCP_PING_FLAG_ADDRESS_CHECK)) { + if (fault.mode == FAULT_CONTACT && !fault.injected) { + fault.injected = 1; + assert(!etcp_send_ping_to_socket(fault.owner, fault.socket, source->instance->my_keys.public_key, + &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_FILTERED || fault.mode == FAULT_LATE_PRE) && !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; + fault.delayed_target = destination; fault.delayed_target_length = target_length; + } + fault.dropped++; return length; + } + fault.direct_received++; + } + return __real_socket_sendto(fd, packet, length, target, target_length); +} + +void __wrap_nat_detection_record_observation(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, + const uint8_t *key, const struct sockaddr_storage *source, const uint8_t *tag, size_t length) { + if (!fault.fixture || socket->instance != fault.helper || memcmp(key, fault.owner->my_keys.public_key, 32) || + length != ROUTE_OBSERVE_TAG_SIZE + 9) { + __real_nat_detection_record_observation(detector, socket, key, source, tag, length); return; + } + fault.observations++; + if (fault.mode == FAULT_NO_REPORT) { fault.dropped++; return; } + struct sockaddr_storage observed = *source; + if (fault.mode == FAULT_DEPENDENT || (fault.mode == FAULT_UNSTABLE && fault.observations == 1)) { + struct sockaddr_in *address = (struct sockaddr_in *)&observed; + address->sin_port = htons(ntohs(address->sin_port) == 65535 ? 65534 : ntohs(address->sin_port) + 1); + fault.altered++; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture: observed mapping changed from=%s to=%s", + sockaddr_storage_to_str(source).str, sockaddr_storage_to_str(&observed).str); + } + __real_nat_detection_record_observation(detector, socket, key, &observed, tag, length); +} + +static void run_case(enum fault_mode mode, uint8_t mapping, uint8_t inbound) { + 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 *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL); + struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", NULL, NULL); + nat_detection_set_automatic(observer->nat_det, 0); nat_detection_set_automatic(helper->nat_det, 0); + nat_fixture_connect(&fixture, observer, "udp", helper); + nat_fixture_connect(&fixture, owner, "udp", observer); + nat_detection_set_automatic(owner->nat_det, 0); + memset(&fault, 0, sizeof(fault)); fault.fixture = &fixture; fault.owner = owner; + fault.observer = observer; fault.helper = helper; fault.socket = test_etcp_bound_socket(owner, "udp"); fault.mode = mode; + assert(!nat_detection_request_check_socket(owner->nat_det, fault.socket, observer->node_id)); + struct nat_check_status status = {0}; + uint64_t end = get_time_tb() + 400000; + do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, fault.socket, &status)); } + while (get_time_tb() < end && (!status.rounds || status.phase || status.cycling)); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture result mode=%d mapping=%s inbound=%s phase=%u cycling=%u reason=%s dropped=%u observations=%u", + mode, address_mapping_name(fault.socket->address_profile.mapping), address_inbound_name(fault.socket->address_profile.inbound), + status.phase, status.cycling, nat_check_reason_name(status.reason), fault.dropped, fault.observations); + assert(!status.phase && !status.cycling); + assert(fault.socket->address_profile.mapping == mapping && fault.socket->address_profile.inbound == inbound); + if (mode == FAULT_NO_PONG || mode == FAULT_FILTERED || mode == FAULT_LATE_PRE || mode == FAULT_NO_REPORT) assert(fault.dropped); + if (mode == FAULT_LATE_PRE || mode == FAULT_CONTACT) assert(fault.injected); + if (mode == FAULT_DEPENDENT || mode == FAULT_UNSTABLE) assert(fault.altered); + fault.fixture = NULL; nat_fixture_cleanup(&fixture); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT fault mode=%d", mode); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG); + test_clock_init(1000); + run_case(FAULT_NO_PONG, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN); + run_case(FAULT_FILTERED, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_CONTACT); + run_case(FAULT_LATE_PRE, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_CONTACT); + run_case(FAULT_CONTACT, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN); + run_case(FAULT_DEPENDENT, ADDRESS_MAP_DEPENDENT, ADDRESS_IN_OPEN); + run_case(FAULT_UNSTABLE, ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN); + run_case(FAULT_NO_REPORT, ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN); + test_clock_finish(); return 0; +}