From 2c5deafe56ac94c84431f831af2de92fb07d759f Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 11 Oct 2026 23:35:47 +0200 Subject: [PATCH] Add independent STUN NAT probes with RFC 5780 discovery --- doc/stun_nat.md | 84 ++++ src/Makefile.am | 4 + src/config_parser.c | 65 ++- src/config_parser.h | 11 + src/config_parser_doc.md | 1 + src/transport_layer/stun_nat.c | 738 +++++++++++++++++++++++++++++++++ src/transport_layer/stun_nat.h | 60 +++ src/utun_instance.c | 5 + src/utun_instance.h | 2 + tests/Makefile.am | 6 + tests/test_stun_nat.c | 341 +++++++++++++++ 11 files changed, 1315 insertions(+), 2 deletions(-) create mode 100644 doc/stun_nat.md create mode 100644 src/transport_layer/stun_nat.c create mode 100644 src/transport_layer/stun_nat.h create mode 100644 tests/test_stun_nat.c diff --git a/doc/stun_nat.md b/doc/stun_nat.md new file mode 100644 index 00000000..e19b5b35 --- /dev/null +++ b/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). diff --git a/src/Makefile.am b/src/Makefile.am index 474772b1..a8ec43e9 100644 --- a/src/Makefile.am +++ b/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 \ diff --git a/src/config_parser.c b/src/config_parser.c index 5444e7a1..6de0ea05 100644 --- a/src/config_parser.c +++ b/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; -} \ No newline at end of file +} diff --git a/src/config_parser.h b/src/config_parser.h index 8e264ab2..4dd9d9d4 100644 --- a/src/config_parser.h +++ b/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]; // пароль для входа в группу diff --git a/src/config_parser_doc.md b/src/config_parser_doc.md index 096cdbed..e64f8565 100644 --- a/src/config_parser_doc.md +++ b/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]` | игнорируется | (зарезервировано) | ### Функции diff --git a/src/transport_layer/stun_nat.c b/src/transport_layer/stun_nat.c new file mode 100644 index 00000000..80c23aa2 --- /dev/null +++ b/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 +#include +#include + +#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; +} diff --git a/src/transport_layer/stun_nat.h b/src/transport_layer/stun_nat.h new file mode 100644 index 00000000..89d8e611 --- /dev/null +++ b/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 +#include + +#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 diff --git a/src/utun_instance.c b/src/utun_instance.c index 1af30989..4aa4d24d 100644 --- a/src/utun_instance.c +++ b/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; diff --git a/src/utun_instance.h b/src/utun_instance.h index bb78e987..a7cf2e3d 100644 --- a/src/utun_instance.h +++ b/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 diff --git a/tests/Makefile.am b/tests/Makefile.am index 0f4b61a7..d2cd0dff 100644 --- a/tests/Makefile.am +++ b/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) diff --git a/tests/test_stun_nat.c b/tests/test_stun_nat.c new file mode 100644 index 00000000..6a8d0b45 --- /dev/null +++ b/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 +#include +#include +#include + +#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; +}