Browse Source

Add independent STUN NAT probes with RFC 5780 discovery

master
evgeny 1 day ago
parent
commit
2c5deafe56
  1. 84
      doc/stun_nat.md
  2. 4
      src/Makefile.am
  3. 65
      src/config_parser.c
  4. 11
      src/config_parser.h
  5. 1
      src/config_parser_doc.md
  6. 738
      src/transport_layer/stun_nat.c
  7. 60
      src/transport_layer/stun_nat.h
  8. 5
      src/utun_instance.c
  9. 2
      src/utun_instance.h
  10. 6
      tests/Makefile.am
  11. 341
      tests/test_stun_nat.c

84
doc/stun_nat.md

@ -0,0 +1,84 @@
# Независимая STUN-диагностика NAT
Модуль `src/transport_layer/stun_nat.c/h` измеряет NAT на сетевых интерфейсах существующих IPv4 UDP-сокетов.
Для каждого исходного сокета создаётся собственный неблокирующий UDP-сокет с тем же локальным IP,
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
Исходный ETCP fd, его порт, обработчики, NAT-поля, NODEINFO и существующий `nat_detection` не изменяются.
Результат описывает тестовый порт; поведение NAT на порту ETCP может отличаться.
## Конфигурация
```ini
[stun]
enabled=yes
server=stun-a.example.org:3478
server=stun-b.example.org:3478
# Дополнительные строки — резервные серверы.
[debug]
nat=debug
```
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
## Результат
`stun_nat_get_result(instance, "имя исходного сокета", &result)` копирует локальный снимок:
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
- `mapping`: UNKNOWN, MATCH, DEPENDENT, EIM, ADM, APDM;
- `filtering`: UNKNOWN, EIF, ADF, APDF;
- причины неполного результата, статус, время измерения и обнаруженная поддержка RFC 5780.
Обычный STUN даёт MATCH при одинаковых адресах и DEPENDENT при разных. MATCH не доказывает EIM,
а обычные Binding-ответы не позволяют определить filtering. Первоначальный сервер опрашивается повторно:
смена его mapping во время проверки инвалидирует классификацию как нестабильную.
## RFC 5780
Корректные OTHER-ADDRESS и RESPONSE-ORIGIN включают автоматическую попытку Behavior Discovery.
Нужны две публичные IP и два порта на серверной стороне, доступные во всех четырёх комбинациях.
1. Обычный Binding получает внешний адрес и альтернативный endpoint.
2. CHANGE-REQUEST с флагами IP+port проверяет EIF.
3. Если ответ не пришёл, контрольный Binding проверяет живость сервера и стабильность mapping.
4. CHANGE-REQUEST только port различает ADF и APDF; после таймаута снова выполняется контрольный Binding.
5. Перед публикацией ADF/APDF альтернативные endpoints проверяются обычными Binding-запросами.
Недоступный альтернативный endpoint даёт UNKNOWN, а не ложную строгую фильтрацию.
6. После filtering проверяется mapping: альтернативный IP с первоначальным портом, затем при необходимости
альтернативный IP с альтернативным портом. Сравнение различает EIM, ADM и APDM.
7. Второй сервер и повтор первого проверяют независимую пару и стабильность результата.
Все filtering-тесты выполняются до открывающих фильтр отправок на альтернативные endpoints.
Собственные предыдущие STUN-отправки учитываются: при пересечении адресов следующего сервера
сомнительный тест filtering пропускается. ERROR 420 и игнорирование CHANGE-REQUEST дают UNKNOWN.
Аутентификация STUN не реализована: требующий её сервер фиксируется как ошибка, затем используется резервный.
Запрос содержит случайный 96-битный transaction ID и FINGERPRINT. Проверяются источник ответа,
transaction ID, границы/выравнивание атрибутов, XOR-MAPPED-ADDRESS, RESPONSE-ORIGIN и FINGERPRINT при его наличии.
Повторы одной транзакции сохраняют ID: RTO от 500 мс удваивается, всего семь отправок, последнее ожидание 8 секунд.
Новые транзакции всех проб ограничены десятью в секунду; ожидания не блокируют event loop.
## Жизненный цикл и проверки
Ядро запускает модуль при `utun_core_start`; остановка уничтожает его до сетевых ресурсов и config.
Перезагрузка конфига отменяет старые DNS/таймеры и создаёт новый модуль.
Раз в три секунды выполняется локальная сверка исходных сокетов: новые/изменённые проверяются,
исчезнувшие удаляются. Неизменившиеся сокеты повторно не опрашиваются.
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
Публичные операции выполняются в uasync-потоке экземпляра.
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
DEBUG содержит этапы, transaction ID, повторы, RTT, DNS и очистку; WARN/ERROR описывают аномалии.
Linux-тест `test_stun_nat` использует реальные локальные UDP-сокеты и ускоренные часы:
обычный STUN, все классы RFC 5780, недоступные альтернативы, потеря запроса, неверные/поздние ответы,
резервные серверы, DNS, нестабильный mapping, отмена и повторная проверка, изменение состава сокетов.
Каждый сценарий проверяет неизменность исходного ETCP-сокета и освобождение таймеров.
Ссылки: [RFC 5780](https://www.rfc-editor.org/rfc/rfc5780), [STUN RFC 8489](https://www.rfc-editor.org/rfc/rfc8489).

4
src/Makefile.am

@ -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 \

65
src/config_parser.c

@ -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;
}
}

