Add independent STUN NAT probes with RFC 5780 discovery
This commit is contained in:
1 parent
9cc996b775
commit
2c5deafe56
11 files changed
+1315
-2
No files matched your search
@@ -37,6 +37,8 @@ utun_CORE_SOURCES = \
|
||||
transport_layer/etcp_session.c \
|
||||
transport_layer/etcp_session.h \
|
||||
transport_layer/etcp_connections.c \
|
||||
transport_layer/stun_nat.c \
|
||||
transport_layer/stun_nat.h \
|
||||
transport_layer/etcp_keepalive.c \
|
||||
transport_layer/etcp_bbr.c \
|
||||
transport_layer/etcp_loadbalancer.c \
|
||||
@@ -181,6 +183,8 @@ libutun_a_SOURCES = \
|
||||
transport_layer/etcp_session.c \
|
||||
transport_layer/etcp_session.h \
|
||||
transport_layer/etcp_connections.c \
|
||||
transport_layer/stun_nat.c \
|
||||
transport_layer/stun_nat.h \
|
||||
transport_layer/etcp_keepalive.c \
|
||||
transport_layer/etcp_bbr.c \
|
||||
transport_layer/etcp_loadbalancer.c \
|
||||
|
||||
+63
-2
@@ -54,7 +54,8 @@ typedef enum {
|
||||
SECTION_NTP,
|
||||
SECTION_LOG_UDP,
|
||||
SECTION_REALITY,
|
||||
SECTION_SOCKS
|
||||
SECTION_SOCKS,
|
||||
SECTION_STUN
|
||||
} section_type_t;
|
||||
|
||||
static char* trim(char *str) {
|
||||
@@ -955,6 +956,51 @@ static int parse_socks(const char *key, const char *value, struct global_config
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Разбирает общий для файлового и буферного парсера список IPv4 STUN-серверов. */
|
||||
static int parse_stun(const char* key, const char* value, struct global_config* global, const char* filename, int line_num) {
|
||||
if (strcmp(key, "enabled") == 0) {
|
||||
if (!strcasecmp(value, "yes") || !strcasecmp(value, "true") || !strcmp(value, "1")) global->stun_enabled = 1;
|
||||
else if (!strcasecmp(value, "no") || !strcasecmp(value, "false") || !strcmp(value, "0")) global->stun_enabled = 0;
|
||||
else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid stun enabled='%s'", filename, line_num, value); return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "server") != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown stun option '%s'; valid: enabled, server", filename, line_num, key);
|
||||
return -1;
|
||||
}
|
||||
const char* colon = strrchr(value, ':');
|
||||
size_t host_len = colon ? (size_t)(colon - value) : strlen(value);
|
||||
char* end = NULL;
|
||||
long port = colon ? strtol(colon + 1, &end, 10) : 3478;
|
||||
if (!host_len || host_len >= sizeof(((struct CFG_STUN_SERVER*)0)->host) ||
|
||||
(colon && (colon[1] == '\0' || *end || port < 1 || port > 65535))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid STUN server '%s'; expected host[:port]", filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < host_len; i++) {
|
||||
unsigned char c = (unsigned char)value[i];
|
||||
if (!(isalnum(c) || c == '-' || c == '.' || c == '_')) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid IPv4 STUN hostname '%s'", filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
for (struct CFG_STUN_SERVER* s = global->stun_servers; s; s = s->next) {
|
||||
if (strlen(s->host) == host_len && !strncasecmp(s->host, value, host_len) && s->port == port) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: duplicate STUN server '%s'", filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
struct CFG_STUN_SERVER* server = u_calloc(1, sizeof(*server));
|
||||
if (!server) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: cannot allocate STUN server", filename, line_num); return -1; }
|
||||
memcpy(server->host, value, host_len);
|
||||
server->port = (uint16_t)port;
|
||||
struct CFG_STUN_SERVER** tail = &global->stun_servers;
|
||||
while (*tail) tail = &(*tail)->next;
|
||||
*tail = server;
|
||||
global->stun_server_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static section_type_t parse_section_header(const char *line, char *name, size_t name_len) {
|
||||
if (line[0] != '[') return SECTION_UNKNOWN;
|
||||
size_t line_len = strlen(line);
|
||||
@@ -983,6 +1029,7 @@ static section_type_t parse_section_header(const char *line, char *name, size_t
|
||||
if (strcasecmp(section, "log_udp") == 0) return SECTION_LOG_UDP;
|
||||
if (strcasecmp(section, "reality") == 0) return SECTION_REALITY;
|
||||
if (strcasecmp(section, "socks") == 0) return SECTION_SOCKS;
|
||||
if (strcasecmp(section, "stun") == 0) return SECTION_STUN;
|
||||
|
||||
char *colon = strchr(section, ':');
|
||||
if (!colon) return SECTION_UNKNOWN;
|
||||
@@ -1254,12 +1301,19 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
|
||||
case SECTION_SOCKS:
|
||||
parse_socks(key, value, &cfg->global, filename, line_num);
|
||||
break;
|
||||
case SECTION_STUN:
|
||||
if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error;
|
||||
break;
|
||||
default:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg->global.stun_enabled && cfg->global.stun_server_count < 2) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: enabled STUN requires at least two servers", filename);
|
||||
goto error;
|
||||
}
|
||||
// Handle final sections
|
||||
if (cur_section == SECTION_SERVER && cur_server) {
|
||||
cur_server->next = cfg->servers;
|
||||
@@ -1406,6 +1460,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
case SECTION_GUI: break;
|
||||
case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
|
||||
case SECTION_SOCKS: parse_socks(key, value, &cfg->global, filename, line_num); break;
|
||||
case SECTION_STUN: if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error; break;
|
||||
case SECTION_LOG_UDP:
|
||||
if (strcmp(key, "ip") == 0) strncpy(cfg->global.log_udp_ip, value, sizeof(cfg->global.log_udp_ip) - 1);
|
||||
else if (strcmp(key, "port") == 0) cfg->global.log_udp_port = atoi(value);
|
||||
@@ -1413,6 +1468,10 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break;
|
||||
}
|
||||
}
|
||||
if (cfg->global.stun_enabled && cfg->global.stun_server_count < 2) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: enabled STUN requires at least two servers", filename);
|
||||
goto error;
|
||||
}
|
||||
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; }
|
||||
if (cur_section == SECTION_CLIENT && cur_client) { cur_client->next = cfg->clients; cfg->clients = cur_client; }
|
||||
if (cur_section == SECTION_NETWORK && cur_network) { cur_network->next = cfg->networks; cfg->networks = cur_network; }
|
||||
@@ -1494,6 +1553,8 @@ void free_config(struct utun_config *config) {
|
||||
ns = next;
|
||||
}
|
||||
|
||||
struct CFG_STUN_SERVER* stun = config->global.stun_servers;
|
||||
while (stun) { struct CFG_STUN_SERVER* next = stun->next; u_free(stun); stun = next; }
|
||||
u_free(config);
|
||||
}
|
||||
|
||||
@@ -1761,4 +1822,4 @@ int config_peer_in_clients(const struct utun_config* config, const uint8_t* pubk
|
||||
if (memcmp(pk_bin, pubkey, SC_PUBKEY_SIZE) == 0) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,13 @@ struct CFG_NTP_SERVER {
|
||||
struct CFG_NTP_SERVER *next;
|
||||
};
|
||||
|
||||
/* STUN-серверы обходятся в порядке конфигурации; DNS выполняет модуль stun_nat. */
|
||||
struct CFG_STUN_SERVER {
|
||||
struct CFG_STUN_SERVER* next;
|
||||
char host[256];
|
||||
uint16_t port;
|
||||
};
|
||||
|
||||
#define MAX_TCP_PROXY_CLIENT_MAPPINGS 32
|
||||
struct tcp_proxy_client_mapping_config {
|
||||
uint16_t local_port;
|
||||
@@ -218,6 +225,10 @@ struct global_config {
|
||||
struct CFG_NTP_SERVER *ntp_servers; // linked list, unlimited
|
||||
int ntp_server_count;
|
||||
int ntp_resync_interval; // seconds, default 3600
|
||||
|
||||
int stun_enabled; // [stun] enabled=yes; по умолчанию выключено
|
||||
struct CFG_STUN_SERVER* stun_servers;
|
||||
int stun_server_count;
|
||||
|
||||
// Chatserver configuration ([chatserver] section)
|
||||
char chatserver_join_password[128]; // пароль для входа в группу
|
||||
|
||||
@@ -69,6 +69,7 @@ free_config(cfg);
|
||||
| `[tcp_proxy_client]` | `parse_tcp_proxy_client()` | `enabled`, `tun_name`, `tun_ip`, `mtu`, `via_node`, `socks_*`, `http_proxy_*`, `forward` (port -> ip:port) |
|
||||
| `[tcp_proxy_server]` | inline | `tcp_recv_buf` |
|
||||
| `[ntp]` | inline | `enabled`, `server`, `interval` |
|
||||
| `[stun]` | `parse_stun()` | `enabled`, повторяющийся `server=host[:port]`; минимум два сервера при включении |
|
||||
| `[gui]` | игнорируется | (зарезервировано) |
|
||||
|
||||
### Функции
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
#include "stun_nat.h"
|
||||
#include "etcp_connections.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../lib/async_dns.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
#include "crc32.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#define STUN_COOKIE 0x2112a442u
|
||||
#define STUN_FINGERPRINT_XOR 0x5354554eu
|
||||
#define STUN_REQUEST 0x0001
|
||||
#define STUN_SUCCESS 0x0101
|
||||
#define STUN_ERROR 0x0111
|
||||
#define STUN_MAPPED 0x0001
|
||||
#define STUN_CHANGE 0x0003
|
||||
#define STUN_ERROR_CODE 0x0009
|
||||
#define STUN_XOR_MAPPED 0x0020
|
||||
#define STUN_FINGERPRINT 0x8028
|
||||
#define STUN_ORIGIN 0x802b
|
||||
#define STUN_OTHER 0x802c
|
||||
#define STUN_REQUESTS 7
|
||||
#define STUN_SCAN_TB 30000
|
||||
|
||||
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 {
|
||||
char name[MAX_CONN_NAME_LEN];
|
||||
struct sockaddr_in addr;
|
||||
uint32_t netif_index;
|
||||
int mark, fib, skipped;
|
||||
char device[64];
|
||||
};
|
||||
|
||||
struct stun_history {
|
||||
struct stun_history* next;
|
||||
struct sockaddr_in addr;
|
||||
};
|
||||
|
||||
struct stun_probe {
|
||||
struct stun_probe* next;
|
||||
struct STUN_NAT* owner;
|
||||
struct stun_source source;
|
||||
struct stun_nat_result result;
|
||||
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;
|
||||
uint8_t transaction[12];
|
||||
unsigned sent;
|
||||
uint64_t sent_at_tb;
|
||||
int seen, capability;
|
||||
enum stun_nat_filtering pending_filter;
|
||||
};
|
||||
|
||||
struct STUN_NAT {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
struct stun_probe* probes;
|
||||
void* scan_timer;
|
||||
uint64_t next_transaction_tb;
|
||||
};
|
||||
|
||||
struct stun_response {
|
||||
struct sockaddr_in mapped, other, origin;
|
||||
int have_mapped, have_other, have_origin, error;
|
||||
};
|
||||
|
||||
static void next_server(struct stun_probe* p);
|
||||
static void start_transaction(struct stun_probe* p, enum stun_stage stage, const struct sockaddr_in* target);
|
||||
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 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; }
|
||||
static void write32(uint8_t* p, uint32_t v) { p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v; }
|
||||
static int same_addr(const struct sockaddr_in* a, const struct sockaddr_in* b) {
|
||||
return a->sin_addr.s_addr == b->sin_addr.s_addr && a->sin_port == b->sin_port;
|
||||
}
|
||||
|
||||
const char* stun_nat_mapping_name(enum stun_nat_mapping v) {
|
||||
static const char* names[] = { "UNKNOWN", "MATCH", "DEPENDENT", "EIM", "ADM", "APDM" };
|
||||
return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "INVALID";
|
||||
}
|
||||
|
||||
const char* stun_nat_filtering_name(enum stun_nat_filtering v) {
|
||||
static const char* names[] = { "UNKNOWN", "EIF", "ADF", "APDF" };
|
||||
return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "INVALID";
|
||||
}
|
||||
|
||||
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" };
|
||||
return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "invalid reason";
|
||||
}
|
||||
|
||||
static const char* stage_name(enum stun_stage stage) {
|
||||
static const char* names[] = { "binding", "change-ip-port", "control-ip-port", "change-port",
|
||||
"control-port", "check-alternate-ip", "check-alternate-port", "mapping-ip", "mapping-port", "verify" };
|
||||
return names[stage];
|
||||
}
|
||||
|
||||
/* Завершает владение сетевыми ресурсами; сам результат остаётся доступен через API. */
|
||||
static void close_probe_io(struct stun_probe* p) {
|
||||
struct UASYNC* ua = p->owner->inst->ua;
|
||||
if (p->timer) { uasync_cancel_timeout(ua, p->timer); p->timer = NULL; }
|
||||
if (p->dns) { adns_cancel(p->dns); p->dns = NULL; }
|
||||
if (p->socket_handle) { uasync_remove_socket_t(ua, p->fd); p->socket_handle = NULL; }
|
||||
if (p->fd != SOCKET_INVALID) { socket_close_wrapper(p->fd); p->fd = SOCKET_INVALID; }
|
||||
while (p->history) { struct stun_history* next = p->history->next; u_free(p->history); p->history = next; }
|
||||
}
|
||||
|
||||
/* Публикует только локальный снимок диагностики и закрывает тестовый порт. */
|
||||
static void finish_probe(struct stun_probe* p) {
|
||||
struct stun_nat_result* r = &p->result;
|
||||
r->measured_at_tb = get_time_tb();
|
||||
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;
|
||||
}
|
||||
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,
|
||||
stun_nat_mapping_name(r->mapping), stun_nat_reason_name(r->mapping_reason),
|
||||
stun_nat_filtering_name(r->filtering), stun_nat_reason_name(r->filtering_reason), r->translated, r->rfc5780, r->status);
|
||||
close_probe_io(p);
|
||||
}
|
||||
|
||||
/* Сохраняет отправки только собственного STUN: повторный тест не должен открыть фильтр заранее. */
|
||||
static int remember_target(struct stun_probe* p) {
|
||||
for (struct stun_history* h = p->history; h; h = h->next) if (same_addr(&h->addr, &p->target)) return 0;
|
||||
struct stun_history* h = u_malloc(sizeof(*h));
|
||||
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot allocate destination history", p->source.name); return -1; }
|
||||
h->addr = p->target;
|
||||
h->next = p->history;
|
||||
p->history = h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Проверяет, что собственные предыдущие тесты не разрешили проверяемый источник. */
|
||||
static int filter_is_clean(const struct stun_probe* p, int port_only) {
|
||||
for (const struct stun_history* h = p->history; h; h = h->next) {
|
||||
if (!port_only && h->addr.sin_addr.s_addr == p->other.sin_addr.s_addr) return 0;
|
||||
if (port_only && h->addr.sin_addr.s_addr == p->primary.sin_addr.s_addr && h->addr.sin_port == p->other.sin_port) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* После таймаута CHANGE проверяет обычный Binding, прежде чем судить о фильтрации. */
|
||||
static void transaction_failed(struct stun_probe* p, enum stun_nat_reason reason) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s stage=%s failed: %s target=%s:%u attempts=%u",
|
||||
p->source.name, stage_name(p->stage), stun_nat_reason_name(reason),
|
||||
ip_to_str(&p->target.sin_addr, AF_INET).str, ntohs(p->target.sin_port), p->sent);
|
||||
switch (p->stage) {
|
||||
case STUN_BASE:
|
||||
p->result.mapping_reason = reason;
|
||||
next_server(p);
|
||||
break;
|
||||
case STUN_FILTER_BOTH:
|
||||
case STUN_FILTER_PORT:
|
||||
if (reason == STUN_REASON_TIMEOUT) {
|
||||
enum stun_stage next = p->stage == STUN_FILTER_BOTH ? STUN_CONTROL_BOTH : STUN_CONTROL_PORT;
|
||||
start_transaction(p, next, &p->primary);
|
||||
} else { p->result.filtering_reason = reason; begin_mapping(p); }
|
||||
break;
|
||||
case STUN_CONTROL_BOTH:
|
||||
case STUN_CONTROL_PORT:
|
||||
case STUN_CHECK_OTHER:
|
||||
case STUN_CHECK_PORT:
|
||||
p->result.filtering_reason = reason;
|
||||
begin_mapping(p);
|
||||
break;
|
||||
case STUN_MAP_IP:
|
||||
case STUN_MAP_PORT:
|
||||
p->result.mapping_reason = reason;
|
||||
finish_server(p);
|
||||
break;
|
||||
case STUN_VERIFY:
|
||||
p->result.mapping = STUN_MAP_UNKNOWN;
|
||||
p->result.mapping_reason = reason;
|
||||
p->result.filtering = STUN_FILTER_UNKNOWN;
|
||||
p->result.filtering_reason = STUN_REASON_UNSTABLE;
|
||||
finish_probe(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Отправляет/повторяет одну транзакцию с тем же случайным ID и экспоненциальным RTO. */
|
||||
static void send_request(void* arg) {
|
||||
struct stun_probe* p = arg;
|
||||
p->timer = NULL;
|
||||
if (p->sent == STUN_REQUESTS) { transaction_failed(p, STUN_REASON_TIMEOUT); return; }
|
||||
if (remember_target(p) < 0) {
|
||||
p->result.status = STUN_NAT_FAILED;
|
||||
p->result.mapping_reason = STUN_REASON_NOMEM;
|
||||
finish_probe(p);
|
||||
return;
|
||||
}
|
||||
uint8_t packet[36] = {0};
|
||||
unsigned flags = p->stage == STUN_FILTER_BOTH ? 6 : p->stage == STUN_FILTER_PORT ? 2 : 0;
|
||||
size_t fp_offset = flags ? 28 : 20;
|
||||
size_t len = fp_offset + 8;
|
||||
write16(packet, STUN_REQUEST);
|
||||
write16(packet + 2, (uint16_t)(len - 20));
|
||||
write32(packet + 4, STUN_COOKIE);
|
||||
memcpy(packet + 8, p->transaction, 12);
|
||||
if (flags) { write16(packet + 20, STUN_CHANGE); write16(packet + 22, 4); write32(packet + 24, flags); }
|
||||
write16(packet + fp_offset, STUN_FINGERPRINT);
|
||||
write16(packet + fp_offset + 2, 4);
|
||||
write32(packet + fp_offset + 4, crc32_calc(packet, fp_offset) ^ STUN_FINGERPRINT_XOR);
|
||||
ssize_t sent = socket_sendto(p->fd, packet, len, (const struct sockaddr*)&p->target, sizeof(p->target));
|
||||
if (sent != (ssize_t)len) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s send failed: sent=%zd expected=%zu error=%s",
|
||||
p->source.name, sent, len, socket_strerror(socket_get_error()));
|
||||
transaction_failed(p, STUN_REASON_IO);
|
||||
return;
|
||||
}
|
||||
p->sent_at_tb = get_time_tb();
|
||||
p->sent++;
|
||||
int delay_ms = p->sent == STUN_REQUESTS ? 8000 : 500 << (p->sent - 1);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN TX source=%s stage=%s target=%s:%u attempt=%u id=%08x%08x%08x RTO=%dms",
|
||||
p->source.name, stage_name(p->stage), ip_to_str(&p->target.sin_addr, AF_INET).str, ntohs(p->target.sin_port),
|
||||
p->sent, read32(p->transaction), read32(p->transaction + 4), read32(p->transaction + 8), delay_ms);
|
||||
p->timer = uasync_set_timeout(p->owner->inst->ua, delay_ms * 10, p, send_request, "stun_retry");
|
||||
if (!p->timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s retry timer allocation failed", p->source.name);
|
||||
p->result.status = STUN_NAT_FAILED;
|
||||
p->result.mapping_reason = STUN_REASON_NOMEM;
|
||||
finish_probe(p);
|
||||
}
|
||||
}
|
||||
|
||||
/* Новые транзакции всех проб ограничены десятью в секунду; event loop не блокируется. */
|
||||
static void start_transaction(struct stun_probe* p, enum stun_stage stage, const struct sockaddr_in* target) {
|
||||
p->stage = stage;
|
||||
p->target = *target;
|
||||
p->expected = *target;
|
||||
if (stage == STUN_FILTER_BOTH) p->expected = p->other;
|
||||
if (stage == STUN_FILTER_PORT) p->expected.sin_port = p->other.sin_port;
|
||||
p->sent = 0;
|
||||
if (random_bytes(p->transaction, sizeof(p->transaction)) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s transaction random generation failed", p->source.name);
|
||||
p->result.status = STUN_NAT_FAILED;
|
||||
p->result.mapping_reason = STUN_REASON_IO;
|
||||
finish_probe(p);
|
||||
return;
|
||||
}
|
||||
uint64_t now = get_time_tb();
|
||||
if (p->owner->next_transaction_tb < now) p->owner->next_transaction_tb = now;
|
||||
uint64_t delay = p->owner->next_transaction_tb - now;
|
||||
p->owner->next_transaction_tb += 1000;
|
||||
if (delay > INT_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s transaction pacing delay overflow", p->source.name);
|
||||
p->result.status = STUN_NAT_FAILED;
|
||||
p->result.mapping_reason = STUN_REASON_IO;
|
||||
finish_probe(p);
|
||||
return;
|
||||
}
|
||||
p->timer = uasync_set_timeout(p->owner->inst->ua, (int)delay, p, send_request, "stun_start");
|
||||
if (!p->timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s start timer allocation failed", p->source.name);
|
||||
p->result.status = STUN_NAT_FAILED;
|
||||
p->result.mapping_reason = STUN_REASON_NOMEM;
|
||||
finish_probe(p);
|
||||
}
|
||||
}
|
||||
|
||||
/* После filtering проверяет изменение IP, затем изменение порта альтернативного IP. */
|
||||
static void begin_mapping(struct stun_probe* p) {
|
||||
if (p->capability && p->result.mapping < STUN_MAP_EIM) {
|
||||
struct sockaddr_in target = p->other;
|
||||
target.sin_port = p->primary.sin_port;
|
||||
start_transaction(p, STUN_MAP_IP, &target);
|
||||
} else finish_server(p);
|
||||
}
|
||||
|
||||
/* Выбирает второй независимый сервер или повторно проверяет первоначальную трансляцию. */
|
||||
static void finish_server(struct stun_probe* p) {
|
||||
if (p->result.server_count < 2) next_server(p);
|
||||
else start_transaction(p, STUN_VERIFY, &p->reference);
|
||||
}
|
||||
|
||||
/* Начинает обычный Binding выбранного независимого сервера. */
|
||||
static void use_server(struct stun_probe* p, const struct sockaddr_in* addr) {
|
||||
p->primary = *addr;
|
||||
p->capability = 0;
|
||||
start_transaction(p, STUN_BASE, &p->primary);
|
||||
}
|
||||
|
||||
/* DNS-ответ принадлежит только живой пробе; destroy отменяет запрос без callback. */
|
||||
static void dns_done(const struct adns_result* res, void* arg) {
|
||||
struct stun_probe* p = arg;
|
||||
p->dns = NULL;
|
||||
if (res->status != ADNS_OK || !res->count) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s DNS failed status=%d", p->source.name, res->status);
|
||||
p->result.mapping_reason = STUN_REASON_DNS;
|
||||
next_server(p);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < res->count; i++) {
|
||||
struct sockaddr_in addr = res->addrs[i];
|
||||
addr.sin_port = htons(p->dns_port);
|
||||
uint32_t ip = ntohl(addr.sin_addr.s_addr);
|
||||
if (ip && ip < 0xe0000000u && (!p->result.server_count || addr.sin_addr.s_addr != p->reference.sin_addr.s_addr)) {
|
||||
use_server(p, &addr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s DNS supplied no independent server IP", p->source.name);
|
||||
next_server(p);
|
||||
}
|
||||
|
||||
/* Обходит список конфига: недоступные серверы заменяются резервными. */
|
||||
static void next_server(struct stun_probe* p) {
|
||||
while (p->cursor) {
|
||||
const struct CFG_STUN_SERVER* server = p->cursor;
|
||||
p->cursor = server->next;
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(server->port);
|
||||
if (inet_pton(AF_INET, server->host, &addr.sin_addr) == 1) {
|
||||
uint32_t ip = ntohl(addr.sin_addr.s_addr);
|
||||
if (!ip || ip >= 0xe0000000u || (p->result.server_count && addr.sin_addr.s_addr == p->reference.sin_addr.s_addr)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s skipping invalid/duplicate server IP=%s", p->source.name, server->host);
|
||||
continue;
|
||||
}
|
||||
use_server(p, &addr);
|
||||
return;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN source=%s resolving %s:%u", p->source.name, server->host, server->port);
|
||||
p->dns_port = server->port;
|
||||
p->dns = adns_resolve(p->owner->inst->ua, server->host, NULL, dns_done, p);
|
||||
if (p->dns) return;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot start DNS for %s", p->source.name, server->host);
|
||||
p->result.mapping_reason = STUN_REASON_DNS;
|
||||
}
|
||||
if (p->result.server_count) {
|
||||
if (p->result.mapping == STUN_MAP_UNKNOWN) p->result.mapping_reason = STUN_REASON_NO_PAIR;
|
||||
start_transaction(p, STUN_VERIFY, &p->reference);
|
||||
} else finish_probe(p);
|
||||
}
|
||||
|
||||
/* Декодирует адрес без unaligned-доступов; IPv6 не смешивается с IPv4 измерением. */
|
||||
static int parse_addr(const uint8_t* data, size_t len, int xored, struct sockaddr_in* out) {
|
||||
if (len != 8 || data[0] || data[1] != 1) return -1;
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->sin_family = AF_INET;
|
||||
out->sin_port = htons(read16(data + 2) ^ (xored ? STUN_COOKIE >> 16 : 0));
|
||||
out->sin_addr.s_addr = htonl(read32(data + 4) ^ (xored ? STUN_COOKIE : 0));
|
||||
uint32_t ip = ntohl(out->sin_addr.s_addr);
|
||||
return !out->sin_port || !ip || ip >= 0xe0000000u ? -1 : 0;
|
||||
}
|
||||
|
||||
/* Проверяет TLV, выравнивание, fingerprint и необходимые поля ответа STUN. */
|
||||
static int parse_response(const uint8_t* data, size_t len, struct stun_response* r) {
|
||||
if (len < 20 || read32(data + 4) != STUN_COOKIE || (read16(data + 2) & 3) || len != 20u + read16(data + 2)) return -1;
|
||||
uint16_t type = read16(data);
|
||||
if (type != STUN_SUCCESS && type != STUN_ERROR) return -1;
|
||||
memset(r, 0, sizeof(*r));
|
||||
for (size_t pos = 20; pos < len;) {
|
||||
if (len - pos < 4) return -1;
|
||||
uint16_t attr = read16(data + pos), size = read16(data + pos + 2);
|
||||
size_t padded = ((size_t)size + 3) & ~(size_t)3;
|
||||
if (padded > len - pos - 4) return -1;
|
||||
const uint8_t* value = data + pos + 4;
|
||||
if (attr == STUN_XOR_MAPPED && !r->have_mapped) {
|
||||
if (parse_addr(value, size, 1, &r->mapped) < 0) return -1;
|
||||
r->have_mapped = 1;
|
||||
} else if (attr == STUN_OTHER && !r->have_other) {
|
||||
if (parse_addr(value, size, 0, &r->other) < 0) return -1;
|
||||
r->have_other = 1;
|
||||
} else if (attr == STUN_ORIGIN && !r->have_origin) {
|
||||
if (parse_addr(value, size, 0, &r->origin) < 0) return -1;
|
||||
r->have_origin = 1;
|
||||
} else if (attr == STUN_ERROR_CODE) {
|
||||
if (size < 4 || value[0] || value[1] || value[2] < 3 || value[2] > 6 || value[3] > 99) return -1;
|
||||
r->error = value[2] * 100 + value[3];
|
||||
} else if (attr == STUN_FINGERPRINT) {
|
||||
if (size != 4 || pos + 8 != len || read32(value) != (crc32_calc(data, pos) ^ STUN_FINGERPRINT_XOR)) return -1;
|
||||
} else if (attr < 0x8000 && attr != STUN_MAPPED && attr != 0x000a) return -1;
|
||||
pos += 4 + padded;
|
||||
}
|
||||
if (type == STUN_ERROR) return r->error ? 0 : -1;
|
||||
return r->have_mapped && !r->error ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Завершает очередной этап, сохраняя mapping и filtering как независимые свойства. */
|
||||
static void handle_response(struct stun_probe* p, const struct stun_response* r) {
|
||||
struct stun_nat_result* result = &p->result;
|
||||
if (r->error) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s stage=%s server error=%d", p->source.name, stage_name(p->stage), r->error);
|
||||
transaction_failed(p, r->error == 420 ? STUN_REASON_UNSUPPORTED : STUN_REASON_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
switch (p->stage) {
|
||||
case STUN_BASE: {
|
||||
unsigned idx = result->server_count++;
|
||||
memcpy(&result->servers[idx], &p->primary, sizeof(p->primary));
|
||||
memcpy(&result->mapped[idx], &r->mapped, sizeof(r->mapped));
|
||||
p->base_mapping = r->mapped;
|
||||
if (idx == 0) p->reference = p->primary;
|
||||
if (!same_addr((const struct sockaddr_in*)&result->local, &r->mapped)) result->translated = 1;
|
||||
else if (result->translated < 0) result->translated = 0;
|
||||
p->capability = r->have_other && r->have_origin && r->other.sin_addr.s_addr != p->primary.sin_addr.s_addr &&
|
||||
r->other.sin_port != p->primary.sin_port;
|
||||
if (p->capability) { p->other = r->other; result->rfc5780 = 1; }
|
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN source=%s server lacks usable RFC5780 OTHER-ADDRESS/RESPONSE-ORIGIN", p->source.name);
|
||||
if (p->capability && result->filtering == STUN_FILTER_UNKNOWN && filter_is_clean(p, 0))
|
||||
start_transaction(p, STUN_FILTER_BOTH, &p->primary);
|
||||
else {
|
||||
if (p->capability && result->filtering == STUN_FILTER_UNKNOWN) result->filtering_reason = STUN_REASON_TRAFFIC;
|
||||
begin_mapping(p);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case STUN_FILTER_BOTH:
|
||||
case STUN_FILTER_PORT:
|
||||
if (!same_addr(&p->base_mapping, &r->mapped)) {
|
||||
result->filtering_reason = STUN_REASON_UNSTABLE;
|
||||
} else {
|
||||
if (p->stage == STUN_FILTER_PORT) {
|
||||
p->pending_filter = STUN_FILTER_ADF;
|
||||
start_transaction(p, STUN_CHECK_OTHER, &p->other);
|
||||
break;
|
||||
}
|
||||
result->filtering = STUN_FILTER_EIF;
|
||||
result->filtering_reason = STUN_REASON_NONE;
|
||||
}
|
||||
begin_mapping(p);
|
||||
break;
|
||||
case STUN_CONTROL_BOTH:
|
||||
case STUN_CONTROL_PORT:
|
||||
if (!same_addr(&p->base_mapping, &r->mapped) || !r->have_other || !same_addr(&p->other, &r->other)) {
|
||||
result->filtering_reason = STUN_REASON_UNSTABLE;
|
||||
begin_mapping(p);
|
||||
} else if (p->stage == STUN_CONTROL_BOTH) {
|
||||
if (filter_is_clean(p, 1)) start_transaction(p, STUN_FILTER_PORT, &p->primary);
|
||||
else { result->filtering_reason = STUN_REASON_TRAFFIC; begin_mapping(p); }
|
||||
} else {
|
||||
p->pending_filter = STUN_FILTER_APDF;
|
||||
start_transaction(p, STUN_CHECK_OTHER, &p->other);
|
||||
}
|
||||
break;
|
||||
case STUN_CHECK_OTHER:
|
||||
if (p->pending_filter == STUN_FILTER_APDF) {
|
||||
struct sockaddr_in target = p->primary;
|
||||
target.sin_port = p->other.sin_port;
|
||||
start_transaction(p, STUN_CHECK_PORT, &target);
|
||||
break;
|
||||
}
|
||||
result->filtering = p->pending_filter;
|
||||
result->filtering_reason = STUN_REASON_NONE;
|
||||
begin_mapping(p);
|
||||
break;
|
||||
case STUN_CHECK_PORT:
|
||||
result->filtering = p->pending_filter;
|
||||
result->filtering_reason = STUN_REASON_NONE;
|
||||
begin_mapping(p);
|
||||
break;
|
||||
case STUN_MAP_IP:
|
||||
p->ip_mapping = r->mapped;
|
||||
if (same_addr(&p->base_mapping, &r->mapped)) {
|
||||
result->mapping = STUN_MAP_EIM;
|
||||
result->mapping_reason = STUN_REASON_NONE;
|
||||
finish_server(p);
|
||||
} else start_transaction(p, STUN_MAP_PORT, &p->other);
|
||||
break;
|
||||
case STUN_MAP_PORT:
|
||||
result->mapping = same_addr(&p->ip_mapping, &r->mapped) ? STUN_MAP_ADM : STUN_MAP_APDM;
|
||||
result->mapping_reason = STUN_REASON_NONE;
|
||||
finish_server(p);
|
||||
break;
|
||||
case STUN_VERIFY:
|
||||
if (!same_addr((const struct sockaddr_in*)&result->mapped[0], &r->mapped)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s first server mapping changed during measurement", p->source.name);
|
||||
result->mapping = STUN_MAP_UNKNOWN;
|
||||
result->filtering = STUN_FILTER_UNKNOWN;
|
||||
result->mapping_reason = result->filtering_reason = STUN_REASON_UNSTABLE;
|
||||
} else if (result->server_count == 2) {
|
||||
int match = same_addr((const struct sockaddr_in*)&result->mapped[0], (const struct sockaddr_in*)&result->mapped[1]);
|
||||
if (result->mapping < STUN_MAP_EIM) {
|
||||
result->mapping = match ? STUN_MAP_MATCH : STUN_MAP_DEPENDENT;
|
||||
result->mapping_reason = STUN_REASON_NONE;
|
||||
} else if (result->mapping == STUN_MAP_EIM && !match) {
|
||||
result->mapping = STUN_MAP_DEPENDENT;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s second server contradicts EIM; retaining observed dependent mapping",
|
||||
p->source.name);
|
||||
}
|
||||
}
|
||||
finish_probe(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Единственный читатель собственного fd: проверяет источник, ID и формат до изменения состояния. */
|
||||
static void read_response(socket_t fd, void* arg) {
|
||||
struct stun_probe* p = arg;
|
||||
uint8_t packet[2048];
|
||||
struct sockaddr_storage from = {0};
|
||||
socklen_t from_len = sizeof(from);
|
||||
ssize_t len = socket_recvfrom(fd, packet, sizeof(packet), (struct sockaddr*)&from, &from_len);
|
||||
if (len < 0) {
|
||||
int err = socket_get_error();
|
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK || err == ERR_INTR) return;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s receive failed: %s", p->source.name, socket_strerror(err));
|
||||
if (p->timer) { uasync_cancel_timeout(p->owner->inst->ua, p->timer); p->timer = NULL; }
|
||||
transaction_failed(p, STUN_REASON_IO);
|
||||
return;
|
||||
}
|
||||
if (len < 20 || from.ss_family != AF_INET || memcmp(packet + 8, p->transaction, 12) || !p->sent) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN source=%s ignoring unrelated/late datagram from=%s len=%zd", p->source.name,
|
||||
sockaddr_storage_to_str(&from).str, len);
|
||||
return;
|
||||
}
|
||||
const struct sockaddr_in* src = (const struct sockaddr_in*)&from;
|
||||
int changed = p->stage == STUN_FILTER_BOTH || p->stage == STUN_FILTER_PORT;
|
||||
if (!same_addr(src, &p->expected) && !(changed && same_addr(src, &p->primary))) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s matching ID from unexpected source=%s", p->source.name, sockaddr_storage_to_str(&from).str);
|
||||
return;
|
||||
}
|
||||
struct stun_response response;
|
||||
if (parse_response(packet, (size_t)len, &response) < 0 || (response.have_origin && !same_addr(src, &response.origin))) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s malformed response stage=%s from=%s len=%zd", p->source.name,
|
||||
stage_name(p->stage), sockaddr_storage_to_str(&from).str, len);
|
||||
return;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN RX source=%s stage=%s from=%s mapped=%s:%u RTT=%llums error=%d",
|
||||
p->source.name, stage_name(p->stage), sockaddr_storage_to_str(&from).str,
|
||||
ip_to_str(&response.mapped.sin_addr, AF_INET).str, ntohs(response.mapped.sin_port),
|
||||
(unsigned long long)((get_time_tb() - p->sent_at_tb) / 10), response.error);
|
||||
if (p->timer) { uasync_cancel_timeout(p->owner->inst->ua, p->timer); p->timer = NULL; }
|
||||
if (changed && !response.error && !same_addr(src, &p->expected)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s server ignored CHANGE-REQUEST", p->source.name);
|
||||
transaction_failed(p, STUN_REASON_UNSUPPORTED);
|
||||
} else handle_response(p, &response);
|
||||
}
|
||||
|
||||
/* Читает фактический путь исходного UDP без изменения его состояния. */
|
||||
static int source_snapshot(const struct ETCP_SOCKET* sock, struct stun_source* source) {
|
||||
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;
|
||||
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) source->skipped = 1;
|
||||
else { source->addr = *(const struct sockaddr_in*)addr; source->addr.sin_port = 0; }
|
||||
if (source->skipped) return 0;
|
||||
#ifdef __linux__
|
||||
socklen_t size = sizeof(source->mark);
|
||||
if (getsockopt(sock->fd, SOL_SOCKET, SO_MARK, &source->mark, &size) < 0) goto error;
|
||||
size = sizeof(source->device);
|
||||
if (getsockopt(sock->fd, SOL_SOCKET, SO_BINDTODEVICE, source->device, &size) < 0) goto error;
|
||||
#endif
|
||||
#ifdef __FreeBSD__
|
||||
socklen_t size = sizeof(source->fib);
|
||||
if (getsockopt(sock->fd, SOL_SOCKET, SO_SETFIB, &source->fib, &size) < 0) goto error;
|
||||
#endif
|
||||
return 0;
|
||||
#if defined(__linux__) || defined(__FreeBSD__)
|
||||
error:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot read routing options: %s", sock->name, socket_strerror(socket_get_error()));
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Создаёт независимый сокет с теми же IP/device/mark/FIB и случайным свободным портом. */
|
||||
static int open_probe_socket(struct stun_probe* p) {
|
||||
p->fd = socket_create_udp(AF_INET);
|
||||
if (p->fd == SOCKET_INVALID) goto error;
|
||||
if (socket_set_nonblocking(p->fd) < 0 || socket_set_reuseaddr(p->fd, 0) < 0) goto error;
|
||||
if (p->source.mark && socket_set_mark(p->fd, p->source.mark) < 0) goto error;
|
||||
if (p->source.device[0] && socket_bind_to_device(p->fd, p->source.device) < 0) goto error;
|
||||
#ifdef __FreeBSD__
|
||||
if (setsockopt(p->fd, SOL_SOCKET, SO_SETFIB, &p->source.fib, sizeof(p->source.fib)) < 0) goto error;
|
||||
#endif
|
||||
struct sockaddr_in local = p->source.addr;
|
||||
int bound = 0;
|
||||
for (int i = 0; i < 16; 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; }
|
||||
#ifdef _WIN32
|
||||
if (socket_get_error() != WSAEADDRINUSE) goto error;
|
||||
#else
|
||||
if (socket_get_error() != EADDRINUSE) goto error;
|
||||
#endif
|
||||
}
|
||||
if (!bound) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s no free random test port after 16 attempts", p->source.name);
|
||||
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; }
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN start source=%s test=%s netif=%u device=%s mark=%d fib=%d fd=%d",
|
||||
p->source.name, sockaddr_storage_to_str(&p->result.local).str, p->source.netif_index,
|
||||
p->source.device, p->source.mark, p->source.fib, (int)p->fd);
|
||||
return 0;
|
||||
error:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot create socket IP=%s device=%s mark=%d fib=%d: %s",
|
||||
p->source.name, ip_to_str(&p->source.addr.sin_addr, AF_INET).str, p->source.device,
|
||||
p->source.mark, p->source.fib, socket_strerror(socket_get_error()));
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Освобождает пробу; никогда не закрывает исходный ETCP fd. */
|
||||
static void destroy_probe(struct stun_probe* p) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN cleanup source=%s status=%d test_fd=%d DNS=%d timer=%d",
|
||||
p->source.name, p->result.status, (int)p->fd, p->dns != NULL, p->timer != NULL);
|
||||
close_probe_io(p);
|
||||
u_free(p);
|
||||
}
|
||||
|
||||
/* Начинает измерение на новом порту, исключая непосредственно предыдущий тестовый порт. */
|
||||
static void restart_probe(struct stun_probe* p) {
|
||||
close_probe_io(p);
|
||||
memset(&p->result, 0, sizeof(p->result));
|
||||
p->result.translated = -1;
|
||||
p->result.mapping_reason = STUN_REASON_NO_PAIR;
|
||||
p->result.filtering_reason = STUN_REASON_UNSUPPORTED;
|
||||
p->cursor = p->owner->inst->config->global.stun_servers;
|
||||
p->capability = 0;
|
||||
p->pending_filter = STUN_FILTER_UNKNOWN;
|
||||
p->sent = 0;
|
||||
if (p->source.skipped) {
|
||||
p->result.status = STUN_NAT_SKIPPED;
|
||||
p->result.mapping_reason = p->result.filtering_reason = STUN_REASON_SOCKET;
|
||||
finish_probe(p);
|
||||
} else if (open_probe_socket(p) < 0) {
|
||||
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 ETCP_SOCKET* sock = mod->inst->etcp_sockets; sock; sock = sock->next) {
|
||||
if (sock->is_tcp) 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 = 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->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; }
|
||||
struct stun_probe* p = *slot;
|
||||
*slot = p->next;
|
||||
destroy_probe(p);
|
||||
}
|
||||
}
|
||||
|
||||
/* Редкая локальная сверка не отправляет повторных запросов на неизменившихся интерфейсах. */
|
||||
static void scan_timer(void* arg) {
|
||||
struct STUN_NAT* mod = arg;
|
||||
mod->scan_timer = NULL;
|
||||
scan_sources(mod);
|
||||
mod->scan_timer = uasync_set_timeout(mod->inst->ua, STUN_SCAN_TB, mod, scan_timer, "stun_scan");
|
||||
if (!mod->scan_timer) DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN interface scan timer allocation failed");
|
||||
}
|
||||
|
||||
int stun_nat_init(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst || !inst->ua || !inst->config) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN init requires instance, uasync and config"); return -1; }
|
||||
if (inst->stun_nat) return 0;
|
||||
if (!inst->config->global.stun_enabled) return 0;
|
||||
if (inst->config->global.stun_server_count < 2) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN requires at least two configured servers"); return -1; }
|
||||
struct STUN_NAT* mod = u_calloc(1, sizeof(*mod));
|
||||
if (!mod) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN module allocation failed"); return -1; }
|
||||
mod->inst = inst;
|
||||
inst->stun_nat = mod;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN initialized with %d servers; independent test sockets", inst->config->global.stun_server_count);
|
||||
scan_timer(mod);
|
||||
if (!mod->scan_timer) { stun_nat_destroy(inst); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
void stun_nat_destroy(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst || !inst->stun_nat) return;
|
||||
struct STUN_NAT* mod = inst->stun_nat;
|
||||
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; }
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN destroyed; test sockets/DNS/timers released");
|
||||
u_free(mod);
|
||||
}
|
||||
|
||||
void stun_nat_recheck(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst || !inst->stun_nat) return;
|
||||
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);
|
||||
}
|
||||
|
||||
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 stun_probe* p = inst->stun_nat->probes; p; p = p->next) {
|
||||
if (!strcmp(p->source.name, socket_name)) { *result = p->result; return 0; }
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* Независимая STUN-диагностика интерфейсов существующих IPv4 UDP-сокетов.
|
||||
* [stun] enabled=yes, повторяющийся server=host[:port]; нужны два разных IP.
|
||||
* Обычный STUN сравнивает mapping; RFC 5780 дополнительно проверяет mapping/filtering.
|
||||
* MATCH означает совпадение для двух серверов, а не доказанный EIM.
|
||||
* Результат локальный: nat_detection, ETCP и NODEINFO не изменяются.
|
||||
* Модуль создаёт собственный UDP-сокет на том же адресе/интерфейсе, с теми же mark/FIB,
|
||||
* самостоятельно принимает STUN и закрывает fd. Результат относится к тестовому порту.
|
||||
* Периодическая сверка замечает добавление/удаление/изменение исходных сокетов.
|
||||
* Все вызовы выполняются в uasync-потоке; destroy отменяет DNS и таймеры до освобождения config. */
|
||||
#ifndef STUN_NAT_H
|
||||
#define STUN_NAT_H
|
||||
|
||||
#include "../lib/socket_compat.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
struct STUN_NAT;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
struct stun_nat_result {
|
||||
enum stun_nat_status status;
|
||||
enum stun_nat_mapping mapping;
|
||||
enum stun_nat_filtering filtering;
|
||||
enum stun_nat_reason mapping_reason, filtering_reason;
|
||||
struct sockaddr_storage local; // адрес независимого тестового UDP-сокета
|
||||
struct sockaddr_storage servers[2], mapped[2]; // реальные серверы и наблюдаемые внешние адреса
|
||||
unsigned server_count;
|
||||
int translated; // -1 = неизвестно, 0 = нет трансляции, 1 = есть
|
||||
int rfc5780; // сервер объявил корректный OTHER-ADDRESS
|
||||
uint64_t measured_at_tb; // локальное монотонное время, 0.1мс
|
||||
};
|
||||
|
||||
/* init обходится без сетевых действий при выключенном [stun]; ошибка <0 с логом.
|
||||
* destroy отменяет DNS/таймеры и закрывает только собственные UDP-сокеты. */
|
||||
int stun_nat_init(struct UTUN_INSTANCE* inst);
|
||||
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);
|
||||
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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "topo_group.h"
|
||||
#include "topo_group_invite.h"
|
||||
#include "nat_detection.h"
|
||||
#include "transport_layer/stun_nat.h"
|
||||
#include "etcp_connections.h"
|
||||
#include "etcp.h"
|
||||
#include "conn_mgr.h"
|
||||
@@ -404,6 +405,7 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
|
||||
if (instance->ua) uasync_print_resources(instance->ua, "INSTANCE_DESTROY_BEFORE");
|
||||
|
||||
instance->running = 0;
|
||||
stun_nat_destroy(instance);
|
||||
etcp_padding_stop(&instance->padding);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] A done — diagnose complete");
|
||||
|
||||
@@ -702,6 +704,7 @@ int utun_core_start(struct UTUN_INSTANCE *instance) {
|
||||
radio_init_instance(instance);
|
||||
|
||||
instance->core_started = 1;
|
||||
if (stun_nat_init(instance) < 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN diagnostics could not start; core remains operational");
|
||||
DEBUG_INFO(DEBUG_CATEGORY_SYS, "core started");
|
||||
return 0;
|
||||
}
|
||||
@@ -1076,8 +1079,10 @@ struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struc
|
||||
fw_load_rules(&instance->fw, &new_config->global);
|
||||
// routing_destroy(instance); // commented per request - BGP self-restarts on conn events
|
||||
// routing_create(instance); // commented per request
|
||||
stun_nat_destroy(instance);
|
||||
if (instance->config) free_config(instance->config);
|
||||
instance->config = new_config;
|
||||
if (instance->core_started && stun_nat_init(instance) < 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN restart failed after config reload");
|
||||
if (client_result < 0) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reload completed with client errors; previous client handles retained");
|
||||
else DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Selective partial reload completed (only changed updated, unchanged untouched, [debug] applied)");
|
||||
return instance;
|
||||
|
||||
@@ -69,6 +69,7 @@ struct CONN_MGR;
|
||||
struct ETCP_CONNECT;
|
||||
struct DB_SYNC;
|
||||
struct NAT_DETECTION;
|
||||
struct STUN_NAT;
|
||||
struct merkle_sync;
|
||||
|
||||
struct NETWORK_ENTRY {
|
||||
@@ -139,6 +140,7 @@ struct UTUN_INSTANCE {
|
||||
struct TOPO_GROUPS* topo_groups; // Groups module for topology exchange
|
||||
sqlite3* topo_sqlite_db; // Shared SQLite DB (nodes/channels/peers)
|
||||
struct NAT_DETECTION* nat_det; // NAT detection module
|
||||
struct STUN_NAT* stun_nat; // независимая локальная STUN-диагностика UDP
|
||||
struct merkle_sync* msync; // Merkle tree sync module (per-instance)
|
||||
|
||||
// Identification
|
||||
|
||||
Reference in new issue
Block a user