11 changed files with 1315 additions and 2 deletions
@ -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). |
||||
@ -0,0 +1,738 @@
|
||||
#include "stun_nat.h" |
||||
#include "etcp_connections.h" |
||||
#include "../utun_instance.h" |
||||
#include "../lib/async_dns.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/platform_compat.h" |
||||
#include "crc32.h" |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <limits.h> |
||||
|
||||
#define STUN_COOKIE 0x2112a442u |
||||
#define STUN_FINGERPRINT_XOR 0x5354554eu |
||||
#define STUN_REQUEST 0x0001 |
||||
#define STUN_SUCCESS 0x0101 |
||||
#define STUN_ERROR 0x0111 |
||||
#define STUN_MAPPED 0x0001 |
||||
#define STUN_CHANGE 0x0003 |
||||
#define STUN_ERROR_CODE 0x0009 |
||||
#define STUN_XOR_MAPPED 0x0020 |
||||
#define STUN_FINGERPRINT 0x8028 |
||||
#define STUN_ORIGIN 0x802b |
||||
#define STUN_OTHER 0x802c |
||||
#define STUN_REQUESTS 7 |
||||
#define STUN_SCAN_TB 30000 |
||||
|
||||
enum stun_stage { STUN_BASE, STUN_FILTER_BOTH, STUN_CONTROL_BOTH, STUN_FILTER_PORT, |
||||
STUN_CONTROL_PORT, STUN_CHECK_OTHER, STUN_CHECK_PORT, STUN_MAP_IP, STUN_MAP_PORT, STUN_VERIFY }; |
||||
|
||||
/* Снимок исходного сокета используется только для выбора того же сетевого пути. */ |
||||
struct stun_source { |
||||
char name[MAX_CONN_NAME_LEN]; |
||||
struct sockaddr_in addr; |
||||
uint32_t netif_index; |
||||
int mark, fib, skipped; |
||||
char device[64]; |
||||
}; |
||||
|
||||
struct stun_history { |
||||
struct stun_history* next; |
||||
struct sockaddr_in addr; |
||||
}; |
||||
|
||||
struct stun_probe { |
||||
struct stun_probe* next; |
||||
struct STUN_NAT* owner; |
||||
struct stun_source source; |
||||
struct stun_nat_result result; |
||||
socket_t fd; |
||||
void* socket_handle; |
||||
void* timer; |
||||
struct adns_query* dns; |
||||
const struct CFG_STUN_SERVER* cursor; |
||||
uint16_t dns_port; |
||||
uint16_t last_port; |
||||
struct stun_history* history; |
||||
struct sockaddr_in primary, other, reference, base_mapping, ip_mapping, target, expected; |
||||
enum stun_stage stage; |
||||
uint8_t transaction[12]; |
||||
unsigned sent; |
||||
uint64_t sent_at_tb; |
||||
int seen, capability; |
||||
enum stun_nat_filtering pending_filter; |
||||
}; |
||||
|
||||
struct STUN_NAT { |
||||
struct UTUN_INSTANCE* inst; |
||||
struct stun_probe* probes; |
||||
void* scan_timer; |
||||
uint64_t next_transaction_tb; |
||||
}; |
||||
|
||||
struct stun_response { |
||||
struct sockaddr_in mapped, other, origin; |
||||
int have_mapped, have_other, have_origin, error; |
||||
}; |
||||
|
||||
static void next_server(struct stun_probe* p); |
||||
static void start_transaction(struct stun_probe* p, enum stun_stage stage, const struct sockaddr_in* target); |
||||
static void begin_mapping(struct stun_probe* p); |
||||
static void finish_server(struct stun_probe* p); |
||||
static void scan_sources(struct STUN_NAT* mod); |
||||
|
||||
static uint16_t read16(const uint8_t* p) { return ((uint16_t)p[0] << 8) | p[1]; } |
||||
static uint32_t read32(const uint8_t* p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } |
||||
static void write16(uint8_t* p, uint16_t v) { p[0] = v >> 8; p[1] = v; } |
||||
static void write32(uint8_t* p, uint32_t v) { p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v; } |
||||
static int same_addr(const struct sockaddr_in* a, const struct sockaddr_in* b) { |
||||
return a->sin_addr.s_addr == b->sin_addr.s_addr && a->sin_port == b->sin_port; |
||||
} |
||||
|
||||
const char* stun_nat_mapping_name(enum stun_nat_mapping v) { |
||||
static const char* names[] = { "UNKNOWN", "MATCH", "DEPENDENT", "EIM", "ADM", "APDM" }; |
||||
return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "INVALID"; |
||||
} |
||||
|
||||
const char* stun_nat_filtering_name(enum stun_nat_filtering v) { |
||||
static const char* names[] = { "UNKNOWN", "EIF", "ADF", "APDF" }; |
||||
return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "INVALID"; |
||||
} |
||||
|
||||
const char* stun_nat_reason_name(enum stun_nat_reason v) { |
||||
static const char* names[] = { "ok", "RFC5780 unsupported", "prior STUN traffic", "timeout", "server error", |
||||
"unstable mapping", "no independent server pair", "DNS failure", "socket I/O failure", "unsupported socket", "out of memory" }; |
||||
return (unsigned)v < sizeof(names) / sizeof(names[0]) ? names[v] : "invalid reason"; |
||||
} |
||||
|
||||
static const char* stage_name(enum stun_stage stage) { |
||||
static const char* names[] = { "binding", "change-ip-port", "control-ip-port", "change-port", |
||||
"control-port", "check-alternate-ip", "check-alternate-port", "mapping-ip", "mapping-port", "verify" }; |
||||
return names[stage]; |
||||
} |
||||
|
||||
/* Завершает владение сетевыми ресурсами; сам результат остаётся доступен через API. */ |
||||
static void close_probe_io(struct stun_probe* p) { |
||||
struct UASYNC* ua = p->owner->inst->ua; |
||||
if (p->timer) { uasync_cancel_timeout(ua, p->timer); p->timer = NULL; } |
||||
if (p->dns) { adns_cancel(p->dns); p->dns = NULL; } |
||||
if (p->socket_handle) { uasync_remove_socket_t(ua, p->fd); p->socket_handle = NULL; } |
||||
if (p->fd != SOCKET_INVALID) { socket_close_wrapper(p->fd); p->fd = SOCKET_INVALID; } |
||||
while (p->history) { struct stun_history* next = p->history->next; u_free(p->history); p->history = next; } |
||||
} |
||||
|
||||
/* Публикует только локальный снимок диагностики и закрывает тестовый порт. */ |
||||
static void finish_probe(struct stun_probe* p) { |
||||
struct stun_nat_result* r = &p->result; |
||||
r->measured_at_tb = get_time_tb(); |
||||
if (r->status == STUN_NAT_PENDING) { |
||||
if (!r->server_count) r->status = STUN_NAT_FAILED; |
||||
else if (r->server_count < 2 || r->mapping == STUN_MAP_UNKNOWN || r->filtering == STUN_FILTER_UNKNOWN) r->status = STUN_NAT_PARTIAL; |
||||
else r->status = STUN_NAT_COMPLETE; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, |
||||
"STUN result source=%s local=%s servers=%u mapping=%s (%s) filtering=%s (%s) translated=%d RFC5780=%d status=%d", |
||||
p->source.name, sockaddr_storage_to_str(&r->local).str, r->server_count, |
||||
stun_nat_mapping_name(r->mapping), stun_nat_reason_name(r->mapping_reason), |
||||
stun_nat_filtering_name(r->filtering), stun_nat_reason_name(r->filtering_reason), r->translated, r->rfc5780, r->status); |
||||
close_probe_io(p); |
||||
} |
||||
|
||||
/* Сохраняет отправки только собственного STUN: повторный тест не должен открыть фильтр заранее. */ |
||||
static int remember_target(struct stun_probe* p) { |
||||
for (struct stun_history* h = p->history; h; h = h->next) if (same_addr(&h->addr, &p->target)) return 0; |
||||
struct stun_history* h = u_malloc(sizeof(*h)); |
||||
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot allocate destination history", p->source.name); return -1; } |
||||
h->addr = p->target; |
||||
h->next = p->history; |
||||
p->history = h; |
||||
return 0; |
||||
} |
||||
|
||||
/* Проверяет, что собственные предыдущие тесты не разрешили проверяемый источник. */ |
||||
static int filter_is_clean(const struct stun_probe* p, int port_only) { |
||||
for (const struct stun_history* h = p->history; h; h = h->next) { |
||||
if (!port_only && h->addr.sin_addr.s_addr == p->other.sin_addr.s_addr) return 0; |
||||
if (port_only && h->addr.sin_addr.s_addr == p->primary.sin_addr.s_addr && h->addr.sin_port == p->other.sin_port) return 0; |
||||
} |
||||
return 1; |
||||
} |
||||
|
||||
/* После таймаута CHANGE проверяет обычный Binding, прежде чем судить о фильтрации. */ |
||||
static void transaction_failed(struct stun_probe* p, enum stun_nat_reason reason) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s stage=%s failed: %s target=%s:%u attempts=%u", |
||||
p->source.name, stage_name(p->stage), stun_nat_reason_name(reason), |
||||
ip_to_str(&p->target.sin_addr, AF_INET).str, ntohs(p->target.sin_port), p->sent); |
||||
switch (p->stage) { |
||||
case STUN_BASE: |
||||
p->result.mapping_reason = reason; |
||||
next_server(p); |
||||
break; |
||||
case STUN_FILTER_BOTH: |
||||
case STUN_FILTER_PORT: |
||||
if (reason == STUN_REASON_TIMEOUT) { |
||||
enum stun_stage next = p->stage == STUN_FILTER_BOTH ? STUN_CONTROL_BOTH : STUN_CONTROL_PORT; |
||||
start_transaction(p, next, &p->primary); |
||||
} else { p->result.filtering_reason = reason; begin_mapping(p); } |
||||
break; |
||||
case STUN_CONTROL_BOTH: |
||||
case STUN_CONTROL_PORT: |
||||
case STUN_CHECK_OTHER: |
||||
case STUN_CHECK_PORT: |
||||
p->result.filtering_reason = reason; |
||||
begin_mapping(p); |
||||
break; |
||||
case STUN_MAP_IP: |
||||
case STUN_MAP_PORT: |
||||
p->result.mapping_reason = reason; |
||||
finish_server(p); |
||||
break; |
||||
case STUN_VERIFY: |
||||
p->result.mapping = STUN_MAP_UNKNOWN; |
||||
p->result.mapping_reason = reason; |
||||
p->result.filtering = STUN_FILTER_UNKNOWN; |
||||
p->result.filtering_reason = STUN_REASON_UNSTABLE; |
||||
finish_probe(p); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/* Отправляет/повторяет одну транзакцию с тем же случайным ID и экспоненциальным RTO. */ |
||||
static void send_request(void* arg) { |
||||
struct stun_probe* p = arg; |
||||
p->timer = NULL; |
||||
if (p->sent == STUN_REQUESTS) { transaction_failed(p, STUN_REASON_TIMEOUT); return; } |
||||
if (remember_target(p) < 0) { |
||||
p->result.status = STUN_NAT_FAILED; |
||||
p->result.mapping_reason = STUN_REASON_NOMEM; |
||||
finish_probe(p); |
||||
return; |
||||
} |
||||
uint8_t packet[36] = {0}; |
||||
unsigned flags = p->stage == STUN_FILTER_BOTH ? 6 : p->stage == STUN_FILTER_PORT ? 2 : 0; |
||||
size_t fp_offset = flags ? 28 : 20; |
||||
size_t len = fp_offset + 8; |
||||
write16(packet, STUN_REQUEST); |
||||
write16(packet + 2, (uint16_t)(len - 20)); |
||||
write32(packet + 4, STUN_COOKIE); |
||||
memcpy(packet + 8, p->transaction, 12); |
||||
if (flags) { write16(packet + 20, STUN_CHANGE); write16(packet + 22, 4); write32(packet + 24, flags); } |
||||
write16(packet + fp_offset, STUN_FINGERPRINT); |
||||
write16(packet + fp_offset + 2, 4); |
||||
write32(packet + fp_offset + 4, crc32_calc(packet, fp_offset) ^ STUN_FINGERPRINT_XOR); |
||||
ssize_t sent = socket_sendto(p->fd, packet, len, (const struct sockaddr*)&p->target, sizeof(p->target)); |
||||
if (sent != (ssize_t)len) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s send failed: sent=%zd expected=%zu error=%s", |
||||
p->source.name, sent, len, socket_strerror(socket_get_error())); |
||||
transaction_failed(p, STUN_REASON_IO); |
||||
return; |
||||
} |
||||
p->sent_at_tb = get_time_tb(); |
||||
p->sent++; |
||||
int delay_ms = p->sent == STUN_REQUESTS ? 8000 : 500 << (p->sent - 1); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN TX source=%s stage=%s target=%s:%u attempt=%u id=%08x%08x%08x RTO=%dms", |
||||
p->source.name, stage_name(p->stage), ip_to_str(&p->target.sin_addr, AF_INET).str, ntohs(p->target.sin_port), |
||||
p->sent, read32(p->transaction), read32(p->transaction + 4), read32(p->transaction + 8), delay_ms); |
||||
p->timer = uasync_set_timeout(p->owner->inst->ua, delay_ms * 10, p, send_request, "stun_retry"); |
||||
if (!p->timer) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s retry timer allocation failed", p->source.name); |
||||
p->result.status = STUN_NAT_FAILED; |
||||
p->result.mapping_reason = STUN_REASON_NOMEM; |
||||
finish_probe(p); |
||||
} |
||||
} |
||||
|
||||
/* Новые транзакции всех проб ограничены десятью в секунду; event loop не блокируется. */ |
||||
static void start_transaction(struct stun_probe* p, enum stun_stage stage, const struct sockaddr_in* target) { |
||||
p->stage = stage; |
||||
p->target = *target; |
||||
p->expected = *target; |
||||
if (stage == STUN_FILTER_BOTH) p->expected = p->other; |
||||
if (stage == STUN_FILTER_PORT) p->expected.sin_port = p->other.sin_port; |
||||
p->sent = 0; |
||||
if (random_bytes(p->transaction, sizeof(p->transaction)) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s transaction random generation failed", p->source.name); |
||||
p->result.status = STUN_NAT_FAILED; |
||||
p->result.mapping_reason = STUN_REASON_IO; |
||||
finish_probe(p); |
||||
return; |
||||
} |
||||
uint64_t now = get_time_tb(); |
||||
if (p->owner->next_transaction_tb < now) p->owner->next_transaction_tb = now; |
||||
uint64_t delay = p->owner->next_transaction_tb - now; |
||||
p->owner->next_transaction_tb += 1000; |
||||
if (delay > INT_MAX) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s transaction pacing delay overflow", p->source.name); |
||||
p->result.status = STUN_NAT_FAILED; |
||||
p->result.mapping_reason = STUN_REASON_IO; |
||||
finish_probe(p); |
||||
return; |
||||
} |
||||
p->timer = uasync_set_timeout(p->owner->inst->ua, (int)delay, p, send_request, "stun_start"); |
||||
if (!p->timer) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s start timer allocation failed", p->source.name); |
||||
p->result.status = STUN_NAT_FAILED; |
||||
p->result.mapping_reason = STUN_REASON_NOMEM; |
||||
finish_probe(p); |
||||
} |
||||
} |
||||
|
||||
/* После filtering проверяет изменение IP, затем изменение порта альтернативного IP. */ |
||||
static void begin_mapping(struct stun_probe* p) { |
||||
if (p->capability && p->result.mapping < STUN_MAP_EIM) { |
||||
struct sockaddr_in target = p->other; |
||||
target.sin_port = p->primary.sin_port; |
||||
start_transaction(p, STUN_MAP_IP, &target); |
||||
} else finish_server(p); |
||||
} |
||||
|
||||
/* Выбирает второй независимый сервер или повторно проверяет первоначальную трансляцию. */ |
||||
static void finish_server(struct stun_probe* p) { |
||||
if (p->result.server_count < 2) next_server(p); |
||||
else start_transaction(p, STUN_VERIFY, &p->reference); |
||||
} |
||||
|
||||
/* Начинает обычный Binding выбранного независимого сервера. */ |
||||
static void use_server(struct stun_probe* p, const struct sockaddr_in* addr) { |
||||
p->primary = *addr; |
||||
p->capability = 0; |
||||
start_transaction(p, STUN_BASE, &p->primary); |
||||
} |
||||
|
||||
/* DNS-ответ принадлежит только живой пробе; destroy отменяет запрос без callback. */ |
||||
static void dns_done(const struct adns_result* res, void* arg) { |
||||
struct stun_probe* p = arg; |
||||
p->dns = NULL; |
||||
if (res->status != ADNS_OK || !res->count) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s DNS failed status=%d", p->source.name, res->status); |
||||
p->result.mapping_reason = STUN_REASON_DNS; |
||||
next_server(p); |
||||
return; |
||||
} |
||||
for (int i = 0; i < res->count; i++) { |
||||
struct sockaddr_in addr = res->addrs[i]; |
||||
addr.sin_port = htons(p->dns_port); |
||||
uint32_t ip = ntohl(addr.sin_addr.s_addr); |
||||
if (ip && ip < 0xe0000000u && (!p->result.server_count || addr.sin_addr.s_addr != p->reference.sin_addr.s_addr)) { |
||||
use_server(p, &addr); |
||||
return; |
||||
} |
||||
} |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s DNS supplied no independent server IP", p->source.name); |
||||
next_server(p); |
||||
} |
||||
|
||||
/* Обходит список конфига: недоступные серверы заменяются резервными. */ |
||||
static void next_server(struct stun_probe* p) { |
||||
while (p->cursor) { |
||||
const struct CFG_STUN_SERVER* server = p->cursor; |
||||
p->cursor = server->next; |
||||
struct sockaddr_in addr = {0}; |
||||
addr.sin_family = AF_INET; |
||||
addr.sin_port = htons(server->port); |
||||
if (inet_pton(AF_INET, server->host, &addr.sin_addr) == 1) { |
||||
uint32_t ip = ntohl(addr.sin_addr.s_addr); |
||||
if (!ip || ip >= 0xe0000000u || (p->result.server_count && addr.sin_addr.s_addr == p->reference.sin_addr.s_addr)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s skipping invalid/duplicate server IP=%s", p->source.name, server->host); |
||||
continue; |
||||
} |
||||
use_server(p, &addr); |
||||
return; |
||||
} |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN source=%s resolving %s:%u", p->source.name, server->host, server->port); |
||||
p->dns_port = server->port; |
||||
p->dns = adns_resolve(p->owner->inst->ua, server->host, NULL, dns_done, p); |
||||
if (p->dns) return; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot start DNS for %s", p->source.name, server->host); |
||||
p->result.mapping_reason = STUN_REASON_DNS; |
||||
} |
||||
if (p->result.server_count) { |
||||
if (p->result.mapping == STUN_MAP_UNKNOWN) p->result.mapping_reason = STUN_REASON_NO_PAIR; |
||||
start_transaction(p, STUN_VERIFY, &p->reference); |
||||
} else finish_probe(p); |
||||
} |
||||
|
||||
/* Декодирует адрес без unaligned-доступов; IPv6 не смешивается с IPv4 измерением. */ |
||||
static int parse_addr(const uint8_t* data, size_t len, int xored, struct sockaddr_in* out) { |
||||
if (len != 8 || data[0] || data[1] != 1) return -1; |
||||
memset(out, 0, sizeof(*out)); |
||||
out->sin_family = AF_INET; |
||||
out->sin_port = htons(read16(data + 2) ^ (xored ? STUN_COOKIE >> 16 : 0)); |
||||
out->sin_addr.s_addr = htonl(read32(data + 4) ^ (xored ? STUN_COOKIE : 0)); |
||||
uint32_t ip = ntohl(out->sin_addr.s_addr); |
||||
return !out->sin_port || !ip || ip >= 0xe0000000u ? -1 : 0; |
||||
} |
||||
|
||||
/* Проверяет TLV, выравнивание, fingerprint и необходимые поля ответа STUN. */ |
||||
static int parse_response(const uint8_t* data, size_t len, struct stun_response* r) { |
||||
if (len < 20 || read32(data + 4) != STUN_COOKIE || (read16(data + 2) & 3) || len != 20u + read16(data + 2)) return -1; |
||||
uint16_t type = read16(data); |
||||
if (type != STUN_SUCCESS && type != STUN_ERROR) return -1; |
||||
memset(r, 0, sizeof(*r)); |
||||
for (size_t pos = 20; pos < len;) { |
||||
if (len - pos < 4) return -1; |
||||
uint16_t attr = read16(data + pos), size = read16(data + pos + 2); |
||||
size_t padded = ((size_t)size + 3) & ~(size_t)3; |
||||
if (padded > len - pos - 4) return -1; |
||||
const uint8_t* value = data + pos + 4; |
||||
if (attr == STUN_XOR_MAPPED && !r->have_mapped) { |
||||
if (parse_addr(value, size, 1, &r->mapped) < 0) return -1; |
||||
r->have_mapped = 1; |
||||
} else if (attr == STUN_OTHER && !r->have_other) { |
||||
if (parse_addr(value, size, 0, &r->other) < 0) return -1; |
||||
r->have_other = 1; |
||||
} else if (attr == STUN_ORIGIN && !r->have_origin) { |
||||
if (parse_addr(value, size, 0, &r->origin) < 0) return -1; |
||||
r->have_origin = 1; |
||||
} else if (attr == STUN_ERROR_CODE) { |
||||
if (size < 4 || value[0] || value[1] || value[2] < 3 || value[2] > 6 || value[3] > 99) return -1; |
||||
r->error = value[2] * 100 + value[3]; |
||||
} else if (attr == STUN_FINGERPRINT) { |
||||
if (size != 4 || pos + 8 != len || read32(value) != (crc32_calc(data, pos) ^ STUN_FINGERPRINT_XOR)) return -1; |
||||
} else if (attr < 0x8000 && attr != STUN_MAPPED && attr != 0x000a) return -1; |
||||
pos += 4 + padded; |
||||
} |
||||
if (type == STUN_ERROR) return r->error ? 0 : -1; |
||||
return r->have_mapped && !r->error ? 0 : -1; |
||||
} |
||||
|
||||
/* Завершает очередной этап, сохраняя mapping и filtering как независимые свойства. */ |
||||
static void handle_response(struct stun_probe* p, const struct stun_response* r) { |
||||
struct stun_nat_result* result = &p->result; |
||||
if (r->error) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s stage=%s server error=%d", p->source.name, stage_name(p->stage), r->error); |
||||
transaction_failed(p, r->error == 420 ? STUN_REASON_UNSUPPORTED : STUN_REASON_SERVER_ERROR); |
||||
return; |
||||
} |
||||
switch (p->stage) { |
||||
case STUN_BASE: { |
||||
unsigned idx = result->server_count++; |
||||
memcpy(&result->servers[idx], &p->primary, sizeof(p->primary)); |
||||
memcpy(&result->mapped[idx], &r->mapped, sizeof(r->mapped)); |
||||
p->base_mapping = r->mapped; |
||||
if (idx == 0) p->reference = p->primary; |
||||
if (!same_addr((const struct sockaddr_in*)&result->local, &r->mapped)) result->translated = 1; |
||||
else if (result->translated < 0) result->translated = 0; |
||||
p->capability = r->have_other && r->have_origin && r->other.sin_addr.s_addr != p->primary.sin_addr.s_addr && |
||||
r->other.sin_port != p->primary.sin_port; |
||||
if (p->capability) { p->other = r->other; result->rfc5780 = 1; } |
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN source=%s server lacks usable RFC5780 OTHER-ADDRESS/RESPONSE-ORIGIN", p->source.name); |
||||
if (p->capability && result->filtering == STUN_FILTER_UNKNOWN && filter_is_clean(p, 0)) |
||||
start_transaction(p, STUN_FILTER_BOTH, &p->primary); |
||||
else { |
||||
if (p->capability && result->filtering == STUN_FILTER_UNKNOWN) result->filtering_reason = STUN_REASON_TRAFFIC; |
||||
begin_mapping(p); |
||||
} |
||||
break; |
||||
} |
||||
case STUN_FILTER_BOTH: |
||||
case STUN_FILTER_PORT: |
||||
if (!same_addr(&p->base_mapping, &r->mapped)) { |
||||
result->filtering_reason = STUN_REASON_UNSTABLE; |
||||
} else { |
||||
if (p->stage == STUN_FILTER_PORT) { |
||||
p->pending_filter = STUN_FILTER_ADF; |
||||
start_transaction(p, STUN_CHECK_OTHER, &p->other); |
||||
break; |
||||
} |
||||
result->filtering = STUN_FILTER_EIF; |
||||
result->filtering_reason = STUN_REASON_NONE; |
||||
} |
||||
begin_mapping(p); |
||||
break; |
||||
case STUN_CONTROL_BOTH: |
||||
case STUN_CONTROL_PORT: |
||||
if (!same_addr(&p->base_mapping, &r->mapped) || !r->have_other || !same_addr(&p->other, &r->other)) { |
||||
result->filtering_reason = STUN_REASON_UNSTABLE; |
||||
begin_mapping(p); |
||||
} else if (p->stage == STUN_CONTROL_BOTH) { |
||||
if (filter_is_clean(p, 1)) start_transaction(p, STUN_FILTER_PORT, &p->primary); |
||||
else { result->filtering_reason = STUN_REASON_TRAFFIC; begin_mapping(p); } |
||||
} else { |
||||
p->pending_filter = STUN_FILTER_APDF; |
||||
start_transaction(p, STUN_CHECK_OTHER, &p->other); |
||||
} |
||||
break; |
||||
case STUN_CHECK_OTHER: |
||||
if (p->pending_filter == STUN_FILTER_APDF) { |
||||
struct sockaddr_in target = p->primary; |
||||
target.sin_port = p->other.sin_port; |
||||
start_transaction(p, STUN_CHECK_PORT, &target); |
||||
break; |
||||
} |
||||
result->filtering = p->pending_filter; |
||||
result->filtering_reason = STUN_REASON_NONE; |
||||
begin_mapping(p); |
||||
break; |
||||
case STUN_CHECK_PORT: |
||||
result->filtering = p->pending_filter; |
||||
result->filtering_reason = STUN_REASON_NONE; |
||||
begin_mapping(p); |
||||
break; |
||||
case STUN_MAP_IP: |
||||
p->ip_mapping = r->mapped; |
||||
if (same_addr(&p->base_mapping, &r->mapped)) { |
||||
result->mapping = STUN_MAP_EIM; |
||||
result->mapping_reason = STUN_REASON_NONE; |
||||
finish_server(p); |
||||
} else start_transaction(p, STUN_MAP_PORT, &p->other); |
||||
break; |
||||
case STUN_MAP_PORT: |
||||
result->mapping = same_addr(&p->ip_mapping, &r->mapped) ? STUN_MAP_ADM : STUN_MAP_APDM; |
||||
result->mapping_reason = STUN_REASON_NONE; |
||||
finish_server(p); |
||||
break; |
||||
case STUN_VERIFY: |
||||
if (!same_addr((const struct sockaddr_in*)&result->mapped[0], &r->mapped)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s first server mapping changed during measurement", p->source.name); |
||||
result->mapping = STUN_MAP_UNKNOWN; |
||||
result->filtering = STUN_FILTER_UNKNOWN; |
||||
result->mapping_reason = result->filtering_reason = STUN_REASON_UNSTABLE; |
||||
} else if (result->server_count == 2) { |
||||
int match = same_addr((const struct sockaddr_in*)&result->mapped[0], (const struct sockaddr_in*)&result->mapped[1]); |
||||
if (result->mapping < STUN_MAP_EIM) { |
||||
result->mapping = match ? STUN_MAP_MATCH : STUN_MAP_DEPENDENT; |
||||
result->mapping_reason = STUN_REASON_NONE; |
||||
} else if (result->mapping == STUN_MAP_EIM && !match) { |
||||
result->mapping = STUN_MAP_DEPENDENT; |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s second server contradicts EIM; retaining observed dependent mapping", |
||||
p->source.name); |
||||
} |
||||
} |
||||
finish_probe(p); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/* Единственный читатель собственного fd: проверяет источник, ID и формат до изменения состояния. */ |
||||
static void read_response(socket_t fd, void* arg) { |
||||
struct stun_probe* p = arg; |
||||
uint8_t packet[2048]; |
||||
struct sockaddr_storage from = {0}; |
||||
socklen_t from_len = sizeof(from); |
||||
ssize_t len = socket_recvfrom(fd, packet, sizeof(packet), (struct sockaddr*)&from, &from_len); |
||||
if (len < 0) { |
||||
int err = socket_get_error(); |
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK || err == ERR_INTR) return; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s receive failed: %s", p->source.name, socket_strerror(err)); |
||||
if (p->timer) { uasync_cancel_timeout(p->owner->inst->ua, p->timer); p->timer = NULL; } |
||||
transaction_failed(p, STUN_REASON_IO); |
||||
return; |
||||
} |
||||
if (len < 20 || from.ss_family != AF_INET || memcmp(packet + 8, p->transaction, 12) || !p->sent) { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN source=%s ignoring unrelated/late datagram from=%s len=%zd", p->source.name, |
||||
sockaddr_storage_to_str(&from).str, len); |
||||
return; |
||||
} |
||||
const struct sockaddr_in* src = (const struct sockaddr_in*)&from; |
||||
int changed = p->stage == STUN_FILTER_BOTH || p->stage == STUN_FILTER_PORT; |
||||
if (!same_addr(src, &p->expected) && !(changed && same_addr(src, &p->primary))) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s matching ID from unexpected source=%s", p->source.name, sockaddr_storage_to_str(&from).str); |
||||
return; |
||||
} |
||||
struct stun_response response; |
||||
if (parse_response(packet, (size_t)len, &response) < 0 || (response.have_origin && !same_addr(src, &response.origin))) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s malformed response stage=%s from=%s len=%zd", p->source.name, |
||||
stage_name(p->stage), sockaddr_storage_to_str(&from).str, len); |
||||
return; |
||||
} |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN RX source=%s stage=%s from=%s mapped=%s:%u RTT=%llums error=%d", |
||||
p->source.name, stage_name(p->stage), sockaddr_storage_to_str(&from).str, |
||||
ip_to_str(&response.mapped.sin_addr, AF_INET).str, ntohs(response.mapped.sin_port), |
||||
(unsigned long long)((get_time_tb() - p->sent_at_tb) / 10), response.error); |
||||
if (p->timer) { uasync_cancel_timeout(p->owner->inst->ua, p->timer); p->timer = NULL; } |
||||
if (changed && !response.error && !same_addr(src, &p->expected)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN source=%s server ignored CHANGE-REQUEST", p->source.name); |
||||
transaction_failed(p, STUN_REASON_UNSUPPORTED); |
||||
} else handle_response(p, &response); |
||||
} |
||||
|
||||
/* Читает фактический путь исходного UDP без изменения его состояния. */ |
||||
static int source_snapshot(const struct ETCP_SOCKET* sock, struct stun_source* source) { |
||||
memset(source, 0, sizeof(*source)); |
||||
snprintf(source->name, sizeof(source->name), "%s", sock->name); |
||||
source->netif_index = sock->netif_index; |
||||
source->skipped = sock->is_tcp || sock->socks_udp || sock->only_local; |
||||
const struct sockaddr_storage* addr = &sock->interface_addr; |
||||
if (addr->ss_family != AF_INET || !((const struct sockaddr_in*)addr)->sin_addr.s_addr) addr = &sock->local_addr; |
||||
if (addr->ss_family != AF_INET || !((const struct sockaddr_in*)addr)->sin_addr.s_addr) source->skipped = 1; |
||||
else { source->addr = *(const struct sockaddr_in*)addr; source->addr.sin_port = 0; } |
||||
if (source->skipped) return 0; |
||||
#ifdef __linux__ |
||||
socklen_t size = sizeof(source->mark); |
||||
if (getsockopt(sock->fd, SOL_SOCKET, SO_MARK, &source->mark, &size) < 0) goto error; |
||||
size = sizeof(source->device); |
||||
if (getsockopt(sock->fd, SOL_SOCKET, SO_BINDTODEVICE, source->device, &size) < 0) goto error; |
||||
#endif |
||||
#ifdef __FreeBSD__ |
||||
socklen_t size = sizeof(source->fib); |
||||
if (getsockopt(sock->fd, SOL_SOCKET, SO_SETFIB, &source->fib, &size) < 0) goto error; |
||||
#endif |
||||
return 0; |
||||
#if defined(__linux__) || defined(__FreeBSD__) |
||||
error: |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot read routing options: %s", sock->name, socket_strerror(socket_get_error())); |
||||
return -1; |
||||
#endif |
||||
} |
||||
|
||||
/* Создаёт независимый сокет с теми же IP/device/mark/FIB и случайным свободным портом. */ |
||||
static int open_probe_socket(struct stun_probe* p) { |
||||
p->fd = socket_create_udp(AF_INET); |
||||
if (p->fd == SOCKET_INVALID) goto error; |
||||
if (socket_set_nonblocking(p->fd) < 0 || socket_set_reuseaddr(p->fd, 0) < 0) goto error; |
||||
if (p->source.mark && socket_set_mark(p->fd, p->source.mark) < 0) goto error; |
||||
if (p->source.device[0] && socket_bind_to_device(p->fd, p->source.device) < 0) goto error; |
||||
#ifdef __FreeBSD__ |
||||
if (setsockopt(p->fd, SOL_SOCKET, SO_SETFIB, &p->source.fib, sizeof(p->source.fib)) < 0) goto error; |
||||
#endif |
||||
struct sockaddr_in local = p->source.addr; |
||||
int bound = 0; |
||||
for (int i = 0; i < 16; i++) { |
||||
uint16_t random_port; |
||||
if (random_bytes((uint8_t*)&random_port, sizeof(random_port)) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s random port generation failed", p->source.name); |
||||
return -1; |
||||
} |
||||
local.sin_port = htons(49152 + (random_port & 16383)); |
||||
if (local.sin_port == p->last_port) continue; |
||||
if (bind(p->fd, (const struct sockaddr*)&local, sizeof(local)) == 0) { bound = 1; break; } |
||||
#ifdef _WIN32 |
||||
if (socket_get_error() != WSAEADDRINUSE) goto error; |
||||
#else |
||||
if (socket_get_error() != EADDRINUSE) goto error; |
||||
#endif |
||||
} |
||||
if (!bound) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s no free random test port after 16 attempts", p->source.name); |
||||
return -1; |
||||
} |
||||
p->last_port = local.sin_port; |
||||
memcpy(&p->result.local, &local, sizeof(local)); |
||||
p->socket_handle = uasync_add_socket_t(p->owner->inst->ua, p->fd, read_response, NULL, NULL, "stun_probe", p); |
||||
if (!p->socket_handle) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot register test socket", p->source.name); return -1; } |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN start source=%s test=%s netif=%u device=%s mark=%d fib=%d fd=%d", |
||||
p->source.name, sockaddr_storage_to_str(&p->result.local).str, p->source.netif_index, |
||||
p->source.device, p->source.mark, p->source.fib, (int)p->fd); |
||||
return 0; |
||||
error: |
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s cannot create socket IP=%s device=%s mark=%d fib=%d: %s", |
||||
p->source.name, ip_to_str(&p->source.addr.sin_addr, AF_INET).str, p->source.device, |
||||
p->source.mark, p->source.fib, socket_strerror(socket_get_error())); |
||||
return -1; |
||||
} |
||||
|
||||
/* Освобождает пробу; никогда не закрывает исходный ETCP fd. */ |
||||
static void destroy_probe(struct stun_probe* p) { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "STUN cleanup source=%s status=%d test_fd=%d DNS=%d timer=%d", |
||||
p->source.name, p->result.status, (int)p->fd, p->dns != NULL, p->timer != NULL); |
||||
close_probe_io(p); |
||||
u_free(p); |
||||
} |
||||
|
||||
/* Начинает измерение на новом порту, исключая непосредственно предыдущий тестовый порт. */ |
||||
static void restart_probe(struct stun_probe* p) { |
||||
close_probe_io(p); |
||||
memset(&p->result, 0, sizeof(p->result)); |
||||
p->result.translated = -1; |
||||
p->result.mapping_reason = STUN_REASON_NO_PAIR; |
||||
p->result.filtering_reason = STUN_REASON_UNSUPPORTED; |
||||
p->cursor = p->owner->inst->config->global.stun_servers; |
||||
p->capability = 0; |
||||
p->pending_filter = STUN_FILTER_UNKNOWN; |
||||
p->sent = 0; |
||||
if (p->source.skipped) { |
||||
p->result.status = STUN_NAT_SKIPPED; |
||||
p->result.mapping_reason = p->result.filtering_reason = STUN_REASON_SOCKET; |
||||
finish_probe(p); |
||||
} else if (open_probe_socket(p) < 0) { |
||||
p->result.mapping_reason = STUN_REASON_IO; |
||||
finish_probe(p); |
||||
} else next_server(p); |
||||
} |
||||
|
||||
/* Сверяет снимки: новый/изменённый интерфейс получает свежую пробу, исчезнувший удаляется. */ |
||||
static void scan_sources(struct STUN_NAT* mod) { |
||||
for (struct stun_probe* p = mod->probes; p; p = p->next) p->seen = 0; |
||||
for (struct ETCP_SOCKET* sock = mod->inst->etcp_sockets; sock; sock = sock->next) { |
||||
if (sock->is_tcp) continue; |
||||
struct stun_source source; |
||||
if (source_snapshot(sock, &source) < 0) continue; |
||||
struct stun_probe** slot = &mod->probes; |
||||
while (*slot && strcmp((*slot)->source.name, source.name)) slot = &(*slot)->next; |
||||
if (*slot && !memcmp(&(*slot)->source, &source, sizeof(source))) { (*slot)->seen = 1; continue; } |
||||
if (*slot) { |
||||
struct stun_probe* old = *slot; |
||||
*slot = old->next; |
||||
destroy_probe(old); |
||||
} |
||||
struct stun_probe* p = u_calloc(1, sizeof(*p)); |
||||
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN source=%s probe allocation failed", source.name); continue; } |
||||
p->owner = mod; |
||||
p->source = source; |
||||
p->seen = 1; |
||||
p->fd = SOCKET_INVALID; |
||||
p->next = mod->probes; |
||||
mod->probes = p; |
||||
restart_probe(p); |
||||
} |
||||
struct stun_probe** slot = &mod->probes; |
||||
while (*slot) { |
||||
if ((*slot)->seen) { slot = &(*slot)->next; continue; } |
||||
struct stun_probe* p = *slot; |
||||
*slot = p->next; |
||||
destroy_probe(p); |
||||
} |
||||
} |
||||
|
||||
/* Редкая локальная сверка не отправляет повторных запросов на неизменившихся интерфейсах. */ |
||||
static void scan_timer(void* arg) { |
||||
struct STUN_NAT* mod = arg; |
||||
mod->scan_timer = NULL; |
||||
scan_sources(mod); |
||||
mod->scan_timer = uasync_set_timeout(mod->inst->ua, STUN_SCAN_TB, mod, scan_timer, "stun_scan"); |
||||
if (!mod->scan_timer) DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN interface scan timer allocation failed"); |
||||
} |
||||
|
||||
int stun_nat_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->ua || !inst->config) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN init requires instance, uasync and config"); return -1; } |
||||
if (inst->stun_nat) return 0; |
||||
if (!inst->config->global.stun_enabled) return 0; |
||||
if (inst->config->global.stun_server_count < 2) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN requires at least two configured servers"); return -1; } |
||||
struct STUN_NAT* mod = u_calloc(1, sizeof(*mod)); |
||||
if (!mod) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "STUN module allocation failed"); return -1; } |
||||
mod->inst = inst; |
||||
inst->stun_nat = mod; |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN initialized with %d servers; independent test sockets", inst->config->global.stun_server_count); |
||||
scan_timer(mod); |
||||
if (!mod->scan_timer) { stun_nat_destroy(inst); return -1; } |
||||
return 0; |
||||
} |
||||
|
||||
void stun_nat_destroy(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->stun_nat) return; |
||||
struct STUN_NAT* mod = inst->stun_nat; |
||||
inst->stun_nat = NULL; |
||||
if (mod->scan_timer) uasync_cancel_timeout(inst->ua, mod->scan_timer); |
||||
while (mod->probes) { struct stun_probe* next = mod->probes->next; destroy_probe(mod->probes); mod->probes = next; } |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN destroyed; test sockets/DNS/timers released"); |
||||
u_free(mod); |
||||
} |
||||
|
||||
void stun_nat_recheck(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->stun_nat) return; |
||||
struct STUN_NAT* mod = inst->stun_nat; |
||||
mod->next_transaction_tb = get_time_tb(); |
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "STUN rechecking interfaces using fresh test ports"); |
||||
for (struct stun_probe* p = mod->probes; p; p = p->next) restart_probe(p); |
||||
scan_sources(mod); |
||||
} |
||||
|
||||
int stun_nat_get_result(const struct UTUN_INSTANCE* inst, const char* socket_name, struct stun_nat_result* result) { |
||||
if (!inst || !inst->stun_nat || !socket_name || !result) return -1; |
||||
for (const struct stun_probe* p = inst->stun_nat->probes; p; p = p->next) { |
||||
if (!strcmp(p->source.name, socket_name)) { *result = p->result; return 0; } |
||||
} |
||||
return -1; |
||||
} |
||||
@ -0,0 +1,60 @@
|
||||
/* Независимая STUN-диагностика интерфейсов существующих IPv4 UDP-сокетов.
|
||||
* [stun] enabled=yes, повторяющийся server=host[:port]; нужны два разных IP. |
||||
* Обычный STUN сравнивает mapping; RFC 5780 дополнительно проверяет mapping/filtering. |
||||
* MATCH означает совпадение для двух серверов, а не доказанный EIM. |
||||
* Результат локальный: nat_detection, ETCP и NODEINFO не изменяются. |
||||
* Модуль создаёт собственный UDP-сокет на том же адресе/интерфейсе, с теми же mark/FIB, |
||||
* самостоятельно принимает STUN и закрывает fd. Результат относится к тестовому порту. |
||||
* Периодическая сверка замечает добавление/удаление/изменение исходных сокетов. |
||||
* Все вызовы выполняются в uasync-потоке; destroy отменяет DNS и таймеры до освобождения config. */ |
||||
#ifndef STUN_NAT_H |
||||
#define STUN_NAT_H |
||||
|
||||
#include "../lib/socket_compat.h" |
||||
#include <stddef.h> |
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
struct UTUN_INSTANCE; |
||||
struct STUN_NAT; |
||||
|
||||
enum stun_nat_status { STUN_NAT_PENDING, STUN_NAT_COMPLETE, STUN_NAT_PARTIAL, STUN_NAT_FAILED, STUN_NAT_SKIPPED }; |
||||
enum stun_nat_mapping { STUN_MAP_UNKNOWN, STUN_MAP_MATCH, STUN_MAP_DEPENDENT, STUN_MAP_EIM, STUN_MAP_ADM, STUN_MAP_APDM }; |
||||
enum stun_nat_filtering { STUN_FILTER_UNKNOWN, STUN_FILTER_EIF, STUN_FILTER_ADF, STUN_FILTER_APDF }; |
||||
enum stun_nat_reason { |
||||
STUN_REASON_NONE, STUN_REASON_UNSUPPORTED, STUN_REASON_TRAFFIC, STUN_REASON_TIMEOUT, STUN_REASON_SERVER_ERROR, |
||||
STUN_REASON_UNSTABLE, STUN_REASON_NO_PAIR, STUN_REASON_DNS, STUN_REASON_IO, STUN_REASON_SOCKET, STUN_REASON_NOMEM |
||||
}; |
||||
|
||||
struct stun_nat_result { |
||||
enum stun_nat_status status; |
||||
enum stun_nat_mapping mapping; |
||||
enum stun_nat_filtering filtering; |
||||
enum stun_nat_reason mapping_reason, filtering_reason; |
||||
struct sockaddr_storage local; // адрес независимого тестового UDP-сокета
|
||||
struct sockaddr_storage servers[2], mapped[2]; // реальные серверы и наблюдаемые внешние адреса
|
||||
unsigned server_count; |
||||
int translated; // -1 = неизвестно, 0 = нет трансляции, 1 = есть
|
||||
int rfc5780; // сервер объявил корректный OTHER-ADDRESS
|
||||
uint64_t measured_at_tb; // локальное монотонное время, 0.1мс
|
||||
}; |
||||
|
||||
/* init обходится без сетевых действий при выключенном [stun]; ошибка <0 с логом.
|
||||
* destroy отменяет DNS/таймеры и закрывает только собственные UDP-сокеты. */ |
||||
int stun_nat_init(struct UTUN_INSTANCE* inst); |
||||
void stun_nat_destroy(struct UTUN_INSTANCE* inst); |
||||
/* Явно повторяет измерения на свежих тестовых сокетах после изменения сети. */ |
||||
void stun_nat_recheck(struct UTUN_INSTANCE* inst); |
||||
/* Копирует результат для имени исходного сокета; -1 если модуль выключен/имя отсутствует. */ |
||||
int stun_nat_get_result(const struct UTUN_INSTANCE* inst, const char* socket_name, struct stun_nat_result* result); |
||||
const char* stun_nat_mapping_name(enum stun_nat_mapping value); |
||||
const char* stun_nat_filtering_name(enum stun_nat_filtering value); |
||||
const char* stun_nat_reason_name(enum stun_nat_reason value); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -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…
Reference in new issue