11
src/config_parser.h

@ -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]; // пароль для входа в группу

1
src/config_parser_doc.md

@ -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]` | игнорируется | (зарезервировано) |
### Функции

738
src/transport_layer/stun_nat.c

@ -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;
}

60
src/transport_layer/stun_nat.h

@ -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

5
src/utun_instance.c

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

2
src/utun_instance.h

@ -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

6
tests/Makefile.am

@ -83,6 +83,7 @@ check_PROGRAMS = \
if OS_LINUX
check_PROGRAMS += \
test_stun_nat \
test_reality_fallback \
test_etcp_padding \
test_ll_queue_regressions \
@ -414,6 +415,11 @@ test_nat_detection_SOURCES = test_nat_detection.c
test_nat_detection_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_nat_detection_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_stun_nat_SOURCES = test_stun_nat.c $(test_clock_sources)
test_stun_nat_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_stun_nat_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_stun_nat_LDFLAGS = $(test_clock_ldflags)
test_nat_engine_SOURCES = test_nat_engine.c
test_nat_engine_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_nat_engine_LDADD = $(LIBUTUN) $(COMMON_LIBS)

341
tests/test_stun_nat.c

@ -0,0 +1,341 @@
/* Реальные UDP-сокеты и локальный STUN-стенд: транзакции, RFC5780, потери и независимость от ETCP. */
#include "../src/transport_layer/stun_nat.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../src/transport_layer/crc32.h"
#include "../lib/debug_config.h"
#include "test_clock.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define COOKIE 0x2112a442u
struct fixture;
struct endpoint {
struct fixture* fixture;
socket_t fd;
struct sockaddr_in addr;
unsigned index, requests;
};
struct fixture {
struct UASYNC* ua;
struct UTUN_INSTANCE inst;
struct ETCP_SOCKET source, original;
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;
uint8_t first_id[12], late[80];
size_t late_len;
struct sockaddr_in test_client;
unsigned bad_sent, retries, primary_bindings;
};
static uint16_t get16(const uint8_t* p) { return ((uint16_t)p[0] << 8) | p[1]; }
static uint32_t get32(const uint8_t* p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; }
static void put16(uint8_t* p, uint16_t v) { p[0] = v >> 8; p[1] = v; }
static void put32(uint8_t* p, uint32_t v) { p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v; }
/* Независимое кодирование атрибутов тестового сервера. */
static size_t add_address(uint8_t* out, size_t pos, uint16_t type, const struct sockaddr_in* addr) {
put16(out + pos, type);
put16(out + pos + 2, 8);
out[pos + 4] = 0;
out[pos + 5] = 1;
put16(out + pos + 6, ntohs(addr->sin_port) ^ (type == 0x0020 ? COOKIE >> 16 : 0));
put32(out + pos + 8, ntohl(addr->sin_addr.s_addr) ^ (type == 0x0020 ? COOKIE : 0));
return pos + 12;
}
static size_t add_fingerprint(uint8_t* out, size_t pos) {
put16(out + 2, (uint16_t)(pos + 8 - 20));
put16(out + pos, 0x8028);
put16(out + pos + 2, 4);
put32(out + pos + 4, crc32_calc(out, pos) ^ 0x5354554eu);
return pos + 8;
}
static void send_reply(struct endpoint* e, const uint8_t* data, size_t len, const struct sockaddr_in* client) {
assert(socket_sendto(e->fd, data, len, (const struct sockaddr*)client, sizeof(*client)) == (ssize_t)len);
}
/* Эмулирует mapping по адресу назначения, filtering — выборочным пропуском CHANGE-ответов. */
static void server_read(socket_t fd, void* arg) {
struct endpoint* e = arg;
struct fixture* f = e->fixture;
uint8_t request[128];
struct sockaddr_in client = {0};
socklen_t size = sizeof(client);
ssize_t len = socket_recvfrom(fd, request, sizeof(request), (struct sockaddr*)&client, &size);
assert(len == 28 || len == 36);
assert(get16(request) == 1 && get32(request + 4) == COOKIE);
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 (!e->index && !flags) f->primary_bindings++;
if (f->drop_primary && !e->index) return;
if (f->drop_alternates && e->index >= 1 && e->index <= 3) return;
if (f->drop_first && !e->index && e->requests == 1) { memcpy(f->first_id, request + 8, 12); return; }
if (f->drop_first && !e->index && e->requests == 2) { assert(!memcmp(f->first_id, request + 8, 12)); f->retries++; }
if (flags && !f->ignored_change && !f->error_change) {
if (f->filtering == STUN_FILTER_APDF || (flags == 6 && f->filtering == STUN_FILTER_ADF)) return;
}
unsigned reply_index = e->index;
if (flags && !f->ignored_change && !f->error_change) reply_index = flags == 6 ? 3 : 1;
struct endpoint* sender = &f->endpoints[reply_index];
uint8_t response[80] = {0};
put16(response, flags && f->error_change ? 0x0111 : 0x0101);
put32(response + 4, COOKIE);
memcpy(response + 8, request + 8, 12);
size_t pos = 20;
if (flags && f->error_change) {
put16(response + pos, 0x0009);
put16(response + pos + 2, 4);
response[pos + 6] = f->error_change / 100;
response[pos + 7] = f->error_change % 100;
pos += 8;
} else {
struct sockaddr_in mapped = {0};
mapped.sin_family = AF_INET;
mapped.sin_addr.s_addr = htonl(0xcb007107u);
unsigned port = 40000;
if (f->mapping == STUN_MAP_ADM) port += (e->index / 2) * 10;
if (f->mapping == STUN_MAP_APDM) port += e->index * 10;
if (f->unstable && !e->index && f->primary_bindings > 1) port++;
mapped.sin_port = htons(port);
if (f->direct) mapped = client;
pos = add_address(response, pos, 0x0020, &mapped);
if (f->rfc && e->index < 4) {
struct sockaddr_in other = f->endpoints[e->index ^ 3].addr;
pos = add_address(response, pos, 0x802c, &other);
pos = add_address(response, pos, 0x802b, &sender->addr);
}
}
size_t response_len = add_fingerprint(response, pos);
if (f->tamper && !e->index && e->requests == 1) {
uint8_t bad[80];
memcpy(bad, response, response_len);
bad[response_len - 1] ^= 1;
send_reply(sender, bad, response_len, &client);
memcpy(bad, response, response_len);
put16(bad + 22, 1000);
send_reply(sender, bad, response_len, &client);
memcpy(bad, response, response_len);
bad[8] ^= 1;
add_fingerprint(bad, pos);
send_reply(sender, bad, response_len, &client);
memcpy(bad, response, response_len);
put16(bad + 26, 49999 ^ (COOKIE >> 16));
add_fingerprint(bad, pos);
send_reply(&f->endpoints[2], bad, response_len, &client);
f->bad_sent = 4;
memcpy(f->late, response, response_len);
f->late_len = response_len;
}
if (f->late_len && e->index == 4) send_reply(&f->endpoints[0], f->late, f->late_len, &client);
send_reply(sender, response, response_len, &client);
}
/* Создаёт четыре RFC5780 endpoint и два независимых обычных сервера в том же event loop. */
static void fixture_init(struct fixture* f) {
f->ua = uasync_create();
assert(f->ua);
f->inst.ua = f->ua;
f->source.fd = socket_create_udp(AF_INET);
assert(f->source.fd != SOCKET_INVALID);
struct sockaddr_in local = {0};
local.sin_family = AF_INET;
assert(inet_pton(AF_INET, "127.0.0.1", &local.sin_addr) == 1);
assert(!bind(f->source.fd, (struct sockaddr*)&local, sizeof(local)));
socklen_t size = sizeof(f->source.local_addr);
assert(!getsockname(f->source.fd, (struct sockaddr*)&f->source.local_addr, &size));
f->source.interface_addr = f->source.local_addr;
f->source.instance = &f->inst;
f->source.nat_type = NAT_VERIFIED_STRICT;
f->source.nat_addr = f->source.local_addr;
snprintf(f->source.name, sizeof(f->source.name), "source");
f->inst.etcp_sockets = &f->source;
f->original = f->source;
for (unsigned i = 0; i < 6; i++) {
struct endpoint* e = &f->endpoints[i];
e->fixture = f;
e->index = i;
e->fd = socket_create_udp(AF_INET);
assert(e->fd != SOCKET_INVALID);
assert(!socket_set_reuseaddr(e->fd, 0));
assert(!socket_set_nonblocking(e->fd));
e->addr.sin_family = AF_INET;
e->addr.sin_addr.s_addr = htonl(0x7f000001u + i / 2);
if (i == 2 || i == 3) e->addr.sin_port = f->endpoints[i - 2].addr.sin_port;
if (i == 5) e->addr.sin_addr.s_addr = htonl(0x7f000004u);
assert(!bind(e->fd, (struct sockaddr*)&e->addr, sizeof(e->addr)));
size = sizeof(e->addr);
assert(!getsockname(e->fd, (struct sockaddr*)&e->addr, &size));
assert(uasync_add_socket_t(f->ua, e->fd, server_read, NULL, NULL, "fake_stun", e));
}
}
/* Запускает настоящий модуль с конфигом; hostname-путь проверяется через /etc/hosts. */
static void fixture_start(struct fixture* f, int hostname, int reserve, int duplicate_ip) {
char config[512];
unsigned second = duplicate_ip ? 1 : 4;
snprintf(config, sizeof(config), "[stun]\nenabled=yes\nserver=%s:%u\nserver=%s:%u\n",
hostname ? "localhost" : "127.0.0.1", ntohs(f->endpoints[0].addr.sin_port),
duplicate_ip ? "127.0.0.1" : "127.0.0.3", ntohs(f->endpoints[second].addr.sin_port));
if (reserve) {
size_t pos = strlen(config);
snprintf(config + pos, sizeof(config) - pos, "server=127.0.0.4:%u\n", ntohs(f->endpoints[5].addr.sin_port));
}
f->inst.config = parse_config_from_buf(config, strlen(config), "test_stun_nat");
assert(f->inst.config);
assert(!stun_nat_init(&f->inst));
}
static struct stun_nat_result wait_result(struct fixture* f) {
struct stun_nat_result result;
uint64_t deadline = get_time_tb() + 3000000;
do {
assert(!stun_nat_get_result(&f->inst, "source", &result));
if (result.status != STUN_NAT_PENDING) return result;
test_clock_poll(f->ua, 10000);
} while (get_time_tb() < deadline);
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "test STUN measurement exceeded virtual deadline");
assert(0);
return result;
}
/* Проверяет неизменность исходного fd, sockaddr, NAT-полей и отсутствие таймеров после destroy. */
static void fixture_destroy(struct fixture* f) {
stun_nat_destroy(&f->inst);
assert(!memcmp(&f->source, &f->original, sizeof(f->source)));
struct sockaddr_storage addr = {0};
socklen_t size = sizeof(addr);
assert(!getsockname(f->source.fd, (struct sockaddr*)&addr, &size));
assert(!memcmp(&addr, &f->source.local_addr, sizeof(addr)));
assert(f->inst.nat_det == NULL);
for (unsigned i = 0; i < 6; i++) {
assert(!uasync_remove_socket_t(f->ua, f->endpoints[i].fd));
socket_close_wrapper(f->endpoints[i].fd);
}
socket_close_wrapper(f->source.fd);
free_config(f->inst.config);
uasync_poll(f->ua, 0);
assert(!f->ua->immediate_queue_head);
assert(!timeout_heap_get_size(f->ua->timeout_heap));
uasync_destroy(f->ua, 0);
}
static void test_config(void) {
const char* good = "[stun]\nenabled=yes\nserver=one.example\nserver=two.example:1234\n";
struct utun_config* cfg = parse_config_from_buf(good, strlen(good), "stun-config");
assert(cfg && cfg->global.stun_enabled && cfg->global.stun_server_count == 2);
assert(!strcmp(cfg->global.stun_servers->host, "one.example") && cfg->global.stun_servers->port == 3478);
assert(cfg->global.stun_servers->next->port == 1234);
free_config(cfg);
char path[] = "/tmp/utun-stun-config-XXXXXX";
int fd = mkstemp(path);
assert(fd >= 0 && write(fd, good, strlen(good)) == (ssize_t)strlen(good));
close(fd);
cfg = parse_config(path);
unlink(path);
assert(cfg && cfg->global.stun_server_count == 2 && cfg->global.stun_servers->port == 3478);
free_config(cfg);
const char* bad[] = {
"[stun]\nenabled=yes\nserver=one\n",
"[stun]\nserver=one:0\n", "[stun]\nserver=one:65536\n", "[stun]\nserver=one:12x\n",
"[stun]\nserver=one:1\nserver=ONE:1\n", "[stun]\nenabled=maybe\n", "[stun]\nwrong=yes\n"
};
for (unsigned i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) assert(!parse_config_from_buf(bad[i], strlen(bad[i]), "bad-stun"));
DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS STUN config: buffer/file, order, defaults, validation");
}
static void run_case(const char* name, struct fixture* f, enum stun_nat_mapping mapping, enum stun_nat_filtering filtering) {
fixture_init(f);
fixture_start(f, f->tamper, f->drop_primary, 0);
struct stun_nat_result r = wait_result(f);
assert(r.server_count == 2 && r.mapping == mapping && r.filtering == filtering);
assert(r.translated == !f->direct);
assert(r.local.ss_family == AF_INET && r.measured_at_tb);
if (f->tamper) assert(f->bad_sent == 4);
if (f->drop_first) assert(f->retries == 1);
if (f->ignored_change || f->error_change) assert(r.filtering_reason == STUN_REASON_UNSUPPORTED);
if (f->drop_alternates) assert(r.filtering_reason == STUN_REASON_TIMEOUT);
if (f->unstable) assert(r.mapping_reason == STUN_REASON_UNSTABLE && r.filtering_reason == STUN_REASON_UNSTABLE);
if (filtering != STUN_FILTER_UNKNOWN) assert(r.status == STUN_NAT_COMPLETE);
else assert(r.status == STUN_NAT_PARTIAL);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS STUN %s mapping=%s filtering=%s", name,
stun_nat_mapping_name(r.mapping), stun_nat_filtering_name(r.filtering));
fixture_destroy(f);
}
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_INFO);
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG);
test_clock_init(10000);
test_config();
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 };
run_case("ordinary dependent", &dependent, STUN_MAP_DEPENDENT, STUN_FILTER_UNKNOWN);
struct fixture eif = { .rfc = 1, .mapping = STUN_MAP_EIM, .filtering = STUN_FILTER_EIF };
run_case("RFC5780 EIM/EIF", &eif, STUN_MAP_EIM, STUN_FILTER_EIF);
struct fixture adf = { .rfc = 1, .mapping = STUN_MAP_ADM, .filtering = STUN_FILTER_ADF };
run_case("RFC5780 ADM/ADF", &adf, STUN_MAP_ADM, STUN_FILTER_ADF);
struct fixture apdf = { .rfc = 1, .mapping = STUN_MAP_APDM, .filtering = STUN_FILTER_APDF };
run_case("RFC5780 APDM/APDF", &apdf, STUN_MAP_APDM, STUN_FILTER_APDF);
struct fixture ignored = { .rfc = 1, .ignored_change = 1, .mapping = STUN_MAP_EIM };
run_case("ignored CHANGE", &ignored, STUN_MAP_EIM, STUN_FILTER_UNKNOWN);
struct fixture error = { .rfc = 1, .error_change = 420, .mapping = STUN_MAP_EIM };
run_case("CHANGE error 420", &error, STUN_MAP_EIM, STUN_FILTER_UNKNOWN);
struct fixture dead = { .rfc = 1, .drop_alternates = 1, .mapping = STUN_MAP_EIM, .filtering = STUN_FILTER_APDF };
run_case("dead alternate endpoints", &dead, STUN_MAP_MATCH, STUN_FILTER_UNKNOWN);
struct fixture tamper = { .tamper = 1, .mapping = STUN_MAP_EIM };
run_case("hostname, malformed/spoofed/late responses", &tamper, STUN_MAP_MATCH, STUN_FILTER_UNKNOWN);
struct fixture loss = { .drop_first = 1, .mapping = STUN_MAP_EIM };
run_case("packet loss/retransmission", &loss, STUN_MAP_MATCH, STUN_FILTER_UNKNOWN);
struct fixture reserve = { .drop_primary = 1, .mapping = STUN_MAP_EIM };
run_case("reserve server", &reserve, STUN_MAP_MATCH, STUN_FILTER_UNKNOWN);
struct fixture unstable = { .unstable = 1, .mapping = STUN_MAP_EIM };
run_case("unstable mapping", &unstable, STUN_MAP_UNKNOWN, STUN_FILTER_UNKNOWN);
struct fixture direct = { .rfc = 1, .direct = 1, .mapping = STUN_MAP_EIM, .filtering = STUN_FILTER_EIF };
run_case("no translation", &direct, STUN_MAP_EIM, STUN_FILTER_EIF);
struct fixture duplicate = { .mapping = STUN_MAP_EIM };
fixture_init(&duplicate);
fixture_start(&duplicate, 0, 0, 1);
struct stun_nat_result result = wait_result(&duplicate);
assert(result.server_count == 1 && result.mapping == STUN_MAP_UNKNOWN && result.mapping_reason == STUN_REASON_NO_PAIR);
fixture_destroy(&duplicate);
struct fixture cancelled = { .mapping = STUN_MAP_EIM };
fixture_init(&cancelled);
fixture_start(&cancelled, 1, 0, 0);
fixture_destroy(&cancelled);
struct fixture changed = { .mapping = STUN_MAP_EIM };
fixture_init(&changed);
fixture_start(&changed, 0, 0, 0);
result = wait_result(&changed);
stun_nat_recheck(&changed.inst);
changed.test_client.sin_family = 0;
struct stun_nat_result rechecked = wait_result(&changed);
assert(rechecked.mapping == STUN_MAP_MATCH && rechecked.server_count == 2);
assert(((struct sockaddr_in*)&rechecked.local)->sin_port != ((struct sockaddr_in*)&result.local)->sin_port);
changed.inst.etcp_sockets = NULL;
test_clock_run_for(changed.ua, 30001);
assert(stun_nat_get_result(&changed.inst, "source", &result) < 0);
changed.inst.etcp_sockets = &changed.source;
changed.test_client.sin_family = 0;
test_clock_run_for(changed.ua, 30001);
result = wait_result(&changed);
assert(result.mapping == STUN_MAP_MATCH);
fixture_destroy(&changed);
test_clock_finish();
puts("PASS STUN independent sockets, RFC5780, fallback, lifecycle and ETCP isolation");
return 0;
}
Loading…
Cancel
Save