Group STUN probes by interface path and refresh independent-port results
This commit is contained in:
1 parent
8b8b73983b
commit
6beae21ec4
2 files changed
+142
-30
No files matched your search
+129
-29
@@ -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 <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user