From 6beae21ec444e00a9cf2df9142b58b8835fb1208 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 12 Oct 2026 00:55:29 +0200 Subject: [PATCH] Group STUN probes by interface path and refresh independent-port results --- src/transport_layer/stun_nat.c | 158 +++++++++++++++++++++++++++------ src/transport_layer/stun_nat.h | 14 ++- 2 files changed, 142 insertions(+), 30 deletions(-) diff --git a/src/transport_layer/stun_nat.c b/src/transport_layer/stun_nat.c index f644b2ad..03d7b835 100644 --- a/src/transport_layer/stun_nat.c +++ b/src/transport_layer/stun_nat.c @@ -5,6 +5,7 @@ #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/platform_compat.h" +#include "../ntp_time.h" #include "crc32.h" #include #include @@ -24,12 +25,16 @@ #define STUN_OTHER 0x802c #define STUN_REQUESTS 7 #define STUN_SCAN_TB 30000 +#define STUN_TTL_TB UINT64_C(3000000) +#define STUN_REFRESH_TB UINT64_C(1800000) +#define STUN_RETRY_TB UINT64_C(300000) enum stun_stage { STUN_BASE, STUN_FILTER_BOTH, STUN_CONTROL_BOTH, STUN_FILTER_PORT, STUN_CONTROL_PORT, STUN_CHECK_OTHER, STUN_CHECK_PORT, STUN_MAP_IP, STUN_MAP_PORT, STUN_VERIFY }; /* Снимок исходного сокета используется только для выбора того же сетевого пути. */ struct stun_source { + struct stun_nat_key key; char name[MAX_CONN_NAME_LEN]; struct sockaddr_in addr; uint32_t netif_index; @@ -37,6 +42,9 @@ struct stun_source { char device[64]; }; +/* Порты не переиспользуются и после удаления/пересоздания группы в этом ядре. */ +struct stun_ports { struct stun_ports* next; struct stun_nat_key key; uint8_t used[2048]; }; + struct stun_history { struct stun_history* next; struct sockaddr_in addr; @@ -47,13 +55,17 @@ struct stun_probe { struct STUN_NAT* owner; struct stun_source source; struct stun_nat_result result; + struct stun_nat_result cached; + struct stun_ports* ports; + uint64_t generation, due; + unsigned members; + int changed, conflict, scan_conflict; socket_t fd; void* socket_handle; void* timer; struct adns_query* dns; const struct CFG_STUN_SERVER* cursor; uint16_t dns_port; - uint16_t last_port; struct stun_history* history; struct sockaddr_in primary, other, reference, base_mapping, ip_mapping, target, expected; enum stun_stage stage; @@ -67,6 +79,7 @@ struct stun_probe { struct STUN_NAT { struct UTUN_INSTANCE* inst; struct stun_probe* probes; + struct stun_ports* ports; void* scan_timer; uint64_t next_transaction_tb; }; @@ -82,6 +95,21 @@ static void begin_mapping(struct stun_probe* p); static void finish_server(struct stun_probe* p); static void scan_sources(struct STUN_NAT* mod); +static int key_equal(const struct stun_nat_key* a, const struct stun_nat_key* b) { + return a->family == b->family && a->netif_index == b->netif_index && !memcmp(a->address, b->address, 16); +} + +static int policy_equal(const struct stun_source* a, const struct stun_source* b) { + return a->mark == b->mark && a->fib == b->fib && a->skipped == b->skipped && !strcmp(a->device, b->device); +} + +static int random_id(uint64_t* value) { + do { if (random_bytes((uint8_t*)value, sizeof(*value))) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN group/evidence random generation failed"); return -1; + } } while (!*value); + return 0; +} + static uint16_t read16(const uint8_t* p) { return ((uint16_t)p[0] << 8) | p[1]; } static uint32_t read32(const uint8_t* p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } static void write16(uint8_t* p, uint16_t v) { p[0] = v >> 8; p[1] = v; } @@ -102,7 +130,8 @@ const char* stun_nat_filtering_name(enum stun_nat_filtering v) { const char* stun_nat_reason_name(enum stun_nat_reason v) { static const char* names[] = { "ok", "RFC5780 unsupported", "prior STUN traffic", "timeout", "server error", - "unstable mapping", "no independent server pair", "DNS failure", "socket I/O failure", "unsupported socket", "out of memory" }; + "unstable mapping", "no independent server pair", "DNS failure", "socket I/O failure", "unsupported socket", "out of memory", + "incompatible routing policy", "no fresh test port" }; return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "invalid reason"; } @@ -126,11 +155,17 @@ static void close_probe_io(struct stun_probe* p) { static void finish_probe(struct stun_probe* p) { struct stun_nat_result* r = &p->result; r->measured_at_tb = get_time_tb(); + int64_t now_us = ntp_time_get_us(p->owner->inst); + if (now_us > 0) r->measured_at_us = (uint64_t)now_us; + else DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN measurement wall time unavailable source=%s", p->source.name); if (r->status == STUN_NAT_PENDING) { if (!r->server_count) r->status = STUN_NAT_FAILED; 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; + 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, "STUN result source=%s local=%s servers=%u mapping=%s (%s) filtering=%s (%s) translated=%d RFC5780=%d status=%d", p->source.name, sockaddr_storage_to_str(&r->local).str, r->server_count, @@ -553,9 +588,17 @@ static int source_snapshot(const struct ETCP_SOCKET* sock, struct stun_source* s memset(source, 0, sizeof(*source)); snprintf(source->name, sizeof(source->name), "%s", sock->name); source->netif_index = sock->netif_index; - source->skipped = sock->is_tcp || sock->socks_udp || sock->only_local; - const struct sockaddr_storage* addr = &sock->interface_addr; + 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_INET6) { + source->key.family = 6; + memcpy(source->key.address, &((const struct sockaddr_in6*)addr)->sin6_addr, 16); + } else if (addr->ss_family == AF_INET) { + source->key.family = 4; + memcpy(source->key.address, &((const struct sockaddr_in*)addr)->sin_addr, 4); + } if (addr->ss_family != AF_INET || !((const struct sockaddr_in*)addr)->sin_addr.s_addr) source->skipped = 1; else { source->addr = *(const struct sockaddr_in*)addr; source->addr.sin_port = 0; } if (source->skipped) return 0; @@ -589,15 +632,25 @@ static int open_probe_socket(struct stun_probe* p) { #endif struct sockaddr_in local = p->source.addr; int bound = 0; - for (int i = 0; i < 16; i++) { + for (int i = 0; i < 32; i++) { uint16_t random_port; if (random_bytes((uint8_t*)&random_port, sizeof(random_port)) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s random port generation failed", p->source.name); return -1; } - local.sin_port = htons(49152 + (random_port & 16383)); - if (local.sin_port == p->last_port) continue; - if (bind(p->fd, (const struct sockaddr*)&local, sizeof(local)) == 0) { bound = 1; break; } + unsigned port = random_port & 16383; + local.sin_port = htons(49152 + port); + if (p->ports->used[port / 8] & (1u << (port % 8))) continue; + int working = 0; + for (const struct ETCP_SOCKET* socket = p->owner->inst->etcp_sockets; socket; socket = socket->next) { + if (socket->local_addr.ss_family != AF_INET) continue; + const struct sockaddr_in* address = (const struct sockaddr_in*)&socket->local_addr; + if (address->sin_addr.s_addr == local.sin_addr.s_addr && address->sin_port == local.sin_port) working = 1; + } + if (working) continue; + if (bind(p->fd, (const struct sockaddr*)&local, sizeof(local)) == 0) { + p->ports->used[port / 8] |= (uint8_t)(1u << (port % 8)); bound = 1; break; + } #ifdef _WIN32 if (socket_get_error() != WSAEADDRINUSE) goto error; #else @@ -605,10 +658,10 @@ static int open_probe_socket(struct stun_probe* p) { #endif } if (!bound) { - DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s no free random test port after 16 attempts", p->source.name); + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s no fresh free test port after 32 attempts", p->source.name); + p->result.mapping_reason = p->result.filtering_reason = STUN_REASON_PORTS; return -1; } - p->last_port = local.sin_port; memcpy(&p->result.local, &local, sizeof(local)); p->socket_handle = uasync_add_socket_t(p->owner->inst->ua, p->fd, read_response, NULL, NULL, "stun_probe", p); if (!p->socket_handle) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot register test socket", p->source.name); return -1; } @@ -631,10 +684,11 @@ static void destroy_probe(struct stun_probe* p) { u_free(p); } -/* Начинает измерение на новом порту, исключая непосредственно предыдущий тестовый порт. */ +/* Начинает новый раунд; действующий кэш сохраняется до TTL при штатном refresh. */ static void restart_probe(struct stun_probe* p) { close_probe_io(p); memset(&p->result, 0, sizeof(p->result)); + p->result.key = p->source.key; p->result.generation = p->generation; p->result.members = p->members; p->result.translated = -1; p->result.mapping_reason = STUN_REASON_NO_PAIR; p->result.filtering_reason = STUN_REASON_UNSUPPORTED; @@ -642,44 +696,69 @@ static void restart_probe(struct stun_probe* p) { p->capability = 0; p->pending_filter = STUN_FILTER_UNKNOWN; p->sent = 0; - if (p->source.skipped) { + if (random_id(&p->result.evidence_id)) { + p->result.status = STUN_NAT_FAILED; p->result.mapping_reason = STUN_REASON_IO; finish_probe(p); return; + } + if (p->source.skipped || p->conflict) { p->result.status = STUN_NAT_SKIPPED; - p->result.mapping_reason = p->result.filtering_reason = STUN_REASON_SOCKET; + p->result.mapping_reason = p->result.filtering_reason = p->conflict ? STUN_REASON_ROUTE : STUN_REASON_SOCKET; finish_probe(p); } else if (open_probe_socket(p) < 0) { - p->result.mapping_reason = STUN_REASON_IO; + if (p->result.mapping_reason != STUN_REASON_PORTS) p->result.mapping_reason = STUN_REASON_IO; finish_probe(p); } else next_server(p); } /* Сверяет снимки: новый/изменённый интерфейс получает свежую пробу, исчезнувший удаляется. */ static void scan_sources(struct STUN_NAT* mod) { - for (struct stun_probe* p = mod->probes; p; p = p->next) p->seen = 0; + for (struct stun_probe* p = mod->probes; p; p = p->next) { + p->seen = 0; p->members = 0; p->scan_conflict = 0; p->changed = 0; + } for (struct ETCP_SOCKET* sock = mod->inst->etcp_sockets; sock; sock = sock->next) { - if (sock->is_tcp || sock->probe_only) continue; + if (sock->is_tcp || sock->probe_only || sock->socks_udp || sock->only_local) continue; struct stun_source source; if (source_snapshot(sock, &source) < 0) continue; - struct stun_probe** slot = &mod->probes; - while (*slot && strcmp((*slot)->source.name, source.name)) slot = &(*slot)->next; - if (*slot && !memcmp(&(*slot)->source, &source, sizeof(source))) { (*slot)->seen = 1; continue; } - if (*slot) { - struct stun_probe* old = *slot; - *slot = old->next; - destroy_probe(old); + struct stun_probe* p = mod->probes; + while (p && !key_equal(&p->source.key, &source.key)) p = p->next; + if (p) { + if (!p->seen) { + p->changed = !policy_equal(&p->source, &source); + p->source = source; + } else if (!policy_equal(&p->source, &source)) p->scan_conflict = 1; + p->seen = 1; p->members++; + continue; } - struct stun_probe* p = u_calloc(1, sizeof(*p)); + p = u_calloc(1, sizeof(*p)); if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s probe allocation failed", source.name); continue; } p->owner = mod; p->source = source; - p->seen = 1; + p->seen = 1; p->members = 1; p->changed = 1; + p->ports = mod->ports; + while (p->ports && !key_equal(&p->ports->key, &source.key)) p->ports = p->ports->next; + if (!p->ports) { + p->ports = u_calloc(1, sizeof(*p->ports)); + if (!p->ports) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN port history allocation failed"); u_free(p); continue; } + p->ports->key = source.key; p->ports->next = mod->ports; mod->ports = p->ports; + } p->fd = SOCKET_INVALID; p->next = mod->probes; mod->probes = p; - restart_probe(p); } struct stun_probe** slot = &mod->probes; while (*slot) { - if ((*slot)->seen) { slot = &(*slot)->next; continue; } + if ((*slot)->seen) { + struct stun_probe* p = *slot; + if (p->changed || p->conflict != p->scan_conflict) { + close_probe_io(p); memset(&p->cached, 0, sizeof(p->cached)); + p->conflict = p->scan_conflict; + if (random_id(&p->generation)) { p->generation = 0; p->due = get_time_tb() + STUN_RETRY_TB; } + else restart_probe(p); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN group source=%s IP=%s netif=%u members=%u generation=%016llx conflict=%d", + p->source.name, ip_to_str(&p->source.addr.sin_addr, AF_INET).str, p->source.netif_index, + p->members, (unsigned long long)p->generation, p->conflict); + } else if (p->result.status != STUN_NAT_PENDING && get_time_tb() >= p->due) restart_probe(p); + slot = &p->next; continue; + } struct stun_probe* p = *slot; *slot = p->next; destroy_probe(p); @@ -716,6 +795,7 @@ void stun_nat_destroy(struct UTUN_INSTANCE* inst) { inst->stun_nat = NULL; if (mod->scan_timer) uasync_cancel_timeout(inst->ua, mod->scan_timer); while (mod->probes) { struct stun_probe* next = mod->probes->next; destroy_probe(mod->probes); mod->probes = next; } + while (mod->ports) { struct stun_ports* next = mod->ports->next; u_free(mod->ports); mod->ports = next; } DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN destroyed; test sockets/DNS/timers released"); u_free(mod); } @@ -725,14 +805,34 @@ void stun_nat_recheck(struct UTUN_INSTANCE* inst) { struct STUN_NAT* mod = inst->stun_nat; mod->next_transaction_tb = get_time_tb(); DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN rechecking interfaces using fresh test ports"); - for (struct stun_probe* p = mod->probes; p; p = p->next) restart_probe(p); scan_sources(mod); + for (struct stun_probe* p = mod->probes; p; p = p->next) { + memset(&p->cached, 0, sizeof(p->cached)); + if (random_id(&p->generation)) continue; + restart_probe(p); + } } int stun_nat_get_result(const struct UTUN_INSTANCE* inst, const char* socket_name, struct stun_nat_result* result) { if (!inst || !inst->stun_nat || !socket_name || !result) return -1; + for (const struct ETCP_SOCKET* socket = inst->etcp_sockets; socket; socket = socket->next) + if (!strcmp(socket->name, socket_name)) return stun_nat_get_socket_result(inst, socket, result); + return -1; +} + +int stun_nat_get_socket_result(const struct UTUN_INSTANCE* inst, const struct ETCP_SOCKET* socket, struct stun_nat_result* result) { + if (!inst || !inst->stun_nat || !socket || !result || socket->instance != inst || + socket->is_tcp || socket->probe_only || socket->socks_udp || socket->only_local) return -1; + 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 (!strcmp(p->source.name, socket_name)) { *result = p->result; return 0; } + if (!key_equal(&p->source.key, &source.key) || !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; + result->refreshing = fresh && p->result.status == STUN_NAT_PENDING; + result->members = p->members; + return 0; } return -1; } diff --git a/src/transport_layer/stun_nat.h b/src/transport_layer/stun_nat.h index 89d8e611..87efb3c3 100644 --- a/src/transport_layer/stun_nat.h +++ b/src/transport_layer/stun_nat.h @@ -20,16 +20,25 @@ extern "C" { struct UTUN_INSTANCE; struct STUN_NAT; +struct ETCP_SOCKET; + +/* Ключ пути не содержит порт/имя рабочего сокета. family — 4 или 6. */ +struct stun_nat_key { uint8_t family, address[16]; uint32_t netif_index; }; enum stun_nat_status { STUN_NAT_PENDING, STUN_NAT_COMPLETE, STUN_NAT_PARTIAL, STUN_NAT_FAILED, STUN_NAT_SKIPPED }; enum stun_nat_mapping { STUN_MAP_UNKNOWN, STUN_MAP_MATCH, STUN_MAP_DEPENDENT, STUN_MAP_EIM, STUN_MAP_ADM, STUN_MAP_APDM }; enum stun_nat_filtering { STUN_FILTER_UNKNOWN, STUN_FILTER_EIF, STUN_FILTER_ADF, STUN_FILTER_APDF }; enum stun_nat_reason { STUN_REASON_NONE, STUN_REASON_UNSUPPORTED, STUN_REASON_TRAFFIC, STUN_REASON_TIMEOUT, STUN_REASON_SERVER_ERROR, - STUN_REASON_UNSTABLE, STUN_REASON_NO_PAIR, STUN_REASON_DNS, STUN_REASON_IO, STUN_REASON_SOCKET, STUN_REASON_NOMEM + STUN_REASON_UNSTABLE, STUN_REASON_NO_PAIR, STUN_REASON_DNS, STUN_REASON_IO, STUN_REASON_SOCKET, STUN_REASON_NOMEM, + STUN_REASON_ROUTE, STUN_REASON_PORTS }; struct stun_nat_result { + struct stun_nat_key key; + uint64_t generation, evidence_id, measured_at_us; + unsigned members; + int refreshing; enum stun_nat_status status; enum stun_nat_mapping mapping; enum stun_nat_filtering filtering; @@ -50,6 +59,9 @@ void stun_nat_destroy(struct UTUN_INSTANCE* inst); void stun_nat_recheck(struct UTUN_INSTANCE* inst); /* Копирует результат для имени исходного сокета; -1 если модуль выключен/имя отсутствует. */ int stun_nat_get_result(const struct UTUN_INSTANCE* inst, const char* socket_name, struct stun_nat_result* result); +/* Проверяет принадлежность текущего рабочего сокета группе и её routing options. + * Во время refresh возвращает ещё действующий кэш; время кэша не омолаживается. */ +int stun_nat_get_socket_result(const struct UTUN_INSTANCE* inst, const struct ETCP_SOCKET* socket, struct stun_nat_result* result); const char* stun_nat_mapping_name(enum stun_nat_mapping value); const char* stun_nat_filtering_name(enum stun_nat_filtering value); const char* stun_nat_reason_name(enum stun_nat_reason value);