Browse Source

Test deduplicated STUN groups and fallback promotion through real ETCP

master
evgeny 13 hours ago
parent
commit
f433413c49
  1. 11
      src/transport_layer/stun_nat.c
  2. 126
      tests/test_stun_nat.c

11
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;

126
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 <assert.h>
#include <stdio.h>
#include <stdlib.h>
@ -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;

Loading…
Cancel
Save