Browse Source

Measure owned endpoint mapping and inbound reachability separately

master
evgeny 3 days ago
parent
commit
ff69552a61
  1. 2
      Makefile.am
  2. 8
      doc/etcp_protocol.txt
  3. 145
      doc/nat_detection.txt
  4. 12
      doc/node_snapshot.md
  5. 2
      src/Makefile.am
  6. 14
      src/chat/chat_member.c
  7. 38
      src/chat/chat_status.c
  8. 15
      src/control_server.c
  9. 51
      src/routing_layer/address_profile.h
  10. 34
      src/routing_layer/conn_mgr_core.c
  11. 1300
      src/routing_layer/nat_detection.c
  12. 184
      src/routing_layer/nat_detection.h
  13. 18
      src/routing_layer/route_connectivity.c
  14. 461
      src/routing_layer/route_ping.c
  15. 63
      src/routing_layer/route_ping.h
  16. 90
      src/routing_layer/route_ping_doc.md
  17. 97
      src/routing_layer/sock_match.c
  18. 108
      src/routing_layer/sock_match.h
  19. 150
      src/routing_layer/topo_node.c
  20. 19
      src/routing_layer/topo_node.h
  21. 227
      src/routing_layer/topo_node_sqlite.c
  22. 2
      src/routing_layer/topo_node_sqlite.h
  23. 10
      src/transport_layer/etcp.c
  24. 225
      src/transport_layer/etcp_connections.c
  25. 39
      src/transport_layer/etcp_connections.h
  26. 9
      src/transport_layer/etcp_dump.c
  27. 41
      src/transport_layer/node_conn_direct.c
  28. 4
      src/utun_instance.c
  29. 4
      tests/Makefile.am
  30. 82
      tests/nat_fixture.h
  31. 27
      tests/stream_netns/README.md
  32. 19
      tests/stream_netns/TESTED.md
  33. 107
      tests/stream_netns/nat_scenario.py
  34. 17
      tests/stream_netns/network.py
  35. 155
      tests/stream_netns/node.c
  36. 8
      tests/stream_netns/probe.py
  37. 6
      tests/stream_netns/run.py
  38. 27
      tests/stream_netns/scenario.py
  39. 2
      tests/test_auto_socket_dynamic.c
  40. 2
      tests/test_etcp_connect.c
  41. 24
      tests/test_etcp_two_instances.c
  42. 3
      tests/test_ipv6_sockets.c
  43. 640
      tests/test_nat_detection.c
  44. 16
      tests/test_node_conn_direct.c
  45. 415
      tests/test_route_ping.c
  46. 122
      tests/test_sock_match.c
  47. 2
      tools/chatgui/src/accountlist.cpp
  48. 4
      tools/chatgui/src/memberlistmodel.cpp
  49. 2
      tools/chatgui/src/nodespage.cpp
  50. 36
      tools/etcpmon/etcpmon_gui.c
  51. 17
      tools/etcpmon/etcpmon_protocol.h

2
Makefile.am

@ -8,6 +8,8 @@ licensedir = $(datadir)/licenses/utun
dist_license_DATA = LICENSE LICENSING THIRD_PARTY_NOTICES
EXTRA_DIST = readme.md \
doc/nat_detection.txt \
doc/node_snapshot.md \
tinycrypt/LICENSE \
tools/chatgui/zxing-cpp/LICENSE \
tools/chatgui/zxing-cpp/core/src/libzueci/LICENSE \

8
doc/etcp_protocol.txt

@ -159,7 +159,7 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
- mtu_local: uint16_t локальный MTU сервера
- local_link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (копируется из запроса)
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
@ -176,6 +176,12 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Ответ на PING. nonce копируется из запроса для сопоставления.
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
socket_id(1), generation(8, BE), round(8, BE).
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть

145
doc/nat_detection.txt

@ -0,0 +1,145 @@
Детектор свойств endpoint
========================
Владелец рабочего сокета единственный вычисляет и публикует его профиль.
Помощники возвращают наблюдения и выполняют прямые испытания. Профиль относится
к протоколу, семейству, фактическому локальному IP:port и поколению сокета.
PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, а не результат измерения.
Модель: src/routing_layer/address_profile.h.
mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT.
inbound: UNKNOWN, OPEN, CONTACT_REQUIRED.
evidence_id: случайный 64-битный ID завершённого раунда владельца.
checked_at: время измерения владельца, Unix microseconds.
generation: случайный 64-битный ID жизненного цикла локального endpoint.
UNCHANGED означает совпадение наблюдаемых и локального адресов на испытанных
направлениях. Оно не доказывает свободный вход. EIM означает одинаковый внешний
endpoint на испытанных разных IP и портах; оно также не доказывает свободный вход.
DEPENDENT означает различие mapping на испытанных назначениях, без общего
внешнего адреса. Дальнейшее разделение ADM/APDM не используется для выбора NCD.
UDP-раунд
---------
1. Владелец A выбирает живого наблюдателя B с доступным native UDP endpoint.
Через проверяемый сокет S выполняет OBSERVE. Ответ содержит фактический
source IP:port, наблюдаемый B, и исходный тег S/generation/round.
2. Через уже установленный ETCP к B запрашивает поиск помощника C.
B выбирает C из своих работающих UDP-пиров и возвращает его ключ и endpoint.
Собственный подтверждённый NAT-профиль B/C не требуется: иначе bootstrap
зависел бы от уже завершённой детекции. Пригодность подтверждают реальные пробы.
3. Если внешний IP C совпадает с IP B, A исключает C и командует B искать
следующего помощника. Совпадающие Node ID и разные порты одного IP не дают
независимого IP-наблюдения. При отсутствии второго IP результат неполный.
4. Для входящего испытания выбирается C, с IP которого S ранее не контактировал.
Все команды C идут A -> B -> C. До первого испытания A не шлёт UDP к C.
C отправляет три аутентифицированные ADDRESS_CHECK к endpoint из наблюдения B.
A регистрирует фактический ingress на S; один отчёт о PONG не заменяет ingress.
5. После завершения первого испытания S выполняет OBSERVE к основному порту C,
затем к другому UDP-порту того же помощника. Вспомогательный порт принадлежит
наблюдателю, скрыт из NODEINFO и исключён из NCD. Проверяемый S не меняется.
6. Если первый ingress отсутствовал, C здоров и mapping к C совпал с mapping к B,
после исходящего контакта выполняется второе входящее испытание от того же C.
Контролируемый переход до/после контакта даёт CONTACT_REQUIRED.
7. S повторяет исходное наблюдение у B. Изменение этого mapping во время раунда
даёт UNKNOWN/address_changed и инвалидирует прежний общий внешний адрес.
8. При совпадении наблюдений B/C/C-alt выбирается UNCHANGED или EIM; различие
назначений даёт DEPENDENT. Первый чистый ingress даёт OPEN; подтверждённый
переход после контакта при неизменном endpoint — CONTACT_REQUIRED.
Если независимых от истории C больше нет, уже контактировавший C может измерить
mapping. Входящая часть остаётся UNKNOWN/contact_history. Отказ проверки одного
facet не превращается в ложный STRICT. Потери, ошибка отправки, отсутствие
помощника, разрыв управления и неправильный ответ имеют отдельную причину.
Контакты и повторы
-----------------
История IP назначения учитывает INIT, DATA, ACK, keepalive, PING/PONG и другие
отправки с S в двух общих точках UDP-send. Любая неожиданная отправка к C до
получения первого ingress делает входящее испытание загрязнённым.
История записывается в nat_contacts общей SQLite один раз на новый IP. Ключ —
семейство и фактический локальный IP:port. Перезапуск на том же endpoint её не
очищает. Максимум 256 IP на endpoint; переполнение/ошибка сохранения отмечаются
постоянным маркером неполной истории. Неизвестная история запрещает вывод OPEN
по испытанию, которое может использовать старое разрешение фильтра.
История характеризует наблюдаемый приложением трафик. Удаление БД, использование
того же tuple другим процессом или изменение NAT вне наблюдаемого раунда нельзя
превратить в доказательство полного отсутствия прежнего состояния. Результаты
остаются подсказками для попыток, подтверждение связи даёт handshake с нужным ключом.
Один активный раунд на S. Таймаут пробы — max(1 секунда, 4*RTT), не более 3 секунд;
три попытки, бюджет раунда 30 секунд. Повтор после ошибки через 30 секунд или
после появления подходящего транспорта; штатный refresh через 180 секунд.
Срок фактов — 300 секунд. Таймеры владельца используют monotonic timebase;
checked_at не обновляется повторным NODEINFO и не зависит от ответа сервера NTP.
При несогласованных часах получатель использует осторожные попытки, не отказ.
TCP и прокси
-------------
TCP inbound проверяется отдельным STCP handshake к объявленному настоящему
listener с проверкой ключа владельца. Его target должен присутствовать в
подписанном snapshot. Внешний порт исходящего TCP-соединения не рекламируется
как listener. Без публичного кандидата профиль остаётся UNKNOWN/no_listener.
UDP-факты не переносятся на TCP. Входящий TCP source port не определяет профиль
listener удалённого клиента. SOCKS5 UDP не классифицируется как native endpoint.
Корреляция и владение
--------------------
Транспортный PONG сопоставляется по криптографически случайному nonce, ожидаемому
ключу, source IP:port, ID и поколению принимающего сокета. Входящее испытание
дополнительно проверяет владельца, round, step и token. Перед callback PING
удаляется из pending. Отмена не вызывает callback и не интерпретирует void* arg
как контекст другого типа. Произвольный выбор первого IPv4/TCP-сокета запрещён.
Broker принимает RUN только для записанного им UDP-наблюдения данного владельца.
Размер wire, версия, enum, границы списка исключений и ненулевые поколения/раунды
проверяются до обработки. Числа новых сообщений — BE, IP — сетевые байты.
owner_key и helper_key имеют разные поля и никогда не меняют значение по роли.
На смене адреса, закрытии сокета, reset/DELETE транспорта и остановке ядра
отменяются собственные раунды и связанные helper jobs. Поздние ответы не
применяются к новому socket generation даже при повторном ID и IP:port.
Детектор останавливается до разборки групп, сокетов и соединений.
Публикация и NCD
---------------
Профиль публикуется только владельцем в общей подписанной записи NODEINFO.
Списки и их границы входят в подпись; mapping/inbound/evidence/checked_at
сохраняются и загружаются вместе. node_addresses — производная проекция:
nat_mapping, nat_inbound, nat_evidence, nat_checked, address_kind.
При изменении формата старый кэш адресов пересоздаётся; старый NAT_TYPE не
интерпретируется как новая модель. Подписанные snapshots и записи чата не удаляются.
Общий NAT-адрес рекламируется только после EIM. DEPENDENT не рекламирует
универсальный внешний endpoint. NTP.reachable не управляет рекламой адресов.
Ошибки публикации логируются; детектор повторяет сохранение/публикацию.
NCD различает OPEN, CONTACT и непроверенные публичные кандидаты. UNKNOWN допускает
ограниченные попытки. Контактирующие EIM используют обычные встречные INIT-пробы
NCD и reverse. Одинаковый внешний IP не запрещает hairpin и не доказывает общую
LAN; частные адреса проверяются маршрутизацией и аутентификацией нужного пира.
Профиль не закрывает уже работающий транспорт. Признак PUBLIC конфигурации
может влиять на очередность попыток CM, но не назначает подтверждённый NAT-тип.
Стриминг сохраняет raw NCD + REVERSE_REQ через группу, без routed DATA fallback.
Проверки
--------
test_route_ping: реальные UDP-наблюдения, эталонные байты IPv4/IPv6, неправильный
round, аутентичный PONG чужого ключа, старое поколение, отмена без callback.
test_nat_detection: разные socket ID, два рабочих UDP endpoint, TCP listener,
замена помощника с тем же IP, повтор по старому контакту, подпись/SQLite профиля,
закрытие S посреди раунда и повторный ID/порт. Утечки/таймеры/сокеты проверяются.
test_sock_match: независимость политики от фактов, UNKNOWN, CONTACT, давность,
частные адреса и разрешённая попытка hairpin.
tests/stream_netns --nat-only: 16 настоящих uTun с 10 NAT, проверка всех UDP/TCP
профилей и точного собственного внешнего адреса. --nat-check включает эту фазу
до обычных JOIN/Merkle/BGP/streaming. Главный network namespace не меняется.

12
doc/node_snapshot.md

@ -13,12 +13,24 @@
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Профиль endpoint в SOCKMETA содержит независимые `mapping` и `inbound`,
`evidence_id` и `checked_at`. Последние относятся к измерению владельца,
а не к версии всего NODEINFO: повтор записи или изменение имени не продлевает
измерение. Новые поля профиля на проводе кодируются явными BE-байтами;
границы списков metadata/адресов/REALITY также входят в каноническую подпись.
Алгоритм и условия неопределённости описаны в `doc/nat_detection.txt`.
Таблица `node_snapshots` хранит полную сериализованную запись без группового
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
источником для восстановления подписанной записи. При наличии сохранённого
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
ошибка сохранения не разрешает публикацию новой версии.
В проекции адресов сохраняются `nat_mapping`, `nat_inbound`, `nat_evidence`,
`nat_checked`, `address_kind`. Уникальность определяется endpoint и видом адреса,
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
начальные адреса). Они могут помочь установить первое соединение, но не
заменяют принятую подписанную запись. По BGP принимаются подписанные записи

2
src/Makefile.am

@ -1,7 +1,7 @@
bin_PROGRAMS = utun
noinst_LIBRARIES = libutun.a
EXTRA_DIST = lwip_tcp/COPYING dnsmasq/COPYING dnsmasq/COPYING-v3
EXTRA_DIST = lwip_tcp/COPYING dnsmasq/COPYING dnsmasq/COPYING-v3 routing_layer/address_profile.h routing_layer/route_ping_doc.md
stream_core_files = \
stream/stream.h \

14
src/chat/chat_member.c

@ -214,19 +214,20 @@ int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uin
}
snprintf(sql, sizeof(sql),
"SELECT node_id, MIN(rtt),"
"SELECT node_id, MIN(CASE WHEN rtt > 0 THEN rtt END),"
" CASE WHEN MAX(addr_type=1) THEN 1"
" WHEN MAX(addr_type=5) THEN 5"
" WHEN MAX(addr_type=2) THEN 2"
" WHEN MAX(addr_type=4) THEN 4"
" WHEN MAX(addr_type=3) THEN 3"
" ELSE 0 END"
" FROM node_addresses WHERE node_id IN (%s) AND rtt > 0 GROUP BY node_id", id_list);
" FROM node_addresses WHERE node_id IN (%s) GROUP BY node_id", id_list);
u_free(id_list);
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
int rtt_val = sqlite3_column_int(st, 1);
int rtt_val = sqlite3_column_type(st, 1) == SQLITE_NULL ? 0xFFFF : sqlite3_column_int(st, 1);
int at = sqlite3_column_int(st, 2);
for (int i = 0; i < cnt; i++) {
if (members[i].node_id == nid) {
@ -315,17 +316,18 @@ int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, u
/* RTT / NAT */
snprintf(sql, sizeof(sql),
"SELECT MIN(rtt),"
"SELECT MIN(CASE WHEN rtt > 0 THEN rtt END),"
" CASE WHEN MAX(addr_type=1) THEN 1"
" WHEN MAX(addr_type=5) THEN 5"
" WHEN MAX(addr_type=2) THEN 2"
" WHEN MAX(addr_type=4) THEN 4"
" WHEN MAX(addr_type=3) THEN 3"
" ELSE 0 END"
" FROM node_addresses WHERE node_id=? AND rtt > 0");
" FROM node_addresses WHERE node_id=?");
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
int rtt_val = sqlite3_column_int(st, 0);
int rtt_val = sqlite3_column_type(st, 0) == SQLITE_NULL ? 0xFFFF : sqlite3_column_int(st, 0);
m.rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val);
m.nat_type = (uint8_t)sqlite3_column_int(st, 1);
}

38
src/chat/chat_status.c

@ -20,8 +20,38 @@
#include "../routing_layer/topo_group.h"
#include <string.h>
static const char* nat_type_str(uint8_t t) {
switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; }
struct address_profile_text { char text[80]; };
static struct address_profile_text link_address_profile_text(const struct ETCP_LINK *link) {
static const struct address_profile unknown = {0};
const struct address_profile *profile = &unknown;
const struct TOPO_NODE *peer = topo_node_registry_find(link->etcp->instance->topo_groups, link->etcp->peer_node_id);
uint8_t socket_id = link->remote_socket_id;
/* У входящего TCP source port относится к соединению, не к listener пира. */
if (link->is_tcp && link->is_server) peer = NULL;
if (peer && link->is_tcp && link->remote_addr.ss_family == AF_INET) {
const struct sockaddr_in *target = (const struct sockaddr_in *)&link->remote_addr;
const struct TOPO_ADDR4 *address = peer->v4_addrs;
while (address && (!(address->protocol & TOPO_PROTO_TCP) || address->port != ntohs(target->sin_port) ||
memcmp(address->addr, &target->sin_addr, 4))) address = address->next;
if (address) socket_id = address->socket_id; else peer = NULL;
} else if (peer && link->is_tcp && link->remote_addr.ss_family == AF_INET6) {
const struct sockaddr_in6 *target = (const struct sockaddr_in6 *)&link->remote_addr;
const struct TOPO_ADDR6 *address = peer->v6_addrs;
while (address && (!(address->protocol & TOPO_PROTO_TCP) || address->port != ntohs(target->sin6_port) ||
memcmp(address->addr, &target->sin6_addr, 16))) address = address->next;
if (address) socket_id = address->socket_id; else peer = NULL;
}
if (peer && link->remote_addr.ss_family == AF_INET) {
for (const struct TOPO_SOCKMETA4 *meta = peer->v4_sock_meta; meta; meta = meta->next)
if (meta->id == socket_id) { profile = &meta->address_profile; break; }
} else if (peer && link->remote_addr.ss_family == AF_INET6) {
for (const struct TOPO_SOCKMETA6 *meta = peer->v6_sock_meta; meta; meta = meta->next)
if (meta->id == socket_id) { profile = &meta->address_profile; break; }
}
struct address_profile_text result;
snprintf(result.text, sizeof(result.text), "%s/%s", address_mapping_name(profile->mapping), address_inbound_name(profile->inbound));
return result;
}
static void get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
@ -117,7 +147,7 @@ static void chat_core_collect_status(struct UTUN_INSTANCE* inst) {
link->is_tcp ? "TCP" : "UDP",
link->conn ? link->conn->name : "?",
link->link_status ? "UP" : "DOWN",
nat_type_str(link->nat_type),
link_address_profile_text(link).text,
rtt_str);
link = link->next; link_idx++;
}
@ -282,7 +312,7 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
"Keepalive: sent=%u recv=%u period=%ums\n"
"Inflight: %u / %u B (%u pkts)\n"
"Encrypt: sent=%zu err=%zu Decrypt: rcvd=%zu err=%zu\n",
link_idx, link->link_status ? "UP" : "DOWN", is_tcp_str, nat_type_str(link->nat_type),
link_idx, link->link_status ? "UP" : "DOWN", is_tcp_str, link_address_profile_text(link).text,
local_str, remote_str,
link->rtt_last / 10.0f, link->tt_last / 10.0f,
link->keepalive_sent_count, link->keepalive_recv_count, link->keepalive_interval,

15
src/control_server.c

@ -955,7 +955,7 @@ static void send_socket_list(struct control_server* server, struct control_clien
uint8_t count = 0;
struct ETCP_SOCKET* sock = instance->etcp_sockets;
while (sock && count < ETCPMON_MAX_CONNECTIONS) {
count++;
if (!sock->probe_only) count++;
sock = sock->next;
}
@ -980,6 +980,8 @@ static void send_socket_list(struct control_server* server, struct control_clien
struct etcpmon_socket_info* info = (struct etcpmon_socket_info*)(buffer + sizeof(*hdr) + sizeof(*rsp));
sock = instance->etcp_sockets;
for (uint8_t i = 0; i < count && sock; i++) {
while (sock && sock->probe_only) sock = sock->next;
if (!sock) break;
strncpy(info[i].name, sock->name, ETCPMON_MAX_CONN_NAME - 1);
info[i].name[ETCPMON_MAX_CONN_NAME - 1] = '\0';
info[i].is_ipv6 = (sock->interface_addr.ss_family == AF_INET6) ? 1 : 0;
@ -997,9 +999,11 @@ static void send_socket_list(struct control_server* server, struct control_clien
info[i].port = 0;
}
info[i].config_type = sock->type;
info[i].nat_type = sock->nat_type;
info[i].mapping = sock->address_profile.mapping; info[i].inbound = sock->address_profile.inbound;
info[i].is_tcp = sock->is_tcp; info[i].generation = sock->generation;
info[i].evidence_id = sock->address_profile.evidence_id; info[i].checked_at = sock->address_profile.checked_at;
/* NAT address from socket (set when INIT_RESPONSE received) */
/* Общий внешний адрес подтверждён измерениями владельца. */
info[i].nat_ip[0] = '\0';
info[i].nat_port = 0;
if (sock->nat_addr.ss_family == AF_INET) {
@ -1011,8 +1015,9 @@ static void send_socket_list(struct control_server* server, struct control_clien
inet_ntop(AF_INET6, &sin6->sin6_addr, info[i].nat_ip, sizeof(info[i].nat_ip));
info[i].nat_port = ntohs(sin6->sin6_port);
}
DEBUG_INFO(DEBUG_CATEGORY_NAT, "socket_list to gui: sock=%s config=%d nat_type=%d nat_ip=%s nat_port=%u",
sock->name, sock->type, sock->nat_type,
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "socket_list to gui: sock=%s config=%u mapping=%s inbound=%s external=%s:%u",
sock->name, sock->type, address_mapping_name(sock->address_profile.mapping),
address_inbound_name(sock->address_profile.inbound),
info[i].nat_ip[0] ? info[i].nat_ip : "none", info[i].nat_port);
sock = sock->next;

51
src/routing_layer/address_profile.h

@ -0,0 +1,51 @@
/* Профиль конкретного endpoint: изменение адреса и доступность входа независимы.
* checked_at — время измерения владельца в Unix microseconds, не версия NODEINFO.
* Профиль помогает выбирать попытки; работоспособность подтверждает транспорт. */
#ifndef ADDRESS_PROFILE_H
#define ADDRESS_PROFILE_H
#include <stdint.h>
enum address_mapping {
ADDRESS_MAP_UNKNOWN,
ADDRESS_MAP_UNCHANGED,
ADDRESS_MAP_EIM,
ADDRESS_MAP_DEPENDENT
};
enum address_inbound {
ADDRESS_IN_UNKNOWN,
ADDRESS_IN_OPEN,
ADDRESS_IN_CONTACT
};
#define ADDRESS_PROFILE_TTL_US UINT64_C(300000000)
struct address_profile {
uint8_t mapping;
uint8_t inbound;
uint64_t evidence_id;
uint64_t checked_at;
};
static inline const char *address_mapping_name(uint8_t value) {
static const char *const names[] = {"UNKNOWN", "UNCHANGED", "EIM", "DEPENDENT"};
return value <= ADDRESS_MAP_DEPENDENT ? names[value] : "INVALID";
}
static inline const char *address_inbound_name(uint8_t value) {
static const char *const names[] = {"UNKNOWN", "OPEN", "CONTACT_REQUIRED"};
return value <= ADDRESS_IN_CONTACT ? names[value] : "INVALID";
}
static inline int address_profile_valid(const struct address_profile *profile) {
return profile && profile->mapping <= ADDRESS_MAP_DEPENDENT && profile->inbound <= ADDRESS_IN_CONTACT &&
((!profile->mapping && !profile->inbound) || (profile->evidence_id && profile->checked_at));
}
static inline int address_profile_fresh(const struct address_profile *profile, uint64_t now_us) {
return profile && profile->evidence_id && profile->checked_at && now_us >= profile->checked_at &&
now_us - profile->checked_at <= ADDRESS_PROFILE_TTL_US;
}
#endif

34
src/routing_layer/conn_mgr_core.c

@ -24,6 +24,7 @@
#include "sock_match.h"
#include "topo_group.h"
#include "route_ping.h"
#include "ntp_time.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
@ -96,17 +97,33 @@ uint8_t cm_classify_v6_addr(const uint8_t addr[16]) {
return CM_V6_DIR;
}
/* Та же классификация, что при создании линков; EIM и прямой IPv6 доступны извне. */
/* Подсказка для расписания проб, не доказательство входа. PUBLIC-политика также
* разрешает сначала попробовать объявленный LAN listener перед запуском резерва. */
int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq) {
struct TOPO_NODE* ni = nq ? topo_node_registry_find(mgr->instance->topo_groups, nq->node_id) : NULL;
if (!ni) return 0;
int64_t current = ntp_time_get_us(mgr->instance);
uint64_t now_us = current > 0 ? (uint64_t)current : 0;
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
if (!(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) continue;
for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next)
if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PUB) return 1;
uint32_t ip; memcpy(&ip, a->addr, 4);
for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) {
if (m->id != a->socket_id) continue;
uint8_t cls = sock_target_class(a->type, m->config_type, &m->address_profile, ip_is_public(ip), now_us);
if (cls != SOCK_TC_PRIV && cls != SOCK_TC_UNREACH) return 1;
if (a->type == TOPO_ADDR_INTERFACE && m->config_type == CFG_SERVER_TYPE_PUBLIC) return 1;
}
}
for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) {
if (!(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) continue;
for (const struct TOPO_SOCKMETA6 *meta = ni->v6_sock_meta; meta; meta = meta->next) {
if (meta->id != a->socket_id) continue;
uint8_t cls = sock_target_class(a->type, meta->config_type, &meta->address_profile,
cm_classify_v6_addr(a->addr) == CM_V6_DIR, now_us);
if (cls != SOCK_TC_PRIV && cls != SOCK_TC_UNREACH) return 1;
if (a->type == TOPO_ADDR_INTERFACE && meta->config_type == CFG_SERVER_TYPE_PUBLIC) return 1;
}
}
for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next)
if ((a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP)) && cm_classify_v6_addr(a->addr) == CM_V6_DIR) return 1;
return 0;
}
@ -454,7 +471,7 @@ static void cm_start_attempt(struct CONN_MGR_ENTRY* entry, int recovery) {
entry->indirect_start_tb = entry->start_tb + (recovery || (!local_public && !peer_public) ? 0 : CONN_MGR_INDIRECT_DELAY_MS * 10);
entry->deadline_tb = entry->indirect_start_tb + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS * 10;
if (!recovery) entry->initial_deadline_tb = entry->deadline_tb;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: %s group=%016llx peer=%016llx public=%d/%d reverse=%llums indirect=%llums reserve=%d",
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: %s group=%016llx peer=%016llx inbound_candidates=%d/%d reverse=%llums indirect=%llums reserve=%d",
recovery ? "recover" : "start", (unsigned long long)mgr->group->group_id, (unsigned long long)entry->node_id,
local_public, peer_public, (unsigned long long)((entry->reverse_start_tb - entry->start_tb) / 10),
(unsigned long long)((entry->indirect_start_tb - entry->start_tb) / 10), entry->indirect_confirmed);
@ -608,10 +625,13 @@ void cm_start_local_scan(struct CONN_MGR_ENTRY* entry) {
(unsigned long long)entry->node_id); return;
}
int addresses = 0;
int64_t current = ntp_time_get_us(entry->mgr->instance);
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
if (!(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) continue;
uint32_t ip; memcpy(&ip, a->addr, 4);
for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next)
if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PRIV) {
if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, &m->address_profile,
ip_is_public(ip), current > 0 ? (uint64_t)current : 0) == SOCK_TC_PRIV) {
addresses++; break;
}
}

1300
src/routing_layer/nat_detection.c

File diff suppressed because it is too large Load Diff

184
src/routing_layer/nat_detection.h

@ -1,161 +1,61 @@
/**
* @file nat_detection.h
* @brief NAT-детекция через третьего пира (STUN-like ping).
*
* Самодостаточный модуль на своём ETCP_ID (0x02):
* сам биндится, сам подписывается на conn_up/conn_down,
* ищет третий узел прямо в instance->connections,
* обновляет ETCP_SOCKET/loc_node и broadcast-ит NODEINFO.
*/
/* Владелец измеряет собственный endpoint. Пиры передают наблюдения и выполняют
* прямые пробы; управление проверяющим идёт через явно выбранного посредника.
* Все операции и callbacks выполняются в uasync-потоке ядра. */
#ifndef NAT_DETECTION_H
#define NAT_DETECTION_H
#include <stddef.h>
#include <stdint.h>
#include "../lib/socket_compat.h"
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include "secure_channel.h"
struct UTUN_INSTANCE;
struct TOPO_GROUP;
struct ETCP_CONN;
struct ETCP_LINK;
struct route_ping_pending;
/* ====== Sub-команды протокола ====== */
#define NATDET_SUBCMD_PING_REQ 0x07 // сервер → третий узел: "пропингуй IP:port"
#define NATDET_SUBCMD_PING_RESP 0x08 // третий узел → сервер: результат пинга
#define NATDET_SUBCMD_NAT_INFO 0x09 // сервер → клиент: "твой NAT — EIM/STRICT/DIRECT"
#define NATDET_SUBCMD_NAT_CHECK_REQ 0x0A // клиент → сервер: "проверь мой NAT, вот мой interface_addr"
/* ====== Wire-формат сообщений ====== */
/** Сервер → третий узел: запрос серии ETCP-пингов на target_ip:target_port */
struct NATDET_PING_REQ {
uint8_t cmd; // ETCP_ID_NAT_DETECTION
uint8_t subcmd; // NATDET_SUBCMD_PING_REQ
uint16_t reserved;
uint32_t request_id; // для корреляции запрос/ответ
uint8_t count; // число пингов в серии
uint8_t socket_id; // сокет для отправки (на третьем узле)
uint16_t interval_ms; // интервал между пингами
uint16_t timeout_ms; // таймаут одного пинга
uint8_t target_ipv4[4]; // IP цели (network byte order)
uint16_t target_port; // порт цели (network byte order)
uint8_t pubkey[SC_PUBKEY_SIZE]; // опционально: pubkey цели для шифрования пинга
} __attribute__((packed));
struct ETCP_SOCKET;
struct NAT_DETECTION;
/** Третий узел → сервер: агрегированная статистика серии пингов */
struct NATDET_PING_RESP {
uint8_t cmd;
uint8_t subcmd; // NATDET_SUBCMD_PING_RESP
uint16_t reserved;
uint32_t request_id;
uint8_t count_sent; // всего отправлено пингов
uint8_t count_ok; // успешно (получен PONG)
uint16_t avg_rtt; // средний RTT в 0.1ms
} __attribute__((packed));
/** Сервер → клиент: результат определения типа NAT */
struct NATDET_MSG_NAT_INFO {
uint8_t cmd;
uint8_t subcmd; // NATDET_SUBCMD_NAT_INFO
uint8_t socket_id; // ID сокета клиента
uint8_t nat_ip[4]; // NAT-адрес клиента (network byte order)
uint16_t nat_port; // NAT-порт клиента (network byte order)
uint8_t nat_type; // NAT_TYPE_EIM / NAT_TYPE_STRICT / NAT_TYPE_DIRECT
} __attribute__((packed));
/** Клиент → сервер: запрос на проверку NAT для указанного сокета */
struct NATDET_MSG_NAT_CHECK_REQ {
uint8_t cmd;
uint8_t subcmd; // NATDET_SUBCMD_NAT_CHECK_REQ
uint8_t socket_id; // ID сокета который надо проверить
uint32_t interface_ip; // IP клиента на интерфейсе (network byte order)
uint16_t interface_port; // порт клиента на интерфейсе (network byte order)
} __attribute__((packed));
/* ====== Callback для завершения пинга ====== */
typedef void (*route_ping_callback_t)(int success, uint16_t avg_rtt,
uint8_t count_sent, uint8_t count_ok, void* arg);
/* ====== Контекст NAT-детекции (один на UTUN_INSTANCE) ====== */
struct NAT_DETECTION {
struct UTUN_INSTANCE* inst;
struct route_ping_pending* ping_pending; // список ожидающих PING_RESP ответов
uint32_t next_ping_req_id; // монотонный счётчик request_id
uint8_t allow_nat_check_local; // разрешить чек для loopback (тесты)
enum nat_check_reason {
NAT_REASON_NONE, NAT_REASON_NO_OBSERVER, NAT_REASON_NO_HELPER, NAT_REASON_CONTROL_LOST,
NAT_REASON_SEND_ERROR, NAT_REASON_TIMEOUT, NAT_REASON_ADDRESS_CHANGED, NAT_REASON_CONTACT_HISTORY,
NAT_REASON_PROXY, NAT_REASON_NO_LISTENER, NAT_REASON_CANCELLED, NAT_REASON_INVALID_OBSERVATION, NAT_REASON_DEPENDENT_MAPPING
};
/** Аргумент, передаваемый в callback nat_link_check_cb */
struct nat_check_arg {
struct ETCP_LINK* link;
uint32_t nat_ip;
uint16_t nat_port;
struct nat_check_status {
uint64_t generation, round;
uint64_t observer, helper;
uint32_t rounds, helper_reselections;
uint8_t phase, reason, history_complete;
};
/* ==================== ПУБЛИЧНОЕ API ==================== */
/* -------- Жизненный цикл -------- */
/** Создать модуль: etcp_bind на 0x02, подписка на new_conn (on_up + on_down). */
struct NAT_DETECTION* nat_detection_create(struct UTUN_INSTANCE* inst);
/** Уничтожить: отписаться от new_conn, etcp_unbind, очистить pending пинги.
* Вызывается из topo_groups_destroy(). */
void nat_detection_destroy(struct NAT_DETECTION* nd);
/* -------- Служебные: вызываются другими модулями в ответ на события ETCP -------- */
/** Линк поднялся (INIT handshake завершён) — запустить для него NAT-чек.
* Вызывается из etcp_connections: handle_init_response_server(). */
void nat_detection_link_ready(struct NAT_DETECTION* nd, struct ETCP_LINK* link);
/** Отменить все pending NAT-пинги для соединения и сбросить nat_check_status.
* Вызывается из etcp.c при реините и etcp_connections.c при смене адреса линка. */
void nat_detection_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn);
/** Отправить клиенту NAT_INFO — результат DIRECT detection (публичный IP без пинга).
* Вызывается из etcp_connections: handle_init_response_server(). */
void nat_detection_send_nat_info(struct NAT_DETECTION* nd, struct ETCP_CONN* to_conn,
uint8_t socket_id, uint32_t nat_ip, uint16_t nat_port,
uint8_t nat_type);
/* -------- Control API -------- */
/** Принудительная перепроверка NAT для всех линков (etcpmon команда "nat").
* Вызывается из control_server. */
void nat_detection_request_check_all(struct NAT_DETECTION* nd);
/* -------- Тестовые хелперы -------- */
/** Разрешить NAT-чек для локальных подсетей (127.x, 10.x, 192.168.x).
* По умолчанию запрещено — только для тестов где узлы на loopback. */
void nat_detection_set_allow_local(struct NAT_DETECTION* nd, int allow);
/** Отправить STUN-пинг через BGP-посредника (to_conn) к target_ip:target_port.
* pubkey — опционально (NULL = взять из nodeinfo).
* count пингов, интервал interval_ms, таймаут одного timeout_ms, общий wait_timeout_ms. */
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* to_conn,
uint32_t target_ip, uint16_t target_port,
uint8_t count, uint16_t interval_ms, uint16_t timeout_ms,
uint16_t wait_timeout_ms, route_ping_callback_t cb,
void* arg, const uint8_t* pubkey);
struct NAT_DETECTION *nat_detection_create(struct UTUN_INSTANCE *instance);
void nat_detection_destroy(struct NAT_DETECTION *detector);
void nat_detection_link_ready(struct NAT_DETECTION *detector, struct ETCP_LINK *link);
void nat_detection_cancel_for_conn(struct NAT_DETECTION *detector, struct ETCP_CONN *connection);
void nat_detection_socket_removed(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket);
void nat_detection_note_send(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket,
const struct sockaddr_storage *destination);
void nat_detection_observed_by_peer(struct NAT_DETECTION *detector, struct ETCP_LINK *link,
const struct sockaddr_storage *observed);
void nat_detection_record_observation(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket,
const uint8_t peer_key[32], const struct sockaddr_storage *source,
const uint8_t *tag, size_t length);
void nat_detection_receive_ingress(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket,
const uint8_t peer_key[32], const struct sockaddr_storage *source,
const uint8_t *tag, size_t length);
void nat_detection_request_check_all(struct NAT_DETECTION *detector);
int nat_detection_request_check_socket(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, uint64_t observer);
void nat_detection_set_allow_local(struct NAT_DETECTION *detector, int allow);
/* При ручном режиме помощники продолжают работать, раунды запускает request_check_all. */
void nat_detection_set_automatic(struct NAT_DETECTION *detector, int automatic);
int nat_detection_get_status(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, struct nat_check_status *status);
const char *nat_check_reason_name(uint8_t reason);
#ifdef __cplusplus
}
#endif
#endif /* NAT_DETECTION_H */
#endif

18
src/routing_layer/route_connectivity.c

@ -93,7 +93,7 @@ static int addr_eq(const struct TOPO_ADDR4* a, uint32_t ip, uint16_t port) {
// собирает список локальных сокетов-кандидатов для probing заданного адреса
// (сопоставление через sock_match — роли сокетов и классы адресов)
static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance,
uint8_t addr_type, uint8_t tcfg, uint8_t tnat,
uint8_t addr_type, uint8_t tcfg, const struct address_profile *profile,
const struct sockaddr_storage* target_addr,
struct ETCP_SOCKET** out_sockets, uint8_t max_count) {
if (!instance || !target_addr || !out_sockets || max_count == 0) return 0;
@ -107,7 +107,7 @@ static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance,
// IPv6: prefer sockets in same /64 subnet
e_sock = instance->etcp_sockets;
while (e_sock && found < (int)max_count) {
if (e_sock->is_tcp) { e_sock = e_sock->next; continue; }
if (e_sock->is_tcp || e_sock->probe_only) { e_sock = e_sock->next; continue; }
if (e_sock->local_addr.ss_family == AF_INET6) {
struct sockaddr_in6* if_sin6 = (struct sockaddr_in6*)&e_sock->interface_addr;
if (memcmp(&if_sin6->sin6_addr, &target_sin6->sin6_addr, 8) == 0) {
@ -121,7 +121,7 @@ static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance,
// Pass 2: all other IPv6 sockets
e_sock = instance->etcp_sockets;
while (e_sock && found < (int)max_count) {
if (e_sock->is_tcp) { e_sock = e_sock->next; continue; }
if (e_sock->is_tcp || e_sock->probe_only) { e_sock = e_sock->next; continue; }
if (e_sock->local_addr.ss_family != AF_INET6) { e_sock = e_sock->next; continue; }
int dup = 0;
for (int i = 0; i < found; i++) if (out_sockets[i] == e_sock) { dup = 1; break; }
@ -137,7 +137,7 @@ static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance,
int nviews = sock_collect_views(instance, views, CONN_MAX_SOCKET_CANDIDATES);
for (int i = 0; i < nviews && found < (int)max_count; i++) {
if (views[i].is_tcp) continue;
if (!sock_match(&views[i], addr_type, tcfg, tnat, target_ip)) continue;
if (!sock_match(&views[i], addr_type, tcfg, profile, target_ip)) continue;
out_sockets[found++] = views[i].sock;
}
@ -346,11 +346,12 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G
ctx->is_tcp = 1;
ctx->candidate_count = 1; ctx->candidate_index = 0; ctx->count_total = 1;
} else {
uint8_t tcfg = CFG_SERVER_TYPE_UNKNOWN, tnat = NAT_TYPE_UNKNOWN;
uint8_t tcfg = CFG_SERVER_TYPE_UNKNOWN;
const struct address_profile *profile = NULL;
for (const struct TOPO_SOCKMETA4* mm = ni->v4_sock_meta; mm; mm = mm->next)
if (mm->id == a->socket_id) { tcfg = mm->config_type; tnat = mm->nat_type; break; }
if (mm->id == a->socket_id) { tcfg = mm->config_type; profile = &mm->address_profile; break; }
struct ETCP_SOCKET* candidates[CONN_MAX_SOCKET_CANDIDATES];
int cand_count = conn_match_candidate_sockets(instance, a->type, tcfg, tnat, &target, candidates, CONN_MAX_SOCKET_CANDIDATES);
int cand_count = conn_match_candidate_sockets(instance, a->type, tcfg, profile, &target, candidates, CONN_MAX_SOCKET_CANDIDATES);
if (cand_count == 0) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "probe: no local sockets for target %s:%u type=%d", ip_to_str(&ip, AF_INET).str, port, a->type); u_free(ctx); continue; }
memcpy(ctx->candidate_sockets, candidates, cand_count * sizeof(struct ETCP_SOCKET*));
ctx->candidate_count = cand_count; ctx->candidate_index = 0; ctx->count_total = CONN_PROBE_COUNT;
@ -393,7 +394,8 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G
ctx->candidate_count = 1; ctx->candidate_index = 0; ctx->count_total = 1;
} else {
struct ETCP_SOCKET* candidates[CONN_MAX_SOCKET_CANDIDATES];
int cand_count = conn_match_candidate_sockets(instance, a6->type, CFG_SERVER_TYPE_UNKNOWN, NAT_TYPE_UNKNOWN, &target, candidates, CONN_MAX_SOCKET_CANDIDATES);
int cand_count = conn_match_candidate_sockets(instance, a6->type, CFG_SERVER_TYPE_UNKNOWN, NULL,
&target, candidates, CONN_MAX_SOCKET_CANDIDATES);
if (cand_count == 0) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "probe: no local sockets for IPv6 target type=%d", a6->type); u_free(ctx); continue; }
memcpy(ctx->candidate_sockets, candidates, cand_count * sizeof(struct ETCP_SOCKET*));
ctx->candidate_count = cand_count; ctx->candidate_index = 0; ctx->count_total = CONN_PROBE_COUNT;

461
src/routing_layer/route_ping.c

@ -1,428 +1,77 @@
#include <stdlib.h>
#include "route_ping.h"
#include <string.h>
#include <stdio.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#endif
#include "../lib/platform_compat.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "utun_instance.h"
#include "etcp_api.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "topo_node.h"
#include "topo_group.h"
#include "route_ping.h"
#include "../lib/u_async.h"
struct route_ping_series_ctx {
struct ETCP_CONN* reply_conn;
uint32_t request_id;
struct sockaddr_storage target_addr;
uint8_t pubkey[SC_PUBKEY_SIZE];
struct ETCP_SOCKET* local_sock;
uint8_t count_total;
uint8_t count_sent;
uint8_t count_ok;
uint32_t sum_rtt; /* в 0.1 ms */
uint16_t timeout_ms;
};
// ==================================== блок обработки запросов на удаленные пинги
struct route_ping_pending {
struct route_ping_pending* next;
struct NAT_DETECTION* nd;
uint32_t request_id;
route_ping_callback_t callback;
void* arg;
void* timeout_timer;
uint8_t cancelled;
struct ETCP_CONN* via_conn; // через кого отправили запрос (third_conn)
};
// если удаленный узел долго не отвечает - вызываем коллбэк по таймауту
static void route_ping_pending_timeout(void* arg) {
struct route_ping_pending* p = (struct route_ping_pending*)arg;
if (!p) return;
p->timeout_timer = NULL;
if (p->cancelled) {
u_free(p);
return;
}
struct NAT_DETECTION* nd = p->nd;
struct route_ping_pending** cur = &nd->ping_pending;
while (*cur) {
if (*cur == p) {
*cur = p->next;
break;
}
cur = &(*cur)->next;
}
if (p->callback) {
p->callback(0, 0, 0, 0, p->arg);
}
u_free(p);
}
// прошел ответ "серия пигнов на удаленном узле завершена"
void route_ping_handle_resp(struct NAT_DETECTION* nd, struct ETCP_CONN* from_conn,
const uint8_t* data, size_t len) {
if (!nd || !from_conn || !data || len < sizeof(struct NATDET_PING_RESP)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args");
return;
}
const struct NATDET_PING_RESP* resp = (const struct NATDET_PING_RESP*)data;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "from=%s request_id=%08x sent=%u ok=%u avg_rtt=%u",
from_conn->log_name, (unsigned)resp->request_id,
(unsigned)resp->count_sent, (unsigned)resp->count_ok, (unsigned)resp->avg_rtt);
topo_node_ping_update_rtt(nd->inst->topo_groups, from_conn->peer_node_id, resp->avg_rtt);
struct route_ping_pending** cur = &nd->ping_pending;
while (*cur) {
struct route_ping_pending* p = *cur;
if (p->request_id == resp->request_id) {
*cur = p->next;
if (p->timeout_timer) {
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer);
p->timeout_timer = NULL;
}
if (p->callback) {
int success = (resp->count_ok > 0) ? 1 : 0;
p->callback(success, resp->avg_rtt, resp->count_sent, resp->count_ok, p->arg);
}
u_free(p);
return;
}
cur = &(*cur)->next;
}
DEBUG_WARN(DEBUG_CATEGORY_BGP, "request_id=%08x not found in pending list",
(unsigned)resp->request_id);
}
void route_ping_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn) {
if (!nd || !conn) return;
if (!nd->ping_pending) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "cancel_for_conn: ping_pending is NULL for %s", conn->log_name);
return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "cancel_for_conn: checking pending for %s", conn->log_name);
int cancelled = 0;
struct route_ping_pending** cur = &nd->ping_pending;
while (*cur) {
struct route_ping_pending* p = *cur;
struct nat_check_arg* na = (struct nat_check_arg*)p->arg;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, " checking pending req_id=%08x via=%s link->etcp=%s",
(unsigned)p->request_id,
p->via_conn ? p->via_conn->log_name : "NULL",
(na && na->link && na->link->etcp) ? na->link->etcp->log_name : "NULL");
if ((na && na->link && na->link->etcp == conn) || p->via_conn == conn) {
*cur = p->next;
uint32_t rid = p->request_id;
if (p->timeout_timer) {
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer);
p->timeout_timer = NULL;
}
p->cancelled = 1;
if (na) na->link = NULL;
u_free(na);
u_free(p);
cancelled++;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "cancel_for_conn: cancelled req_id=%08x", (unsigned)rid);
} else {
cur = &(*cur)->next;
}
}
if (cancelled == 0) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "cancel_for_conn: no pending found for %s", conn->log_name);
}
static void write64(uint8_t *out, uint64_t value) {
for (unsigned i = 0; i < 8; i++) out[i] = (uint8_t)(value >> (56 - i * 8));
}
void route_ping_cancel_for_link(struct NAT_DETECTION* nd, struct ETCP_LINK* link) {
if (!nd || !link) return;
struct route_ping_pending** cur = &nd->ping_pending;
while (*cur) {
struct route_ping_pending* p = *cur;
struct nat_check_arg* na = (struct nat_check_arg*)p->arg;
if (na && na->link == link) {
*cur = p->next;
uint32_t rid = p->request_id;
if (p->timeout_timer) {
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer);
p->timeout_timer = NULL;
}
p->cancelled = 1;
na->link = NULL;
u_free(na);
u_free(p);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "cancel_for_link: cancelled req_id=%08x link=%p", (unsigned)rid, (void*)link);
} else {
cur = &(*cur)->next;
}
}
int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address) {
if (!out || !address) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "address encode: missing argument"); return -1; }
memset(out, 0, ROUTE_ADDR_WIRE_SIZE);
if (address->ss_family == AF_INET) {
const struct sockaddr_in *v4 = (const struct sockaddr_in *)address;
out[0] = 4; memcpy(out + 1, &v4->sin_addr, 4); memcpy(out + 17, &v4->sin_port, 2);
} else if (address->ss_family == AF_INET6) {
const struct sockaddr_in6 *v6 = (const struct sockaddr_in6 *)address;
out[0] = 6; memcpy(out + 1, &v6->sin6_addr, 16); memcpy(out + 17, &v6->sin6_port, 2);
} else { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "address encode: unsupported family=%d", address->ss_family); return -1; }
return 0;
}
void route_ping_destroy_pending(struct NAT_DETECTION* nd) {
if (!nd) return;
struct route_ping_pending* p = nd->ping_pending;
while (p) {
struct route_ping_pending* next = p->next;
if (p->timeout_timer) {
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer);
p->timeout_timer = NULL;
}
p->cancelled = 1;
u_free(p);
p = next;
}
nd->ping_pending = NULL;
int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address) {
if (!in || !address) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "address decode: missing argument"); return -1; }
memset(address, 0, sizeof(*address));
if (in[0] == 4) {
struct sockaddr_in *v4 = (struct sockaddr_in *)address;
v4->sin_family = AF_INET; memcpy(&v4->sin_addr, in + 1, 4); memcpy(&v4->sin_port, in + 17, 2);
static const uint8_t zero[12];
if (memcmp(in + 5, zero, 12)) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "address decode: noncanonical IPv4"); return -1; }
} else if (in[0] == 6) {
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)address;
v6->sin6_family = AF_INET6; memcpy(&v6->sin6_addr, in + 1, 16); memcpy(&v6->sin6_port, in + 17, 2);
} else { DEBUG_WARN(DEBUG_CATEGORY_NAT, "address decode: invalid family=%u", in[0]); return -1; }
if (!in[17] && !in[18]) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "address decode: zero port"); return -1; }
return 0;
}
// ========================================================================
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* to_conn,
uint32_t target_ip, uint16_t target_port,
uint8_t count, uint16_t interval_ms, uint16_t timeout_ms,
uint16_t wait_timeout_ms, route_ping_callback_t cb, void* arg,
const uint8_t* pubkey) {
(void)group;
if (!nd || !to_conn || count == 0 || timeout_ms == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args");
return -1;
}
size_t pkt_size = pubkey ? sizeof(struct NATDET_PING_REQ) : offsetof(struct NATDET_PING_REQ, pubkey);
struct NATDET_PING_REQ* req_pkt = u_calloc(1, pkt_size);
if (!req_pkt) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "alloc failed");
return -2;
}
req_pkt->cmd = ETCP_ID_NAT_DETECTION;
req_pkt->subcmd = NATDET_SUBCMD_PING_REQ;
uint32_t new_req_id = nd->next_ping_req_id++;
req_pkt->request_id = new_req_id;
req_pkt->count = count;
req_pkt->interval_ms = interval_ms;
req_pkt->timeout_ms = timeout_ms;
/* IP уже в network byte order, порт конвертируем */
memcpy(req_pkt->target_ipv4, &target_ip, 4);
req_pkt->target_port = htons(target_port);
if (pubkey) {
memcpy(req_pkt->pubkey, pubkey, SC_PUBKEY_SIZE);
}
struct ll_entry* e = queue_entry_new(0);
if (!e) {
u_free(req_pkt);
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "queue_entry_new failed");
return -3;
int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port) {
if (!a || !b || a->ss_family != b->ss_family) return 0;
if (a->ss_family == AF_INET) {
const struct sockaddr_in *av = (const struct sockaddr_in *)a, *bv = (const struct sockaddr_in *)b;
return av->sin_addr.s_addr == bv->sin_addr.s_addr && (!include_port || av->sin_port == bv->sin_port);
}
e->dgram = (uint8_t*)req_pkt;
e->len = pkt_size;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "request_id=%08x ip=%s port=%u pubkey=%s",
(unsigned)new_req_id,
ip_to_str(&target_ip, AF_INET).str, (unsigned)target_port, pubkey ? "yes" : "no");
int ret = etcp_send(to_conn, e);
if (ret != 0) {
u_free(req_pkt);
queue_entry_free(e);
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "etcp_send failed");
return -4;
if (a->ss_family == AF_INET6) {
const struct sockaddr_in6 *av = (const struct sockaddr_in6 *)a, *bv = (const struct sockaddr_in6 *)b;
return !memcmp(&av->sin6_addr, &bv->sin6_addr, 16) && (!include_port || av->sin6_port == bv->sin6_port);
}
struct route_ping_pending* pending = u_calloc(1, sizeof(struct route_ping_pending));
if (!pending) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "pending alloc failed");
return -5;
}
pending->nd = nd;
pending->request_id = new_req_id;
pending->callback = cb;
pending->arg = arg;
pending->via_conn = to_conn;
pending->next = nd->ping_pending;
nd->ping_pending = pending;
pending->timeout_timer = uasync_set_timeout(nd->inst->ua, wait_timeout_ms * 10, pending,
route_ping_pending_timeout, "route_ping");
return 0;
}
static void route_ping_series_finish(struct route_ping_series_ctx* ctx) {
if (!ctx || !ctx->reply_conn) {
u_free(ctx);
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "zero args");
return;
}
uint16_t avg_rtt = (ctx->count_ok > 0)
? (uint16_t)(ctx->sum_rtt / ctx->count_ok)
: (uint16_t)(ctx->timeout_ms * 10); /* таймаут как "плохо" */
/* Отправляем ответ по BGP */
struct NATDET_PING_RESP* resp = u_calloc(1, sizeof(struct NATDET_PING_RESP));
if (resp) {
resp->cmd = ETCP_ID_NAT_DETECTION;
resp->subcmd = NATDET_SUBCMD_PING_RESP;
resp->request_id = ctx->request_id;
resp->count_sent = ctx->count_sent;
resp->count_ok = ctx->count_ok;
resp->avg_rtt = avg_rtt;
struct ll_entry* e = queue_entry_new(0);
if (e) {
e->dgram = (uint8_t*)resp;
e->len = sizeof(struct NATDET_PING_RESP);
if (etcp_send(ctx->reply_conn, e) != 0) {
queue_dgram_free(e); queue_entry_free(e);
} else {
DEBUG_INFO(DEBUG_CATEGORY_BGP,
"PING series done %s request_id=%08x sent=%u ok=%u avg_rtt=%u",
ctx->reply_conn->log_name,
(unsigned)ctx->request_id,
(unsigned)ctx->count_sent,
(unsigned)ctx->count_ok,
(unsigned)avg_rtt);
}
} else {
u_free(resp);
}
}
u_free(ctx);
void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t round) {
memcpy(out, "NAT2UDP!", 8); out[8] = socket->sock_id;
write64(out + 9, socket->generation); write64(out + 17, round);
}
/* Callback одного пинга из серии */
static void route_ping_single_cb(int success,
uint16_t rtt,
void* arg,
uint64_t nonce,
const uint8_t* resp_data,
size_t resp_data_len) {
(void)nonce; (void)resp_data; (void)resp_data_len;
struct route_ping_series_ctx* ctx = (struct route_ping_series_ctx*)arg;
if (!ctx) return;
/* Обновляем статистику */
ctx->count_sent++;
if (success) {
ctx->count_ok++;
ctx->sum_rtt += rtt;
}
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "ping cb: ok=%d rtt=%u sent=%d/%d nonce=%016llx",
success, rtt, ctx->count_sent, ctx->count_total, (unsigned long long)nonce);
/* Если ещё не все пакеты отправлены — сразу шлём следующий */
if (ctx->count_sent < ctx->count_total) {
int ret = etcp_send_ping_to_socket(
ctx->reply_conn->instance,
ctx->local_sock,
ctx->pubkey,
&ctx->target_addr,
ctx->timeout_ms,
route_ping_single_cb,
ctx,
NULL, 0, 0);
if (ret != 0) {
/* Не смогли отправить следующий — завершаем серию досрочно */
route_ping_series_finish(ctx);
}
/* else: следующий пинг запущен, ждём его callback */
} else {
/* Все пакеты обработаны */
route_ping_series_finish(ctx);
}
int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE],
const struct sockaddr_storage *peer_address, uint64_t round, int timeout_ms,
etcp_ping_callback_t callback, void *arg) {
if (!socket || !socket->generation || !round) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "observe: invalid endpoint/round"); return -1; }
uint8_t tag[ROUTE_OBSERVE_TAG_SIZE]; route_ping_make_tag(tag, socket, round);
return etcp_send_ping_to_socket(socket->instance, socket, peer_key, peer_address, timeout_ms,
callback, arg, tag, sizeof(tag), ETCP_PING_FLAG_OBSERVE);
}
// Обработчик PING_REQ (нам запросили "пропингуй узел и верни результат")
void route_ping_handle_req(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* from_conn,
const uint8_t* data,
size_t len) {
if (!from_conn || !nd || !data || len < offsetof(struct NATDET_PING_REQ, pubkey)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "ping req: bad args len=%zu", len);
return;
}
const struct NATDET_PING_REQ* req_pkt = (const struct NATDET_PING_REQ*)data;
/* Создаём контекст серии */
struct route_ping_series_ctx* ctx = u_calloc(1, sizeof(struct route_ping_series_ctx));
if (!ctx) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "ping req: alloc ctx failed");
return;
}
ctx->reply_conn = from_conn;
ctx->request_id = req_pkt->request_id;
ctx->count_total = req_pkt->count;
ctx->timeout_ms = req_pkt->timeout_ms;
{
if (group) {
struct TOPO_GROUP_NODE* req_nq = topo_node_find_by_id(group, from_conn->peer_node_id);
if (req_nq) req_nq->connectivity.ping_req_time = get_time_tb();
}
}
if (len >= sizeof(struct NATDET_PING_REQ)) {
memcpy(ctx->pubkey, req_pkt->pubkey, SC_PUBKEY_SIZE);
int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket,
uint64_t round, struct sockaddr_storage *observed) {
if (!data || !socket || length != ROUTE_ADDR_WIRE_SIZE + ROUTE_OBSERVE_TAG_SIZE) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "observation: invalid size=%zu", length); return -1;
}
/* Целевой адрес - в sockaddr храним в network byte order (как в пакете) */
struct sockaddr_in* sin = (struct sockaddr_in*)&ctx->target_addr;
sin->sin_family = AF_INET;
memcpy(&sin->sin_addr.s_addr, req_pkt->target_ipv4, 4); /* network byte order */
sin->sin_port = req_pkt->target_port; /* порт в network byte order */
/* Ищем первый IPv4-сокет */
struct ETCP_SOCKET* ls = (group ? group->instance : nd->inst)->etcp_sockets;
while (ls) {
if (ls->local_addr.ss_family == AF_INET) {
ctx->local_sock = ls;
break;
}
ls = ls->next;
}
if (!ctx->local_sock) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "ping req: no IPv4 socket");
route_ping_series_finish(ctx); /* отправит 0/0 */
return;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "PING series start request_id=%08x target=%s:%u count=%u timeout=%u",
(unsigned)ctx->request_id, ip_to_str(&sin->sin_addr, AF_INET).str,
ntohs(sin->sin_port), (unsigned)ctx->count_total, (unsigned)ctx->timeout_ms);
/* Запускаем первый пинг (дальше цепочка через callback) */
int ret = etcp_send_ping_to_socket(
(group ? group->instance : nd->inst),
ctx->local_sock,
ctx->pubkey,
&ctx->target_addr,
ctx->timeout_ms,
route_ping_single_cb,
ctx,
NULL, 0, 0);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "ping req: cannot start first ping");
route_ping_series_finish(ctx);
uint8_t tag[ROUTE_OBSERVE_TAG_SIZE]; route_ping_make_tag(tag, socket, round);
if (memcmp(data + ROUTE_ADDR_WIRE_SIZE, tag, sizeof(tag))) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "observation: stale endpoint generation/round"); return -1;
}
return route_ping_addr_decode(data, observed);
}

63
src/routing_layer/route_ping.h

@ -1,56 +1,33 @@
/**
* @file route_ping.h
* @brief Удалённый пинг через BGP-посредника.
*
* A просит B пропинговать C и вернуть сводную статистику:
* A --PING_REQ--> B --ETCP ping--> C
* A <--PING_RESP-- B (sent/ok/avg_rtt)
*
* Промежуточный узел задаётся ЯВНО в to_conn — вызывающий сам выбирает,
* через кого слать запрос. Автоподбор посредника (nat_check_find_third_node)
* находится в nat_detection.c, не здесь.
*/
/* Прямые UDP-наблюдения с указанного рабочего сокета. Управление помощниками —
* nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет. */
#ifndef ROUTE_PING_H
#define ROUTE_PING_H
#include <stddef.h>
#include <stdint.h>
#include "etcp_connections.h"
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include "nat_detection.h"
/** Запросить у to_conn серию пингов на target_ip:target_port.
* cb вызывается всегда ровно 1 раз:
* success=1 если хоть один пинг успешен (PONG получен)
* success=0 при общем таймауте или все пинги провалились
* @param to_conn промежуточный узел — выбирается вызывающим явно */
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* to_conn,
uint32_t target_ip, uint16_t target_port,
uint8_t count, uint16_t interval_ms, uint16_t timeout_ms,
uint16_t wait_timeout_ms, route_ping_callback_t cb,
void* arg, const uint8_t* pubkey);
#define ROUTE_ADDR_WIRE_SIZE 19
#define ROUTE_OBSERVE_TAG_SIZE 25
/** Отменить все ожидающие ответы (при остановке NAT_DETECTION). */
void route_ping_destroy_pending(struct NAT_DETECTION* nd);
int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address);
int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address);
int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port);
/** Отменить pending для conn (при разрыве/реините соединения). */
void route_ping_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn);
/** Отменить pending для конкретного линка (при закрытии линка без разрыва conn). */
void route_ping_cancel_for_link(struct NAT_DETECTION* nd, struct ETCP_LINK* link);
/** Входящий PING_REQ — мы посредник: пингуем цель, шлём PING_RESP. */
void route_ping_handle_req(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* from_conn, const uint8_t* data, size_t len);
/** Входящий PING_RESP — мы инициатор: находим pending по request_id, вызываем cb. */
void route_ping_handle_resp(struct NAT_DETECTION* nd,
struct ETCP_CONN* from_conn, const uint8_t* data, size_t len);
/* Ответ: address(19) || tag(25), все числа BE, IP — неизменённые сетевые байты. */
int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE],
const struct sockaddr_storage *peer_address, uint64_t round, int timeout_ms,
etcp_ping_callback_t callback, void *arg);
int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket,
uint64_t round, struct sockaddr_storage *observed);
void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t round);
#ifdef __cplusplus
}
#endif
#endif // ROUTE_PING_H
#endif

90
src/routing_layer/route_ping_doc.md

@ -1,62 +1,28 @@
# route_ping — удалённый пинг через BGP (NAT-детекция, проверка живости)
## 1. Назначение
Механизм запроса удалённого пинга через BGP-канал. Узел A отправляет узлу B TOPO_PING_REQ
с IP:port цели, узел B выполняет серию пингов (`etcp_send_ping_to_socket`) и возвращает
TOPO_PING_RESP с результатами (count_ok, avg_rtt).
Используется:
- `route_connectivity` — для зондирования связности (прямые пинги, без BGP)
- `topo_group` — для NAT-детекции через промежуточный узел (third-party ping)
- Общий механизм проверки живости адресов
## 2. Как пользоваться
```c
// Отправить запрос пинга адреса target_ip:target_port через промежуточный узел to_conn
int ret = route_ping_send_req_addr(group, to_conn,
target_ip, target_port,
count=2, interval_ms=100, timeout_ms=500,
wait_timeout_ms=2000, callback, arg, pubkey);
```
Промежуточный узел (to_conn) получает запрос → выполняет серию `etcp_send_ping_to_socket()`
→ собирает статистику → отправляет ответ.
Ключевой нюанс: `wait_timeout_ms` задаёт таймаут ожидания ответа на локальной стороне.
Если ответ не приходит вовремя, `route_ping_pending_timeout` вызывает callback с `success=0`.
## 3. API
### Протокольные структуры (packed, wire-format)
| Структура | Поля |
|-----------|------|
| `TOPO_PING_REQ` | `cmd=ETCP_ID_TOPO_ENTRY`, `subcmd=0x07`, `request_id`, `count`, `interval_ms`, `timeout_ms`, `target_ipv4`, `target_port`, `pubkey` (опционально) |
| `TOPO_PING_RESP` | `cmd`, `subcmd=0x08`, `request_id`, `count_sent`, `count_ok`, `avg_rtt` (0.1ms) |
### Внутренние структуры
| Структура | Назначение |
|-----------|------------|
| `route_ping_series_ctx` | Контекст серии пингов (на отвечающей стороне): `reply_conn`, `request_id`, `target_addr`, `local_sock`, счётчики, статистика |
| `route_ping_pending` | Ожидающий запрос (на запрашивающей стороне): `request_id`, `callback`, `timeout_timer`, `via_conn` |
### Функции
| Функция | Назначение |
|---------|------------|
| `route_ping_send_req_addr(group, to_conn, target_ip, target_port, count, interval, timeout, wait_timeout, cb, arg, pubkey)` | Отправить TOPO_PING_REQ через BGP, зарегистрировать callback |
| `route_ping_handle_req(group, from_conn, data, len)` | Обработать входящий PING_REQ: найти локальный IPv4-сокет, запустить серию `etcp_send_ping_to_socket()`, по завершении отправить PING_RESP |
| `route_ping_handle_resp(group, from_conn, data, len)` | Обработать PING_RESP: найти pending по `request_id`, отменить таймер, вызвать callback |
| `route_ping_destroy_pending(group)` | Очистить весь список pending (при уничтожении BGP) |
| `route_ping_cancel_for_conn(group, conn)` | Отменить pending запросы, связанные с конкретным `ETCP_CONN` (при разрыве) |
### Callback
```c
typedef void (*route_ping_callback_t)(int success, uint16_t avg_rtt,
uint8_t count_sent, uint8_t count_ok, void* arg);
```
Вызывается при получении ответа или по таймауту (`success=0`).
### Зависимости
- `src/topo_group.h` — BGP-канал (список `ping_pending`, `next_ping_req_id`)
- `src/etcp_api.h` — `etcp_send()` для отправки через BGP
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` для собственно пинга
- `src/topo_node.h` — `topo_node_find_by_id()`
# route_ping — наблюдение рабочего UDP endpoint
Модуль кодирует адреса и выполняет прямое UDP-наблюдение через конкретный
`ETCP_SOCKET`. Выбор B/C, пересылка управляющих сообщений и классификация
профиля принадлежат `nat_detection`; алгоритм — в `doc/nat_detection.txt`.
`route_ping_observe(socket, peer_key, peer_address, round, timeout_ms, callback, arg)`
передаёт PING с `ETCP_PING_FLAG_OBSERVE`. PONG содержит фактически наблюдаемый
source IP:port и исходный тег. Профиль не меняется от единственного ответа.
`route_ping_observation` проверяет размер, ID/поколение сокета и round.
Адрес занимает 19 байт: family(1: 4/6), IP(16: IPv4 + 12 нулей либо IPv6),
port(2, BE). Тег занимает 25 байт: `NAT2UDP!`(8), socket_id(1),
generation(8, BE), round(8, BE). Неправильные family, скрытые IPv4-байты
и нулевой порт отвергаются. Новый wire не использует native packed integers.
В транспорте nonce генерируется CSPRNG; PONG должен соответствовать ожидаемому
ключу, source IP:port и принимающему socket generation. Pending снимается
до callback. `etcp_cancel_pings(instance, callback, arg)` отменяет только
совпадающие запросы без callback; arg остаётся собственностью вызывающего.
`ETCP_PING_FLAG_ADDRESS_CHECK` обозначает прямое входящее испытание. Его
receipt фиксируется на рабочем сокете владельца до отправки PONG. Потеря
обратного PONG поэтому не превращает полученный ingress в отрицательный результат.
UDP-результаты не используются для TCP listener и SOCKS5 endpoint.
Проверки: `tests/test_route_ping.c`, `tests/test_nat_detection.c`,
изолированный стенд `tests/stream_netns/run.py --nat-only`.

97
src/routing_layer/sock_match.c

@ -1,4 +1,3 @@
/* sock_match.c — классификация сокетов и сопоставление адресов при подключении */
#include "sock_match.h"
#include <string.h>
@ -8,66 +7,48 @@
#include "etcp_connections.h"
#include "config_parser.h"
#include "topo_node.h"
/* Нормализация NAT-типа: сырые (NAT_TYPE_*) или проверенные (NAT_VERIFIED_*) значения. */
static int nat_is_eim(uint8_t n) { return n == NAT_TYPE_EIM || n == NAT_VERIFIED_EIM; }
static int nat_is_direct(uint8_t n) { return n == NAT_TYPE_DIRECT || n == NAT_VERIFIED_DIRECT; }
uint8_t sock_target_class(uint8_t a_type, uint8_t tcfg, uint8_t tnat) {
if (tcfg == CFG_SERVER_TYPE_PUBLIC || tcfg == CFG_SERVER_TYPE_UNKNOWN) return SOCK_TC_PUB;
if (tcfg == CFG_SERVER_TYPE_PRIVATE || tcfg == CFG_SERVER_TYPE_LOCAL) return SOCK_TC_PRIV;
/* CFG_SERVER_TYPE_NAT */
if (nat_is_eim(tnat) || nat_is_direct(tnat))
return (a_type == TOPO_ADDR_INTERFACE) ? SOCK_TC_PRIV : SOCK_TC_PUB; /* EIM=direct: NAT-адрес публичный, INTERFACE — приватный */
return (a_type == TOPO_ADDR_INTERFACE) ? SOCK_TC_PRIV : SOCK_TC_UNREACH; /* strict/unknown: публичный адрес не пробиваем */
#include "ntp_time.h"
uint8_t sock_target_class(uint8_t address_type, uint8_t config_type, const struct address_profile *profile,
int public_address, uint64_t now_us) {
if (!public_address || config_type == CFG_SERVER_TYPE_PRIVATE || config_type == CFG_SERVER_TYPE_LOCAL) return SOCK_TC_PRIV;
if (address_type == TOPO_ADDR_NAT && profile && profile->mapping == ADDRESS_MAP_DEPENDENT) return SOCK_TC_UNREACH;
if (!address_profile_fresh(profile, now_us)) return SOCK_TC_PROBE;
if (profile->inbound == ADDRESS_IN_OPEN) return SOCK_TC_PUB;
if (profile->inbound == ADDRESS_IN_CONTACT) return SOCK_TC_CONTACT;
return SOCK_TC_PROBE;
}
int sock_match(const struct sock_view* v, uint8_t a_type, uint8_t tcfg, uint8_t tnat, uint32_t target_ip4) {
uint8_t tc = sock_target_class(a_type, tcfg, tnat);
if (tc == SOCK_TC_UNREACH) return 0; /* strict/unknown NAT-адрес — напрямую не пробьём */
if (tc == SOCK_TC_PUB) {
if (!v->has_inet) return 0; /* у нашего сокета нет интернета */
if (a_type == TOPO_ADDR_NAT && v->is_eim && nat_is_eim(tnat)
&& v->public_ip4 && v->public_ip4 == target_ip4) return 0; /* оба EIM за одним NAT — hairpin */
return 1;
}
return v->has_priv; /* приватный адрес — только с нашего приватного интерфейса */
int sock_match(const struct sock_view *view, uint8_t address_type, uint8_t config_type,
const struct address_profile *profile, uint32_t target_ip4) {
if (!view) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "socket match: missing local view"); return 0; }
uint8_t target = sock_target_class(address_type, config_type, profile, target_ip4 && ip_is_public(target_ip4), view->now_us);
if (target == SOCK_TC_UNREACH) return 0;
/* Одинаковый внешний IP не запрещает hairpin и не доказывает общую LAN.
Учитываем маршруты при фактической попытке, затем проверяем ключ пира. */
return target == SOCK_TC_PRIV ? view->has_priv : view->has_inet;
}
int sock_collect_views(struct UTUN_INSTANCE* inst, struct sock_view* out, int max) {
if (!inst || !out || max <= 0) return 0;
int n = 0;
struct ETCP_SOCKET* s = inst->etcp_sockets;
while (s && n < max) {
if (s->local_addr.ss_family != AF_INET) { s = s->next; continue; }
struct sock_view* v = &out[n++];
v->sock = s;
v->family = AF_INET;
v->is_tcp = s->is_tcp;
v->local_ip4 = 0; v->public_ip4 = 0;
/* Локальный адрес (interface, иначе local). */
{ const struct sockaddr_storage* ia = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
if (ia && ia->ss_family == AF_INET) v->local_ip4 = ((const struct sockaddr_in*)ia)->sin_addr.s_addr; }
/* Публичный адрес (NAT): nat_addr, иначе локальный если он публичный. */
{ const struct sockaddr_in* na = (const struct sockaddr_in*)&s->nat_addr;
if (s->nat_addr.ss_family == AF_INET && na->sin_addr.s_addr != 0)
v->public_ip4 = na->sin_addr.s_addr; }
/* Классификация по адресу, а не по type из конфига:
* приватный локальный адрес → можем ходить в LAN (has_priv);
* исходящие считаем доступными (интернет + локалка). */
int local_public = v->local_ip4 && ip_is_public(v->local_ip4);
v->has_priv = (uint8_t)!local_public;
v->has_inet = 1;
/* NAT-результат (главный критерий если есть) — для hairpin-фильтра. */
v->is_eim = (uint8_t)nat_is_eim(s->nat_type);
if (!v->public_ip4 && local_public) v->public_ip4 = v->local_ip4;
v->role = local_public ? SOCK_ROLE_DIRECT : (v->is_eim ? SOCK_ROLE_DIRECT : SOCK_ROLE_NAT);
s = s->next;
int sock_collect_views(struct UTUN_INSTANCE *instance, struct sock_view *out, int max) {
if (!instance || !out || max <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "socket views: invalid arguments"); return 0; }
int count = 0;
int64_t time = ntp_time_get_us(instance);
for (struct ETCP_SOCKET *socket = instance->etcp_sockets; socket && count < max; socket = socket->next) {
if (socket->probe_only || socket->local_addr.ss_family != AF_INET) continue;
struct sock_view *view = &out[count++]; memset(view, 0, sizeof(*view));
view->sock = socket; view->family = AF_INET; view->is_tcp = socket->is_tcp;
view->now_us = time > 0 ? (uint64_t)time : 0;
const struct sockaddr_storage *local = socket->is_tcp ? &socket->interface_addr : &socket->local_addr;
if (local->ss_family == AF_INET) view->local_ip4 = ((const struct sockaddr_in *)local)->sin_addr.s_addr;
if (socket->nat_addr.ss_family == AF_INET)
view->public_ip4 = ((const struct sockaddr_in *)&socket->nat_addr)->sin_addr.s_addr;
if (!view->public_ip4 && ip_is_public(view->local_ip4)) view->public_ip4 = view->local_ip4;
view->address_profile = socket->address_profile;
view->has_priv = 1; /* LAN-кандидат проверяется реальной маршрутизацией и handshake. */
view->has_inet = socket->type != CFG_SERVER_TYPE_LOCAL;
view->role = !view->has_inet ? SOCK_ROLE_LOCAL :
address_profile_fresh(&view->address_profile, view->now_us) && view->address_profile.inbound == ADDRESS_IN_OPEN ?
SOCK_ROLE_DIRECT : SOCK_ROLE_OUTGOING;
}
return n;
return count;
}

108
src/routing_layer/sock_match.h

@ -1,100 +1,38 @@
/**
* @file sock_match.h
* @brief Классификация сокетов и сопоставление адресов при подключении.
*
* ─── Общая идея ────────────────────────────────────────────────────────────
* Узел имеет несколько сокетов с разной природой сети (публичный IP, NAT,
* скрытый/приватный, без интернета). Чтобы при подключении не создавать
* заведомо мёртвые линки, каждый наш сокет сводится к одной из трёх РОЛЕЙ,
* а каждый адрес пира — к одному из трёх КЛАССОВ. Соединение пробуем только
* для совместимых пар «наш сокет → адрес пира». Для удобства формируем
* список наших сокетов в плоском виде (struct sock_view): роль, локальный IP
* (interface), публичный IP (NAT) и флаги достижимости.
*
* ─── Роли нашего сокета (что он умеет как ИСТОЧНИК) ───────────────────────
* DIRECT — публичный IP / EIM NAT / verified-direct: интернет в обе стороны.
* NAT — только исходящие (strict/unknown NAT или скрытый private):
* интернет есть, но входящих соединений нет.
* LOCAL — без интернета: type=local из конфига, либо любой сокет при
* недоступном NTP (узел оффлайн). Работает только в LAN.
*
* EIM-сокет приравнивается к DIRECT (endpoint-independent mapping пробивается
* снаружи, поэтому его публичный адрес достижим как у public).
*
* ─── Реклама адресов (что узел публикует в nodeinfo) ──────────────────────
* PUBLIC → свой публичный IP (INTERFACE).
* NAT + EIM → приватный IP (INTERFACE) + публичный IP (NAT).
* NAT + STRICT → только приватный IP (INTERFACE), публичный НЕ светим.
* LOCAL → только приватный IP (INTERFACE) — для связи двух клиентов
* в одной локалке.
* PRIVATE → скрыт, не рекламируется вовсе (но usable как источник).
*
* ─── Сопоставление (наш сокет → адрес пира) ───────────────────────────────
* Адрес пира классифицируется: PUB (публичный, из интернета), PRIV
* (приватный/LAN), UNREACH (напрямую недостижим). Правила:
* UNREACH → не пробуем (strict/unknown NAT-адрес);
* PUB → нужен интернет у нашего сокета (role != LOCAL), плюс
* hairpin-фильтр: если оба узла EIM за одним NAT (одинаковый
* публичный IP) — по публичному адресу не ходим, остаётся LAN;
* PRIV → только с нашего сокета, имеющего приватный интерфейс
* (has_priv) — приватный адрес достижим лишь из той же сети.
*/
/* Подбор кандидатов для прямого транспорта. PUBLIC в конфиге — политика,
* OPEN/CONTACT — измеренные свойства endpoint. Совместимость разрешает пробу;
* только аутентифицированный handshake подтверждает связь с нужным узлом. */
#ifndef SOCK_MATCH_H
#define SOCK_MATCH_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include "address_profile.h"
struct UTUN_INSTANCE;
struct ETCP_SOCKET;
/* Роли нашего сокета (как источника соединения) */
#define SOCK_ROLE_DIRECT 0 /* публичный / EIM / verified-direct — интернет в обе стороны */
#define SOCK_ROLE_NAT 1 /* только исходящие (strict/unknown NAT, скрытый private) */
#define SOCK_ROLE_LOCAL 2 /* без интернета (type=local или NTP недоступен) */
#define SOCK_ROLE_DIRECT 0
#define SOCK_ROLE_OUTGOING 1
#define SOCK_ROLE_LOCAL 2
/* Классы целевого адреса пира */
#define SOCK_TC_PUB 0 /* публичный — достижим из интернета */
#define SOCK_TC_PRIV 1 /* приватный / LAN */
#define SOCK_TC_UNREACH 2 /* напрямую недостижим (strict/unknown NAT) */
#define SOCK_TC_PUB 0
#define SOCK_TC_PRIV 1
#define SOCK_TC_UNREACH 2
#define SOCK_TC_CONTACT 3
#define SOCK_TC_PROBE 4
/**
* Плоское представление нашего сокета для сопоставления.
* "ip сокета" — local_ip4 (interface), "публичный" — public_ip4 (NAT/прямой),
* "тип" — role + флаги has_inet/has_priv/is_eim.
*
* Флаги вычисляются из КЛАССИФИКАЦИИ АДРЕСА и результата NAT-детекции,
* а не из type сокета в конфиге:
* has_priv — локальный IPv4 приватный/не-маршрутизируемый (можем ходить в LAN);
* has_inet — исходящие считаем доступными в интернет (всегда 1);
* is_eim — результат NAT-детекции EIM (для hairpin-фильтра).
*/
struct sock_view {
struct ETCP_SOCKET* sock;
uint8_t role; /* SOCK_ROLE_* (производное, для логов) */
uint8_t has_inet; /* 1 — интернет доступен */
uint8_t has_priv; /* 1 — есть приватный (LAN) интерфейс */
uint8_t is_eim; /* 1 — NAT + EIM (для hairpin) */
uint8_t family; /* AF_INET / AF_INET6 */
uint8_t is_tcp;
uint32_t local_ip4; /* interface_addr, network order (0 если нет) */
uint32_t public_ip4; /* nat_addr / interface если public, network order (0 если нет) */
struct ETCP_SOCKET *sock;
uint8_t role, has_inet, has_priv, family, is_tcp;
uint32_t local_ip4, public_ip4;
uint64_t now_us;
struct address_profile address_profile;
};
/* Класс адреса пира по типу адреса (TOPO_ADDR_*), конфиг-типу и NAT-типу сокета. */
uint8_t sock_target_class(uint8_t a_type, uint8_t tcfg, uint8_t tnat);
/* 1 — наш сокет (view) подходит для подключения к целевому адресу (target_ip4, network order). */
int sock_match(const struct sock_view* v, uint8_t a_type, uint8_t tcfg, uint8_t tnat, uint32_t target_ip4);
/* public_address — классификация IP в соответствующем семействе, не доказательство входа. */
uint8_t sock_target_class(uint8_t address_type, uint8_t config_type, const struct address_profile *profile,
int public_address, uint64_t now_us);
int sock_match(const struct sock_view *view, uint8_t address_type, uint8_t config_type,
const struct address_profile *profile, uint32_t target_ip4);
int sock_collect_views(struct UTUN_INSTANCE *instance, struct sock_view *out, int max);
/* Собирает список наших сокетов в плоском виде (v4). Возвращает число элементов. */
int sock_collect_views(struct UTUN_INSTANCE* inst, struct sock_view* out, int max);
#ifdef __cplusplus
}
#endif
#endif /* SOCK_MATCH_H */

150
src/routing_layer/topo_node.c

@ -91,7 +91,10 @@ void topo_node_apply_reality(struct ETCP_LINK* link, const struct TOPO_REALITY_S
prepend даёт одинаковый порядок при неизменном наборе сокетов). */
static int sockmeta4_list_equal(struct TOPO_SOCKMETA4* a, struct TOPO_SOCKMETA4* b) {
while (a && b) {
if (a->id != b->id || a->config_type != b->config_type || a->nat_type != b->nat_type) return 0;
if (a->id != b->id || a->config_type != b->config_type ||
a->address_profile.mapping != b->address_profile.mapping || a->address_profile.inbound != b->address_profile.inbound ||
a->address_profile.evidence_id != b->address_profile.evidence_id || a->address_profile.checked_at != b->address_profile.checked_at)
return 0;
a = a->next; b = b->next;
}
return a == NULL && b == NULL;
@ -106,7 +109,10 @@ static int addr4_list_equal(struct TOPO_ADDR4* a, struct TOPO_ADDR4* b) {
}
static int sockmeta6_list_equal(struct TOPO_SOCKMETA6* a, struct TOPO_SOCKMETA6* b) {
while (a && b) {
if (a->id != b->id || a->config_type != b->config_type || a->nat_type != b->nat_type) return 0;
if (a->id != b->id || a->config_type != b->config_type ||
a->address_profile.mapping != b->address_profile.mapping || a->address_profile.inbound != b->address_profile.inbound ||
a->address_profile.evidence_id != b->address_profile.evidence_id || a->address_profile.checked_at != b->address_profile.checked_at)
return 0;
a = a->next; b = b->next;
}
return a == NULL && b == NULL;
@ -339,6 +345,24 @@ void topo_node_registry_unref(struct TOPO_GROUPS* groups, uint64_t node_id) {
// ===== serialization =====
static void sockmeta_wire_write(uint8_t *out, uint8_t id, uint8_t config, const struct address_profile *profile) {
out[0] = id; out[1] = config; out[2] = profile->mapping; out[3] = profile->inbound;
for (unsigned i = 0; i < 8; i++) {
out[4 + i] = (uint8_t)(profile->evidence_id >> (56 - i * 8));
out[12 + i] = (uint8_t)(profile->checked_at >> (56 - i * 8));
}
}
static int sockmeta_wire_read(const uint8_t *in, struct address_profile *profile) {
memset(profile, 0, sizeof(*profile)); profile->mapping = in[2]; profile->inbound = in[3];
for (unsigned i = 0; i < 8; i++) {
profile->evidence_id = (profile->evidence_id << 8) | in[4 + i];
profile->checked_at = (profile->checked_at << 8) | in[12 + i];
}
if (!address_profile_valid(profile)) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid address profile in NODEINFO"); return -1; }
return 0;
}
/* Собирает канонический буфер для Ed25519-подписи узла (id, ver, pubkey, имя, тип, адреса). */
int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size) {
if (!ni || !buf || buf_size < 64 + 9) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "build_sig_msg: invalid args"); return -1; }
@ -356,13 +380,24 @@ int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size)
if (name_len) { memcpy(buf + off, ni->node_name, name_len); off += name_len; }
buf[off++] = '\0';
if (off + 2 > buf_size) return -1;
if (off + 7 > buf_size) return -1;
buf[off++] = ni->client_type;
buf[off++] = ni->client_activity;
/* Границы списков тоже подписаны: нельзя переразбить то же тело на другие секции. */
int counts[] = {topo_list_count((struct _topo_head *)ni->v4_sock_meta), topo_list_count((struct _topo_head *)ni->v4_addrs),
topo_list_count((struct _topo_head *)ni->v6_sock_meta), topo_list_count((struct _topo_head *)ni->v6_addrs),
topo_list_count((struct _topo_head *)ni->reality_socks)};
for (unsigned i = 0; i < sizeof(counts) / sizeof(counts[0]); i++) {
if (counts[i] > UINT8_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "signed node section exceeds wire limits"); return -1; }
buf[off++] = (uint8_t)counts[i];
}
{ struct TOPO_SOCKMETA4* e = ni->v4_sock_meta; while (e) {
if (off + 3 > buf_size) return -1;
buf[off++] = e->id; buf[off++] = e->config_type; buf[off++] = e->nat_type; e = e->next;
if (off + sizeof(struct TOPOMSG_SOCKMETA4) > buf_size || !address_profile_valid(&e->address_profile)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid/oversized v4 socket profile"); return -1;
}
sockmeta_wire_write(buf + off, e->id, e->config_type, &e->address_profile);
off += sizeof(struct TOPOMSG_SOCKMETA4); e = e->next;
}}
{ struct TOPO_ADDR4* e = ni->v4_addrs; while (e) {
if (off + 9 > buf_size) return -1;
@ -371,8 +406,11 @@ int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size)
buf[off++] = e->type; buf[off++] = e->socket_id; buf[off++] = e->protocol; e = e->next;
}}
{ struct TOPO_SOCKMETA6* e = ni->v6_sock_meta; while (e) {
if (off + 3 > buf_size) return -1;
buf[off++] = e->id; buf[off++] = e->config_type; buf[off++] = e->nat_type; e = e->next;
if (off + sizeof(struct TOPOMSG_SOCKMETA6) > buf_size || !address_profile_valid(&e->address_profile)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid/oversized v6 socket profile"); return -1;
}
sockmeta_wire_write(buf + off, e->id, e->config_type, &e->address_profile);
off += sizeof(struct TOPOMSG_SOCKMETA6); e = e->next;
}}
{ struct TOPO_ADDR6* e = ni->v6_addrs; while (e) {
if (off + 21 > buf_size) return -1;
@ -504,16 +542,16 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
if (msg.node_name_len) { memcpy(dp, ni->node_name, msg.node_name_len); dp += msg.node_name_len; }
{ struct TOPO_SOCKMETA4* e = ni->v4_sock_meta; while (e) {
struct TOPOMSG_SOCKMETA4 w = { .id=e->id, .config_type=e->config_type, .nat_type=e->nat_type };
memcpy(dp, &w, sizeof(w)); dp += sizeof(w); e = e->next;
sockmeta_wire_write(dp, e->id, e->config_type, &e->address_profile);
dp += sizeof(struct TOPOMSG_SOCKMETA4); e = e->next;
}}
{ struct TOPO_ADDR4* e = ni->v4_addrs; while (e) {
struct TOPOMSG_ADDR4 w = { .port=e->port, .type=e->type, .socket_id=e->socket_id, .protocol=e->protocol };
memcpy(w.addr, e->addr, 4); memcpy(dp, &w, sizeof(w)); dp += sizeof(w); e = e->next;
}}
{ struct TOPO_SOCKMETA6* e = ni->v6_sock_meta; while (e) {
struct TOPOMSG_SOCKMETA6 w = { .id=e->id, .config_type=e->config_type, .nat_type=e->nat_type };
memcpy(dp, &w, sizeof(w)); dp += sizeof(w); e = e->next;
sockmeta_wire_write(dp, e->id, e->config_type, &e->address_profile);
dp += sizeof(struct TOPOMSG_SOCKMETA6); e = e->next;
}}
{ struct TOPO_ADDR6* e = ni->v6_addrs; while (e) {
struct TOPOMSG_ADDR6 w = { .port=e->port, .type=e->type, .socket_id=e->socket_id, .protocol=e->protocol };
@ -582,7 +620,11 @@ int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t
const struct TOPOMSG_SOCKMETA4* w = (const struct TOPOMSG_SOCKMETA4*)dp; dp += sizeof(*w);
struct TOPO_SOCKMETA4* e = memory_pool_alloc(group->instance->topo_groups->v4_sock_meta_pool);
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
e->id = w->id; e->config_type = w->config_type; e->nat_type = w->nat_type; e->next = NULL;
e->id = w->id; e->config_type = w->config_type; e->next = NULL;
if (sockmeta_wire_read((const uint8_t *)w, &e->address_profile)) {
memory_pool_free(group->instance->topo_groups->v4_sock_meta_pool, e);
ni->v4_sock_meta = head; topo_node_free_raw(group->instance->topo_groups, ni); return -1;
}
if (!head) head = tail = e; else { tail->next = e; tail = e; }
}
ni->v4_sock_meta = head; }
@ -600,7 +642,11 @@ int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t
const struct TOPOMSG_SOCKMETA6* w = (const struct TOPOMSG_SOCKMETA6*)dp; dp += sizeof(*w);
struct TOPO_SOCKMETA6* e = memory_pool_alloc(group->instance->topo_groups->v6_sock_meta_pool);
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
e->id = w->id; e->config_type = w->config_type; e->nat_type = w->nat_type; e->next = NULL;
e->id = w->id; e->config_type = w->config_type; e->next = NULL;
if (sockmeta_wire_read((const uint8_t *)w, &e->address_profile)) {
memory_pool_free(group->instance->topo_groups->v6_sock_meta_pool, e);
ni->v6_sock_meta = head; topo_node_free_raw(group->instance->topo_groups, ni); return -1;
}
if (!head) head = tail = e; else { tail->next = e; tail = e; }
}
ni->v6_sock_meta = head; }
@ -784,7 +830,10 @@ void topo_node_dump_all(struct TOPO_GROUP* group) {
int si = 0;
{ struct TOPO_SOCKMETA4* sm = ni->v4_sock_meta; while (sm) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, " v4sock[%d]: id=%u config=%u nat=%u", si++, sm->id, sm->config_type, sm->nat_type);
DEBUG_INFO(DEBUG_CATEGORY_BGP, " v4sock[%d]: id=%u config=%u mapping=%s inbound=%s evidence=%016llx checked=%llu",
si++, sm->id, sm->config_type, address_mapping_name(sm->address_profile.mapping),
address_inbound_name(sm->address_profile.inbound), (unsigned long long)sm->address_profile.evidence_id,
(unsigned long long)sm->address_profile.checked_at);
sm = sm->next;
}}
{ const char* atn[] = {"INTERFACE","NAT","REAL"};
@ -795,7 +844,10 @@ void topo_node_dump_all(struct TOPO_GROUP* group) {
a = a->next;
}}
{ int si6 = 0; struct TOPO_SOCKMETA6* sm6 = ni->v6_sock_meta; while (sm6) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, " v6sock[%d]: id=%u config=%u nat=%u", si6++, sm6->id, sm6->config_type, sm6->nat_type);
DEBUG_INFO(DEBUG_CATEGORY_BGP, " v6sock[%d]: id=%u config=%u mapping=%s inbound=%s evidence=%016llx checked=%llu",
si6++, sm6->id, sm6->config_type, address_mapping_name(sm6->address_profile.mapping),
address_inbound_name(sm6->address_profile.inbound), (unsigned long long)sm6->address_profile.evidence_id,
(unsigned long long)sm6->address_profile.checked_at);
sm6 = sm6->next;
}}
{ int ai6 = 0; struct TOPO_ADDR6* a6 = ni->v6_addrs; while (a6) {
@ -875,7 +927,9 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
FMT_ADD(" ed25519: "); for (int k = 0; k < SC_PUBKEY_SIZE; k++) FMT_ADD("%02x", ni->ed25519_public_key[k]); FMT_ADD("\n");
{ struct TOPO_SOCKMETA4* sm = ni->v4_sock_meta; int si = 0; while (sm) {
FMT_ADD(" v4sock[%d]: id=%u config=%u nat=%u\n", si++, sm->id, sm->config_type, sm->nat_type); sm = sm->next;
FMT_ADD(" v4sock[%d]: id=%u config=%u mapping=%s inbound=%s checked=%llu\n", si++, sm->id, sm->config_type,
address_mapping_name(sm->address_profile.mapping), address_inbound_name(sm->address_profile.inbound),
(unsigned long long)sm->address_profile.checked_at); sm = sm->next;
}}
{ const char* atn[] = {"INTERFACE","NAT","REAL"}; struct TOPO_ADDR4* a = ni->v4_addrs; int ai = 0; while (a) {
const char* proto = (a->protocol & TOPO_PROTO_REALITY) ? "tcp+reality" : (a->protocol & TOPO_PROTO_TCP) ? "tcp" : (a->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
@ -884,7 +938,9 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
a = a->next;
}}
{ struct TOPO_SOCKMETA6* sm6 = ni->v6_sock_meta; int s6i = 0; while (sm6) {
FMT_ADD(" v6sock[%d]: id=%u config=%u nat=%u\n", s6i++, sm6->id, sm6->config_type, sm6->nat_type); sm6 = sm6->next;
FMT_ADD(" v6sock[%d]: id=%u config=%u mapping=%s inbound=%s checked=%llu\n", s6i++, sm6->id, sm6->config_type,
address_mapping_name(sm6->address_profile.mapping), address_inbound_name(sm6->address_profile.inbound),
(unsigned long long)sm6->address_profile.checked_at); sm6 = sm6->next;
}}
{ struct TOPO_ADDR6* a6 = ni->v6_addrs; int a6i = 0; while (a6) {
const char* proto = (a6->protocol & TOPO_PROTO_REALITY) ? "tcp+reality" : (a6->protocol & TOPO_PROTO_TCP) ? "tcp" : (a6->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
@ -1098,11 +1154,12 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
struct ETCP_SOCKET* e_sock = instance->etcp_sockets;
while (e_sock) {
if (e_sock->is_tcp) { e_sock = e_sock->next; continue; }
if (e_sock->is_tcp || e_sock->probe_only) { e_sock = e_sock->next; continue; }
if (e_sock->type == CFG_SERVER_TYPE_PRIVATE) { e_sock = e_sock->next; continue; } /* private скрыт, LOCAL рекламируем (LAN) */
if (e_sock->local_addr.ss_family == AF_INET) {
{ struct TOPO_SOCKMETA4* sm = memory_pool_alloc(instance->topo_groups->v4_sock_meta_pool);
sm->id = e_sock->sock_id; sm->config_type = e_sock->type; sm->nat_type = e_sock->nat_type;
if (!sm) goto allocation_failed;
sm->id = e_sock->sock_id; sm->config_type = e_sock->type; sm->address_profile = e_sock->address_profile;
sm->next = m4_head; m4_head = sm; }
struct sockaddr_in* local_sin = (struct sockaddr_in*)&e_sock->local_addr;
struct sockaddr_in* if_sin = (struct sockaddr_in*)&e_sock->interface_addr;
@ -1112,17 +1169,17 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
uint32_t pre_ip = use_local ? local_sin->sin_addr.s_addr : if_sin->sin_addr.s_addr;
uint16_t pre_port = ntohs(use_local ? local_sin->sin_port : if_sin->sin_port);
{ struct TOPO_ADDR4* a = memory_pool_alloc(instance->topo_groups->v4_addr_pool);
if (!a) goto allocation_failed;
memcpy(a->addr, &pre_ip, 4); a->port = pre_port;
a->type = TOPO_ADDR_INTERFACE; a->socket_id = e_sock->sock_id;
a->protocol = TOPO_PROTO_UDP;
a->next = v4_head; v4_head = a; }
/* адрес ПОСЛЕ NAT — дополнительно, если NAT определён (EIM) и есть интернет.
DIRECT (nat==интерфейс), STRICT и оффлайн не рекламируем отдельным адресом. */
if (instance->ntp.reachable
&& e_sock->nat_addr.ss_family == AF_INET && nat_sin->sin_addr.s_addr != 0
&& e_sock->nat_type != NAT_VERIFIED_STRICT
&& e_sock->nat_type != NAT_VERIFIED_DIRECT) {
/* Общий внешний адрес публикует только владелец после наблюдений EIM.
Наличие ответа интернет-NTP не связано с работоспособностью этого endpoint. */
if (e_sock->nat_addr.ss_family == AF_INET && nat_sin->sin_addr.s_addr != 0 &&
e_sock->address_profile.mapping == ADDRESS_MAP_EIM) {
struct TOPO_ADDR4* a = memory_pool_alloc(instance->topo_groups->v4_addr_pool);
if (!a) goto allocation_failed;
memcpy(a->addr, &nat_sin->sin_addr.s_addr, 4); a->port = ntohs(nat_sin->sin_port);
a->type = TOPO_ADDR_NAT; a->socket_id = e_sock->sock_id;
a->protocol = TOPO_PROTO_UDP;
@ -1130,13 +1187,23 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
}
} else if (e_sock->local_addr.ss_family == AF_INET6) {
{ struct TOPO_SOCKMETA6* sm6 = memory_pool_alloc(instance->topo_groups->v6_sock_meta_pool);
sm6->id = e_sock->sock_id; sm6->config_type = e_sock->type; sm6->nat_type = e_sock->nat_type;
if (!sm6) goto allocation_failed;
sm6->id = e_sock->sock_id; sm6->config_type = e_sock->type; sm6->address_profile = e_sock->address_profile;
sm6->next = m6_head; m6_head = sm6; }
struct sockaddr_in6* if_sin6 = (struct sockaddr_in6*)&e_sock->interface_addr;
{ struct TOPO_ADDR6* a6 = memory_pool_alloc(instance->topo_groups->v6_addr_pool);
if (!a6) goto allocation_failed;
memcpy(a6->addr, &if_sin6->sin6_addr, 16); a6->port = ntohs(if_sin6->sin6_port);
a6->type = TOPO_ADDR_INTERFACE; a6->socket_id = e_sock->sock_id; a6->protocol = TOPO_PROTO_UDP;
a6->next = v6_head; v6_head = a6; }
if (e_sock->nat_addr.ss_family == AF_INET6 && e_sock->address_profile.mapping == ADDRESS_MAP_EIM) {
const struct sockaddr_in6 *mapped = (const struct sockaddr_in6 *)&e_sock->nat_addr;
struct TOPO_ADDR6 *a6 = memory_pool_alloc(instance->topo_groups->v6_addr_pool);
if (!a6) goto allocation_failed;
memcpy(a6->addr, &mapped->sin6_addr, 16); a6->port = ntohs(mapped->sin6_port);
a6->type = TOPO_ADDR_NAT; a6->socket_id = e_sock->sock_id; a6->protocol = TOPO_PROTO_UDP;
a6->next = v6_head; v6_head = a6;
}
}
e_sock = e_sock->next;
}
@ -1150,25 +1217,29 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
{ struct TOPO_SOCKMETA4* sm = memory_pool_alloc(instance->topo_groups->v4_sock_meta_pool);
sm->id = ts->sock_id; sm->config_type = ts->type; sm->nat_type = ts->nat_type;
if (!sm) goto allocation_failed;
sm->id = ts->sock_id; sm->config_type = ts->type; sm->address_profile = ts->address_profile;
sm->next = m4_head; m4_head = sm; }
{ struct TOPO_ADDR4* a = memory_pool_alloc(instance->topo_groups->v4_addr_pool);
if (!a) goto allocation_failed;
memcpy(a->addr, &sin->sin_addr.s_addr, 4); a->port = ntohs(sin->sin_port);
a->type = TOPO_ADDR_INTERFACE; a->socket_id = ts->sock_id; a->protocol = proto;
a->next = v4_head; v4_head = a; }
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
{ struct TOPO_SOCKMETA6* sm6 = memory_pool_alloc(instance->topo_groups->v6_sock_meta_pool);
sm6->id = ts->sock_id; sm6->config_type = ts->type; sm6->nat_type = ts->nat_type;
if (!sm6) goto allocation_failed;
sm6->id = ts->sock_id; sm6->config_type = ts->type; sm6->address_profile = ts->address_profile;
sm6->next = m6_head; m6_head = sm6; }
{ struct TOPO_ADDR6* a6 = memory_pool_alloc(instance->topo_groups->v6_addr_pool);
if (!a6) goto allocation_failed;
memcpy(a6->addr, &sin6->sin6_addr, 16); a6->port = ntohs(sin6->sin6_port);
a6->type = TOPO_ADDR_INTERFACE; a6->socket_id = ts->sock_id; a6->protocol = proto;
a6->next = v6_head; v6_head = a6; }
}
if (is_reality) {
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(struct TOPO_REALITY_SOCK));
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "reality_sock alloc failed"); }
if (!r) goto allocation_failed;
else {
r->socket_id = ts->sock_id;
memcpy(r->short_id, r_short_id, 8);
@ -1217,14 +1288,17 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my addresses unchanged (ver=%d), persist DB", ni->ver);
}
int database_failed = 0;
/* всегда: адреса (addrs_put) + классификация node_type/storage (nodeinfo_updated) */
if (instance->topo_sqlite_db) {
time_t now_sec = ntp_time_get_seconds(instance);
topo_node_sqlite_node_update_verified(instance->topo_sqlite_db, instance->node_id,
if (topo_node_sqlite_node_update_verified(instance->topo_sqlite_db, instance->node_id,
instance->name, instance->my_keys.public_key, instance->my_ed25519_pubkey,
(uint64_t)now_sec, now_sec);
topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, ni);
topo_node_sqlite_nodeinfo_updated(instance->topo_sqlite_db, instance->node_id);
(uint64_t)now_sec, now_sec) < 0 ||
topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, ni) < 0 ||
topo_node_sqlite_nodeinfo_updated(instance->topo_sqlite_db, instance->node_id) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local address projection update failed"); database_failed = 1;
}
}
if (changed) {
@ -1244,7 +1318,15 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
ge = ge->next;
}
}
return changed ? 1 : 0;
return database_failed ? -1 : changed ? 1 : 0;
allocation_failed:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local address snapshot allocation failed");
free_v4_sock_list(instance->topo_groups->v4_sock_meta_pool, m4_head);
free_v4_addr_list(instance->topo_groups->v4_addr_pool, v4_head);
free_v6_sock_list(instance->topo_groups->v6_sock_meta_pool, m6_head);
free_v6_addr_list(instance->topo_groups->v6_addr_pool, v6_head);
free_reality_sock_list(r_head);
return -1;
}
/* Callback изменения сокета (адрес/статус) — инициирует пересборку адресов своего узла. */

19
src/routing_layer/topo_node.h

@ -19,6 +19,7 @@ extern "C" {
#include <stddef.h>
#include "../lib/ll_queue.h"
#include "secure_channel.h"
#include "address_profile.h"
struct TOPO_GROUP;
struct TOPO_GROUPS;
@ -27,6 +28,7 @@ struct ETCP_LINK;
struct NODE_CONN_DIRECT;
struct ETCP_CONN;
struct UTUN_INSTANCE;
struct reality_client_config;
#define TOPO_ADDR_INTERFACE 0
#define TOPO_ADDR_NAT 1
@ -41,6 +43,7 @@ struct UTUN_INSTANCE;
#define ADDR_TYPE_NAT_EIM 2
#define ADDR_TYPE_NAT_STRICT 3
#define ADDR_TYPE_NAT 4 /* за NAT, тип неизвестен */
#define ADDR_TYPE_CONTACT 5 /* адрес не изменён, вход требует предварительного контакта */
#define PROBE_STATUS_NONE 0
#define PROBE_STATUS_IN_PROGRESS 1
@ -101,9 +104,15 @@ struct TOPO_CONNECTIVITY {
#define PING_TTL_TB (86400ULL * 10000ULL)
struct TOPOMSG_SOCKMETA4 { uint8_t id, config_type, nat_type; } __attribute__((packed));
struct TOPOMSG_SOCKMETA4 {
uint8_t id, config_type, mapping, inbound;
uint8_t evidence_id[8], checked_at[8];
} __attribute__((packed));
struct TOPOMSG_ADDR4 { uint8_t addr[4]; uint16_t port; uint8_t type, socket_id, protocol; } __attribute__((packed));
struct TOPOMSG_SOCKMETA6 { uint8_t id, config_type, nat_type; } __attribute__((packed));
struct TOPOMSG_SOCKMETA6 {
uint8_t id, config_type, mapping, inbound;
uint8_t evidence_id[8], checked_at[8];
} __attribute__((packed));
struct TOPOMSG_ADDR6 { uint8_t addr[16]; uint16_t port; uint8_t type, socket_id, protocol; } __attribute__((packed));
struct TOPOMSG_SUBNET4 { uint8_t addr[4]; uint8_t prefix_length; } __attribute__((packed));
struct TOPOMSG_SUBNET6 { uint8_t addr[16]; uint8_t prefix_length; } __attribute__((packed));
@ -134,9 +143,9 @@ struct TOPOMSG_NODE {
#define TOPOMSG_NODE_HDR_SIZE sizeof(struct TOPOMSG_NODE)
struct TOPO_SOCKMETA4 { struct TOPO_SOCKMETA4* next; uint8_t id, config_type, nat_type; };
struct TOPO_SOCKMETA4 { struct TOPO_SOCKMETA4* next; uint8_t id, config_type; struct address_profile address_profile; };
struct TOPO_ADDR4 { struct TOPO_ADDR4* next; uint8_t addr[4]; uint16_t port; uint8_t type, socket_id, protocol; };
struct TOPO_SOCKMETA6 { struct TOPO_SOCKMETA6* next; uint8_t id, config_type, nat_type; };
struct TOPO_SOCKMETA6 { struct TOPO_SOCKMETA6* next; uint8_t id, config_type; struct address_profile address_profile; };
struct TOPO_ADDR6 { struct TOPO_ADDR6* next; uint8_t addr[16]; uint16_t port; uint8_t type, socket_id, protocol; };
struct TOPO_SUBNET4 { struct TOPO_SUBNET4* next; uint8_t addr[4]; uint8_t prefix_length; };
struct TOPO_SUBNET6 { struct TOPO_SUBNET6* next; uint8_t addr[16]; uint8_t prefix_length; };
@ -269,7 +278,7 @@ void topo_node_ping_update_rtt(struct TOPO_GROUPS* groups, uint64_t node_id, uin
uint16_t topo_get_chain_rtt(struct TOPO_GROUP_NODE* nq);
/* Лимиты буфера подписываемых данных и полного сетевого NODEINFO. */
#define TOPO_SIG_MSG_MAX_SIZE 2048
#define TOPO_SIG_MSG_MAX_SIZE 8192
#define TOPO_NODE_WIRE_MAX_SIZE 16384
/* Канонические байты подписи: node_id, ver, timestamp, X25519-ключ, имя,
* тип/активность и полные списки сокетов/адресов/Reality. Длина или -1. */

227
src/routing_layer/topo_node_sqlite.c

@ -62,28 +62,29 @@ static int addr_type_from_ip(int family, const uint8_t* addr) {
}
/* Итоговая классификация: конфиг + автоопределение + IP (всё локально). */
static int addr_type_classify(int config_type, int nat_type) {
if (config_type == CFG_SERVER_TYPE_PUBLIC) return ADDR_TYPE_DIRECT;
static int addr_type_classify(int config_type, int global, const struct address_profile *profile) {
if (config_type == CFG_SERVER_TYPE_PRIVATE || config_type == CFG_SERVER_TYPE_LOCAL) return ADDR_TYPE_NETIF;
if (nat_type == NAT_TYPE_EIM) return ADDR_TYPE_NAT_EIM;
if (nat_type == NAT_TYPE_STRICT) return ADDR_TYPE_NAT_STRICT;
if (nat_type == NAT_TYPE_DIRECT) return ADDR_TYPE_DIRECT;
if (nat_type == NAT_VERIFIED_EIM) return ADDR_TYPE_NAT_EIM;
if (nat_type == NAT_VERIFIED_STRICT) return ADDR_TYPE_NAT_STRICT;
if (nat_type == NAT_VERIFIED_DIRECT) return ADDR_TYPE_DIRECT;
return ADDR_TYPE_NAT; /* за NAT, тип неизвестен */
if (!profile) return ADDR_TYPE_NAT;
if (profile->mapping == ADDRESS_MAP_EIM) return ADDR_TYPE_NAT_EIM;
if (profile->mapping == ADDRESS_MAP_DEPENDENT) return ADDR_TYPE_NAT_STRICT;
if (!global) return config_type == CFG_SERVER_TYPE_NAT ? ADDR_TYPE_NAT : ADDR_TYPE_NETIF;
if (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_OPEN) return ADDR_TYPE_DIRECT;
if (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_CONTACT) return ADDR_TYPE_CONTACT;
return ADDR_TYPE_NAT;
}
static void smeta4_lookup(struct TOPO_SOCKMETA4* head, uint8_t sid, uint8_t* cfg, uint8_t* nat) {
*cfg = CFG_SERVER_TYPE_UNKNOWN; *nat = NAT_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA4* m = head; m; m = m->next)
if (m->id == sid) { *cfg = m->config_type; *nat = m->nat_type; return; }
static const struct address_profile *smeta4_lookup(struct TOPO_SOCKMETA4 *head, uint8_t sid, uint8_t *config) {
*config = CFG_SERVER_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA4 *meta = head; meta; meta = meta->next)
if (meta->id == sid) { *config = meta->config_type; return &meta->address_profile; }
return NULL;
}
static void smeta6_lookup(struct TOPO_SOCKMETA6* head, uint8_t sid, uint8_t* cfg, uint8_t* nat) {
*cfg = CFG_SERVER_TYPE_UNKNOWN; *nat = NAT_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA6* m = head; m; m = m->next)
if (m->id == sid) { *cfg = m->config_type; *nat = m->nat_type; return; }
static const struct address_profile *smeta6_lookup(struct TOPO_SOCKMETA6 *head, uint8_t sid, uint8_t *config) {
*config = CFG_SERVER_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA6 *meta = head; meta; meta = meta->next)
if (meta->id == sid) { *config = meta->config_type; return &meta->address_profile; }
return NULL;
}
/* reality-параметры сокета → фиксированный blob для node_addresses.options.
@ -103,88 +104,76 @@ static int reality_opts_build(struct TOPO_NODE* ni, uint8_t sid, struct TOPO_ADD
/* ── Персистенция адресов узла (источник истины — BGP/topo_group) ── */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni) {
if (!db || !ni) return -1;
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
sqlite3_stmt* ds = NULL;
int deleted = 0;
if (sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &ds, NULL) == SQLITE_OK) {
sqlite3_bind_int64(ds, 1, (sqlite3_int64)node_id);
int drc = sqlite3_step(ds);
deleted = sqlite3_changes(db);
if (drc != SQLITE_DONE)
DEBUG_WARN(DEBUG_CATEGORY_BGP, "addrs_put: DELETE rc=%d (not DONE) node=%016llx: %s",
drc, (unsigned long long)node_id, sqlite3_errmsg(db));
sqlite3_finalize(ds);
} else {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "addrs_put: DELETE prepare failed node=%016llx: %s",
(unsigned long long)node_id, sqlite3_errmsg(db));
}
sqlite3_stmt* is = NULL;
if (sqlite3_prepare_v2(db,
"INSERT OR IGNORE INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type,nat_type,socket_id,options)"
" VALUES(?,?,?,?,?,?,?,?,?,?)", -1, &is, NULL) != SQLITE_OK) {
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
static int address_row_put(sqlite3_stmt *statement, struct TOPO_NODE *node, uint64_t node_id,
int family, const uint8_t *address, uint16_t port, uint8_t kind, uint8_t socket_id,
uint8_t protocol, uint8_t config, const struct address_profile *profile) {
static const struct address_profile unknown = {0};
if (!profile) profile = &unknown;
int summary = addr_type_classify(config, addr_type_from_ip(family, address) == ADDR_TYPE_DIRECT, profile);
struct TOPO_ADDR_REALITY_OPTS reality;
int has_reality = (protocol & TOPO_PROTO_REALITY) && reality_opts_build(node, socket_id, &reality);
sqlite3_bind_int64(statement, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(statement, 2, family); sqlite3_bind_int(statement, 3, protocol);
sqlite3_bind_blob(statement, 4, address, family == 4 ? 4 : 16, SQLITE_STATIC);
sqlite3_bind_int(statement, 5, port); sqlite3_bind_int(statement, 6, summary);
sqlite3_bind_int(statement, 7, config); sqlite3_bind_int(statement, 8, profile->mapping);
sqlite3_bind_int(statement, 9, socket_id);
if (has_reality) sqlite3_bind_blob(statement, 10, &reality, sizeof(reality), SQLITE_STATIC);
else sqlite3_bind_null(statement, 10);
sqlite3_bind_int(statement, 11, kind); sqlite3_bind_int(statement, 12, profile->inbound);
sqlite3_bind_int64(statement, 13, (sqlite3_int64)profile->evidence_id);
sqlite3_bind_int64(statement, 14, (sqlite3_int64)profile->checked_at);
int result = sqlite3_step(statement);
sqlite3_reset(statement);
if (result != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection insert failed node=%016llx family=%d socket=%u kind=%u"
" mapping=%s inbound=%s: %s", (unsigned long long)node_id, family, socket_id, kind,
address_mapping_name(profile->mapping), address_inbound_name(profile->inbound),
sqlite3_errmsg(sqlite3_db_handle(statement)));
return -1;
}
return 0;
}
int topo_node_sqlite_addrs_put(sqlite3 *db, uint64_t node_id, struct TOPO_NODE *node) {
if (!db || !node) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection: invalid arguments"); return -1; }
if (sqlite3_exec(db, "SAVEPOINT node_address_projection", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection savepoint failed: %s", sqlite3_errmsg(db)); return -1;
}
sqlite3_stmt *statement = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &statement, NULL) != SQLITE_OK) goto fail;
sqlite3_bind_int64(statement, 1, (sqlite3_int64)node_id);
if (sqlite3_step(statement) != SQLITE_DONE) goto fail;
sqlite3_finalize(statement); statement = NULL;
if (sqlite3_prepare_v2(db, "INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type,"
"nat_mapping,socket_id,options,address_kind,nat_inbound,nat_evidence,nat_checked)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)", -1, &statement, NULL) != SQLITE_OK) goto fail;
int written = 0;
struct TOPO_ADDR_REALITY_OPTS ropts;
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
uint8_t cfg, nat; smeta4_lookup(ni->v4_sock_meta, a->socket_id, &cfg, &nat);
int at = (a->type == TOPO_ADDR_INTERFACE) ? addr_type_from_ip(4, a->addr)
: addr_type_classify(cfg, nat);
int has_reality = (a->protocol & TOPO_PROTO_REALITY) && reality_opts_build(ni, a->socket_id, &ropts);
sqlite3_bind_int64(is, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(is, 2, 4);
sqlite3_bind_int(is, 3, (int)a->protocol);
sqlite3_bind_blob(is, 4, a->addr, 4, SQLITE_STATIC);
sqlite3_bind_int(is, 5, (int)a->port);
sqlite3_bind_int(is, 6, at);
sqlite3_bind_int(is, 7, (int)cfg);
sqlite3_bind_int(is, 8, (int)nat);
sqlite3_bind_int(is, 9, (int)a->socket_id);
if (has_reality) sqlite3_bind_blob(is, 10, &ropts, sizeof(ropts), SQLITE_STATIC);
else sqlite3_bind_null(is, 10);
if (sqlite3_step(is) != SQLITE_DONE)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "addrs_put: INSERT failed node=%016llx fam=4 sock=%d at=%d cfg=%d nat=%d addr=%d.%d.%d.%d:%d: %s",
(unsigned long long)node_id, a->socket_id, at, cfg, nat,
a->addr[0], a->addr[1], a->addr[2], a->addr[3], a->port, sqlite3_errmsg(db));
sqlite3_reset(is);
for (const struct TOPO_ADDR4 *address = node->v4_addrs; address; address = address->next) {
uint8_t config;
const struct address_profile *profile = smeta4_lookup(node->v4_sock_meta, address->socket_id, &config);
if (address_row_put(statement, node, node_id, 4, address->addr, address->port, address->type,
address->socket_id, address->protocol, config, profile)) goto fail;
written++;
}
for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) {
uint8_t cfg, nat; smeta6_lookup(ni->v6_sock_meta, a->socket_id, &cfg, &nat);
int at = (a->type == TOPO_ADDR_INTERFACE) ? ADDR_TYPE_NETIF
: addr_type_classify(cfg, nat);
int has_reality = (a->protocol & TOPO_PROTO_REALITY) && reality_opts_build(ni, a->socket_id, &ropts);
sqlite3_bind_int64(is, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(is, 2, 6);
sqlite3_bind_int(is, 3, (int)a->protocol);
sqlite3_bind_blob(is, 4, a->addr, 16, SQLITE_STATIC);
sqlite3_bind_int(is, 5, (int)a->port);
sqlite3_bind_int(is, 6, at);
sqlite3_bind_int(is, 7, (int)cfg);
sqlite3_bind_int(is, 8, (int)nat);
sqlite3_bind_int(is, 9, (int)a->socket_id);
if (has_reality) sqlite3_bind_blob(is, 10, &ropts, sizeof(ropts), SQLITE_STATIC);
else sqlite3_bind_null(is, 10);
if (sqlite3_step(is) != SQLITE_DONE)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "addrs_put: INSERT failed node=%016llx fam=6 sock=%d at=%d cfg=%d nat=%d: %s",
(unsigned long long)node_id, a->socket_id, at, cfg, nat, sqlite3_errmsg(db));
sqlite3_reset(is);
for (const struct TOPO_ADDR6 *address = node->v6_addrs; address; address = address->next) {
uint8_t config;
const struct address_profile *profile = smeta6_lookup(node->v6_sock_meta, address->socket_id, &config);
if (address_row_put(statement, node, node_id, 6, address->addr, address->port, address->type,
address->socket_id, address->protocol, config, profile)) goto fail;
written++;
}
sqlite3_finalize(is);
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "addrs_put: node=%016llx deleted=%d v4=%d v6=%d written=%d",
(unsigned long long)node_id, deleted,
topo_list_count((struct _topo_head*)ni->v4_addrs),
topo_list_count((struct _topo_head*)ni->v6_addrs), written);
sqlite3_finalize(statement); statement = NULL;
if (sqlite3_exec(db, "RELEASE node_address_projection", NULL, NULL, NULL) != SQLITE_OK) goto fail;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "address projection stored node=%016llx endpoints=%d",
(unsigned long long)node_id, written);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection failed node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db));
sqlite3_finalize(statement);
if (sqlite3_exec(db, "ROLLBACK TO node_address_projection; RELEASE node_address_projection", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection rollback failed: %s", sqlite3_errmsg(db));
return -1;
}
int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id) {
@ -252,6 +241,29 @@ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
int topo_node_sqlite_init(sqlite3* db) {
if (!db) return -1;
/* Проекция адресов производная. Старый формат пересоздаём, не интерпретируя NAT_TYPE_* как новый профиль. */
sqlite3_stmt *schema = NULL;
if (sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='node_addresses'",
-1, &schema, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema inspection failed: %s", sqlite3_errmsg(db)); return -1;
}
int exists = sqlite3_step(schema) == SQLITE_ROW;
sqlite3_finalize(schema); schema = NULL;
if (exists) {
int compatible = sqlite3_prepare_v2(db,
"SELECT nat_mapping,nat_inbound,nat_evidence,nat_checked,address_kind FROM node_addresses LIMIT 0",
-1, &schema, NULL);
sqlite3_finalize(schema);
if (compatible == SQLITE_ERROR) {
if (sqlite3_exec(db, "DROP TABLE node_addresses", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "old address projection reset failed: %s", sqlite3_errmsg(db)); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "old address projection reset; signed NODEINFO will rebuild endpoint profiles");
} else if (compatible != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema read failed: %s", sqlite3_errmsg(db)); return -1;
}
}
const char* sql =
"CREATE TABLE IF NOT EXISTS node_snapshots ("
"node_id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, record BLOB NOT NULL);"
@ -277,13 +289,17 @@ int topo_node_sqlite_init(sqlite3* db) {
" rtt INTEGER,"
" addr_type INTEGER DEFAULT 0,"
" config_type INTEGER DEFAULT 0,"
" nat_type INTEGER DEFAULT 0,"
" nat_mapping INTEGER NOT NULL DEFAULT 0,"
" nat_inbound INTEGER NOT NULL DEFAULT 0,"
" nat_evidence INTEGER NOT NULL DEFAULT 0,"
" nat_checked INTEGER NOT NULL DEFAULT 0,"
" address_kind INTEGER NOT NULL DEFAULT 0,"
" socket_id INTEGER NOT NULL DEFAULT 0,"
" options BLOB," // reality-параметры (TOPO_ADDR_REALITY_OPTS)
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_na_unique ON node_addresses(node_id, family, socket_id, addr_type);"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_na_unique ON node_addresses(node_id,family,socket_id,protocol,address,port,address_kind);"
"CREATE INDEX IF NOT EXISTS idx_na_addr_lookup ON node_addresses(family, address, port, socket_id);"
"CREATE TABLE IF NOT EXISTS channels ("
@ -863,8 +879,11 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) {
}
}
static const int at2type[] = { [ADDR_TYPE_NETIF]=1, [ADDR_TYPE_DIRECT]=1, [ADDR_TYPE_NAT_EIM]=2, [ADDR_TYPE_NAT_STRICT]=1, [ADDR_TYPE_NAT]=2 };
int base_type = (best_at < 0) ? 0 : at2type[best_at];
static const int at2type[] = { [ADDR_TYPE_NETIF]=1, [ADDR_TYPE_DIRECT]=1, [ADDR_TYPE_NAT_EIM]=2,
[ADDR_TYPE_NAT_STRICT]=1, [ADDR_TYPE_NAT]=2, [ADDR_TYPE_CONTACT]=2 };
int base_type = best_at < 0 || (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]) ? 0 : at2type[best_at];
if (best_at >= 0 && (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]))
DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid projected address type=%d node=%016llx", best_at, (unsigned long long)node_id);
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
@ -1054,11 +1073,10 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
struct TOPO_ADDR4* v4_head = NULL; struct TOPO_ADDR4* v4_tail = NULL;
struct TOPO_SOCKMETA4* m4_head = NULL; struct TOPO_SOCKMETA4* m4_tail = NULL;
struct TOPO_REALITY_SOCK* r_head = NULL; struct TOPO_REALITY_SOCK* r_tail = NULL;
uint8_t sock_id_counter = 0;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type,options FROM node_addresses"
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses"
" WHERE node_id=? AND family=4 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
@ -1066,10 +1084,9 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 4) continue;
int port = sqlite3_column_int(s, 1);
int at = sqlite3_column_int(s, 2);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)(sock_id_raw ? sock_id_raw : ++sock_id_counter);
uint8_t type = (at == ADDR_TYPE_NETIF) ? TOPO_ADDR_INTERFACE : TOPO_ADDR_NAT;
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool);
if (!a4) continue;
memset(a4, 0, sizeof(*a4));
@ -1086,7 +1103,8 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
if (m4) {
m4->id = sock_id;
m4->config_type = (uint8_t)sqlite3_column_int(s, 5);
m4->nat_type = (uint8_t)sqlite3_column_int(s, 6);
/* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */
memset(&m4->address_profile, 0, sizeof(m4->address_profile));
m4->next = NULL;
if (!m4_head) m4_head = m4_tail = m4;
else { m4_tail->next = m4; m4_tail = m4; }
@ -1118,7 +1136,7 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type,options FROM node_addresses"
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses"
" WHERE node_id=? AND family=6 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
@ -1126,10 +1144,9 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 16) continue;
int port = sqlite3_column_int(s, 1);
int at = sqlite3_column_int(s, 2);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)(sock_id_raw ? sock_id_raw : ++sock_id_counter);
uint8_t type = (at == ADDR_TYPE_NETIF) ? TOPO_ADDR_INTERFACE : TOPO_ADDR_NAT;
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool);
if (!a6) continue;
memset(a6, 0, sizeof(*a6));
@ -1146,7 +1163,7 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
if (m6) {
m6->id = sock_id;
m6->config_type = (uint8_t)sqlite3_column_int(s, 5);
m6->nat_type = (uint8_t)sqlite3_column_int(s, 6);
memset(&m6->address_profile, 0, sizeof(m6->address_profile));
m6->next = NULL;
if (!m6_head) m6_head = m6_tail = m6;
else { m6_tail->next = m6; m6_tail = m6; }

2
src/routing_layer/topo_node_sqlite.h

@ -25,7 +25,7 @@ uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id);
struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id);
/* Персистентная запись адресов узла в node_addresses (источник истины — BGP/topo_group).
* DELETE + INSERT из ni->v4_addrs/v6_addrs, классификация config_type/nat_type из SOCKMETA. */
* DELETE + INSERT из ni->v4_addrs/v6_addrs; mapping/inbound/evidence из подписанного SOCKMETA. */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni);
/* Есть ли у узла запись мембера хотя бы в одной peers_<ch> (реальный мембер с ключами). */

10
src/transport_layer/etcp.c

@ -10,7 +10,7 @@
#include "etcp_router.h"
#include "routing.h"
#include "topo_group.h"
#include "route_ping.h"
#include "nat_detection.h"
#include "../lib/u_async.h"
#include "../lib/ll_queue.h"
#include "../lib/debug_config.h"
@ -667,14 +667,6 @@ void etcp_conn_reinit_id(struct ETCP_CONN* etcp, const char* reason, uint64_t re
// Сбрасываем ретрансмиты роутера ДО очистки ETCP-очередей (избегаем гонки с data_pool)
if (etcp->peer_node_id) etcp_router_pause_retrans_for_node(etcp->instance, etcp->peer_node_id);
// Сбрасываем NAT-статус у всех линков этого соединения
struct ETCP_LINK* l = etcp->links;
while (l) {
l->nat_type = NAT_TYPE_UNKNOWN;
l->nat_check_status = NAT_CHECK_NONE;
l = l->next;
}
// Вызываем etcp_conn_reset для сброса состояния
etcp_conn_reset(etcp);

225
src/transport_layer/etcp_connections.c

@ -24,6 +24,7 @@
#include "topo_group.h"
#include "node_conn_direct.h"
#include "nat_detection.h"
#include "route_ping.h"
#include "../lib/memory_pool.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
@ -42,11 +43,22 @@ static const char* server_type_str(uint8_t type) {
static const char* names[] = {"UNKNOWN", "PUBLIC", "NAT", "PRIVATE", "LOCAL"};
return type < 5 ? names[type] : "?";
}
// Читаемое имя NAT-типа (NAT_TYPE_*/NAT_VERIFIED_*) для логов.
static const char* nat_type_str(uint8_t nat_type) {
if (nat_type >= 4 && nat_type <= 7) nat_type -= 4;
static const char* names[] = {"UNKNOWN", "EIM", "STRICT", "DIRECT"};
return nat_type < 4 ? names[nat_type] : "?";
/* ID уникален среди живых сокетов; поколение не повторяется при переиспользовании ID. */
static int socket_identity_init(struct ETCP_SOCKET *socket, struct UTUN_INSTANCE *instance) {
for (unsigned attempt = 0; attempt < 256; attempt++) {
uint8_t candidate = instance->next_socket_id++;
struct ETCP_SOCKET *existing = instance->etcp_sockets;
while (existing && existing->sock_id != candidate) existing = existing->next;
if (existing) continue;
socket->sock_id = candidate;
do {
if (random_bytes((uint8_t *)&socket->generation, sizeof(socket->generation))) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket generation RNG failed name=%s", socket->name); return -1;
}
} while (!socket->generation);
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "no free socket ID name=%s", socket->name); return -1;
}
/* ── Forward declarations ────────────────────────────────────────────
@ -809,8 +821,7 @@ struct ETCP_SOCKET* etcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_S
e_sock->errorcode = 0;
e_sock->pkt_format_errors = 0;
e_sock->type = type;
e_sock->sock_id = instance->next_socket_id++;
e_sock->nat_type = NAT_TYPE_UNKNOWN; // только результат детекции NAT, серверные сокеты стартуют с unknown
if (socket_identity_init(e_sock, instance)) { socket_close_wrapper(e_sock->fd); u_free(e_sock); return NULL; }
e_sock->mtu = mtu;
e_sock->only_local = only_local;
e_sock->links_queue = queue_new(instance->ua, 256, offsetof(struct link_queue_entry, key), LINK_ADDR_KEY_SIZE, "links");
@ -872,6 +883,7 @@ struct ETCP_SOCKET* etcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_S
void etcp_socket_remove(struct ETCP_SOCKET* conn) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "");
if (!conn) return;
if (conn->instance && conn->instance->nat_det) nat_detection_socket_removed(conn->instance->nat_det, conn);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[ETCP] Removing socket %p, socket_id=%p", conn, conn->socket_id);
@ -928,7 +940,7 @@ struct ETCP_SOCKET* tcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SE
strncpy(ts->name, server->name, MAX_CONN_NAME_LEN - 1);
ts->name[MAX_CONN_NAME_LEN - 1] = '\0';
}
ts->sock_id = instance->next_socket_id++;
if (socket_identity_init(ts, instance)) { u_free(ts); return NULL; }
if (server->ip.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&server->ip;
@ -1005,6 +1017,7 @@ struct ETCP_SOCKET* tcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SE
// Удаление TCP-сокета и всех его линков, включая созданные вне NCD.
void tcp_socket_remove(struct ETCP_SOCKET* sock) {
if (!sock || !sock->instance) return;
if (sock->instance->nat_det) nat_detection_socket_removed(sock->instance->nat_det, sock);
struct UTUN_INSTANCE* inst = sock->instance;
struct ETCP_SOCKET** pp = &inst->etcp_sockets;
while (*pp && *pp != sock) pp = &(*pp)->next;
@ -1390,6 +1403,8 @@ static int etcp_tcp_send(struct ETCP_DGRAM* dgram) {
// Единая точка отправки UDP: SOCKS5-релей (если сокет proxied) → send_hook (dummynet) → socket_sendto.
ssize_t etcp_udp_send(struct ETCP_LINK* link, socket_t fd, const void* buf, size_t len,
const struct sockaddr* addr, socklen_t addr_len) {
if (link && link->conn && link->conn->instance->nat_det)
nat_detection_note_send(link->conn->instance->nat_det, link->conn, (const struct sockaddr_storage *)addr);
if (link && link->conn && link->conn->socks_udp)
return socks_udp_sendto(link->conn->socks_udp, buf, len, (const struct sockaddr_storage*)addr);
if (link && link->send_hook)
@ -1498,6 +1513,7 @@ static int etcp_send_ping_raw(struct ETCP_DGRAM* dgram, struct ETCP_SOCKET* e_so
}
memcpy(enc_buf + enc_buf_len, dgram->data + len, dgram->noencrypt_len);
size_t total = enc_buf_len + dgram->noencrypt_len;
if (e_sock->instance->nat_det) nat_detection_note_send(e_sock->instance->nat_det, e_sock, addr);
ssize_t sent;
if (e_sock->socks_udp) {
sent = socks_udp_sendto(e_sock->socks_udp, enc_buf, total, addr);
@ -1528,12 +1544,8 @@ static size_t etcp_build_ping_response_data(const uint8_t* req_data, size_t req_
// Таймаут пинга: снять из pending_pings и вызвать callback с success=0.
static void ping_timeout_cbk(void* arg) {
struct PING_CONTEXT* ctx = (struct PING_CONTEXT*)arg;
if (!ctx || !ctx->cb) return;
if (ctx->timeout_timer) {
uasync_cancel_timeout(ctx->instance->ua, ctx->timeout_timer);
ctx->timeout_timer = NULL;
}
ctx->cb(0, 0, ctx->arg, ctx->nonce, NULL, 0);
if (!ctx) return;
ctx->timeout_timer = NULL;
if (ctx->instance->pending_pings == ctx) {
ctx->instance->pending_pings = ctx->next;
} else {
@ -1541,10 +1553,25 @@ static void ping_timeout_cbk(void* arg) {
while (prev && prev->next != ctx) prev = prev->next;
if (prev) prev->next = ctx->next;
}
if (ctx->cb) ctx->cb(0, 0, ctx->arg, ctx->nonce, NULL, 0);
if (ctx->user_data) u_free(ctx->user_data);
u_free(ctx);
}
void etcp_cancel_pings(struct UTUN_INSTANCE *instance, etcp_ping_callback_t callback, void *arg) {
if (!instance) return;
struct PING_CONTEXT **cursor = &instance->pending_pings;
while (*cursor) {
struct PING_CONTEXT *ping = *cursor;
if (ping->cb != callback || ping->arg != arg) { cursor = &ping->next; continue; }
*cursor = ping->next;
if (ping->timeout_timer) uasync_cancel_timeout(instance->ua, ping->timeout_timer);
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "UDP probe cancelled nonce=%016llx socket=%u generation=%016llx",
(unsigned long long)ping->nonce, ping->socket_id, (unsigned long long)ping->socket_generation);
u_free(ping->user_data); u_free(ping);
}
}
// Отправка UDP-пинга через конкретный сокет: сборка PING-кодограммы (nonce, user_data,
// обфусцированный pubkey), регистрация в pending_pings с таймаутом.
int etcp_send_ping_to_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET* e_sock,
@ -1577,7 +1604,15 @@ int etcp_send_ping_to_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET*
ctx->instance = instance;
ctx->cb = cb;
ctx->arg = user_arg;
ctx->nonce = get_current_timestamp() ^ (uint64_t)rand();
do {
if (random_bytes((uint8_t *)&ctx->nonce, sizeof(ctx->nonce))) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "UDP ping nonce RNG failed"); u_free(ctx); return -3;
}
struct PING_CONTEXT *pending = instance->pending_pings;
while (pending && pending->nonce != ctx->nonce) pending = pending->next;
if (ctx->nonce && !pending) break;
} while (1);
ctx->socket_id = e_sock->sock_id; ctx->socket_generation = e_sock->generation; ctx->peer_addr = *addr;
ctx->timeout_timer = NULL;
ctx->send_time = get_time_tb();
ctx->user_data = NULL;
@ -1664,7 +1699,7 @@ int etcp_send_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubkey_bi
return -1;
}
struct ETCP_SOCKET* e_sock = instance->etcp_sockets;
while (e_sock && (e_sock->is_tcp || e_sock->local_addr.ss_family != addr->ss_family)) e_sock = e_sock->next;
while (e_sock && (e_sock->is_tcp || e_sock->probe_only || e_sock->local_addr.ss_family != addr->ss_family)) e_sock = e_sock->next;
if (!e_sock) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "no UDP socket for addr_family=%d", addr->ss_family);
return -2;
@ -1755,6 +1790,14 @@ static int handle_ping(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const
}
const uint8_t* udata = (ulen > 0) ? (pkt->data + 20) : NULL;
if ((flags & ETCP_PING_FLAG_OBSERVE) && (flags & ETCP_PING_FLAG_ADDRESS_CHECK)) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "UDP probe has incompatible flags=%02x", flags); return 7;
}
if ((flags & ETCP_PING_FLAG_OBSERVE) && e_sock->instance->nat_det)
nat_detection_record_observation(e_sock->instance->nat_det, e_sock, decrypted_pubkey, addr, udata, ulen);
if ((flags & ETCP_PING_FLAG_ADDRESS_CHECK) && e_sock->instance->nat_det)
nat_detection_receive_ingress(e_sock->instance->nat_det, e_sock, decrypted_pubkey, addr, udata, ulen);
if ((flags & ETCP_PING_FLAG_SEND_RTT) && ulen >= 2) {
uint16_t rtt_val = wire_read16(udata);
topo_node_ping_update_rtt(e_sock->instance->topo_groups, peer_id, rtt_val);
@ -1776,8 +1819,13 @@ static int handle_ping(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const
memcpy(p, &nid, 8); p += 8;
uint64_t n = htobe64(nonce);
memcpy(p, &n, 8); p += 8;
uint16_t resp_ulen_be = htobe16(ulen);
uint16_t response_size = ulen + ((flags & ETCP_PING_FLAG_OBSERVE) ? ROUTE_ADDR_WIRE_SIZE : 0);
uint16_t resp_ulen_be = htobe16(response_size);
memcpy(p, &resp_ulen_be, 2); p += 2;
if (flags & ETCP_PING_FLAG_OBSERVE) {
if (route_ping_addr_encode(p, addr)) { u_free(resp); return 7; }
p += ROUTE_ADDR_WIRE_SIZE;
}
size_t copied = etcp_build_ping_response_data(udata, ulen, p, PACKET_DATA_SIZE - (p - resp->data) - SC_PUBKEY_ENC_SIZE);
p += copied;
uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE];
@ -1807,7 +1855,8 @@ static void rtt_send_ping_cb(int success, uint16_t rtt, void* arg, uint64_t nonc
}
// Обработка входящего PONG: поиск pending_pings по nonce, вызов callback с RTT и данными.
static int handle_pong(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const struct sockaddr_storage* addr, size_t pkt_len) {
static int handle_pong(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const struct sockaddr_storage* addr,
const uint8_t *decrypted_pubkey, size_t pkt_len) {
if (pkt_len < 23) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "PONG too short: pkt_len=%zu from %s", pkt_len, sockaddr_storage_to_str(addr).str);
return 7;
@ -1835,6 +1884,13 @@ static int handle_pong(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const
int found = 0;
while (ctx) {
if (ctx->nonce == nonce) {
if (ctx->socket_id != e_sock->sock_id || ctx->socket_generation != e_sock->generation ||
memcmp(ctx->peer_pubkey, decrypted_pubkey, SC_PUBKEY_SIZE) ||
!route_ping_addr_equal(&ctx->peer_addr, addr, 1)) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "UDP PONG identity/endpoint mismatch nonce=%016llx socket=%u generation=%016llx",
(unsigned long long)nonce, e_sock->sock_id, (unsigned long long)e_sock->generation);
memory_pool_free(e_sock->instance->pkt_pool, pkt); return 0;
}
found = 1;
if (prev) prev->next = ctx->next;
else e_sock->instance->pending_pings = ctx->next;
@ -1894,62 +1950,21 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk
resp->mtu[0]=link->mtu_local>>8;
resp->mtu[1]=link->mtu_local;
resp->link_id = link->local_link_id;
resp->remote_socket_id = req->socket_id;
resp->remote_socket_id = e_sock->sock_id;
resp->only_local = e_sock->only_local;
resp->type = e_sock->type;
// Add client's IP:port (so client behind NAT can know its external address)
/* Наблюдение относится к клиенту; адрес сервера хранится только в remote_addr. */
if (addr->ss_family == AF_INET) {
struct sockaddr_in *sin = (struct sockaddr_in*)addr;
memcpy(resp->peer_ipv4, &sin->sin_addr.s_addr, 4);
uint16_t port = ntohs(sin->sin_port);
resp->peer_port[0] = port >> 8;
resp->peer_port[1] = port & 0xFF;
link->nat_ip = sin->sin_addr.s_addr;
link->nat_port = port;
const struct sockaddr_in *source = (const struct sockaddr_in *)addr;
memcpy(resp->peer_ipv4, &source->sin_addr.s_addr, 4);
memcpy(resp->peer_port, &source->sin_port, 2);
} else {
// For IPv6, set to 0 (not supported for NAT traversal)
memset(resp->peer_ipv4, 0, 4);
memset(resp->peer_port, 0, 2);
link->nat_ip = 0;
link->nat_port = 0;
}
// DIRECT detection: if client reports its own address and it matches observed source → real public IP
if (pkt_len >= ETCP_INIT_REQ_SIZE && addr->ss_family == AF_INET && link->nat_ip != 0) {
if (req_src_ip != 0 && req_src_ip == link->nat_ip
&& req_src_port == link->nat_port
&& !is_local_subnet(link->nat_ip))
{
link->nat_type = NAT_TYPE_DIRECT;
link->nat_check_status = NAT_CHECK_EIM;
if (link->etcp->instance->nat_det) {
nat_detection_send_nat_info(link->etcp->instance->nat_det, link->etcp,
link->remote_socket_id,
link->nat_ip, link->nat_port, NAT_TYPE_DIRECT);
}
DEBUG_INFO(DEBUG_CATEGORY_NAT, "DIRECT IP: %s:%u for %s",
ip_to_str(&link->nat_ip, AF_INET).str, link->nat_port,
link->etcp->log_name);
}
}
/* Self-detection: if my IP is non-local, I'm on a public IP */
DEBUG_INFO(DEBUG_CATEGORY_NAT, "self-detect check: socket=%s type=%s nat=%s if_addr=%s local_addr=%s",
e_sock->name, server_type_str(e_sock->type), nat_type_str(e_sock->nat_type),
sockaddr_storage_to_str(&e_sock->interface_addr).str,
sockaddr_storage_to_str(&e_sock->local_addr).str);
if (e_sock->nat_type != NAT_VERIFIED_DIRECT && e_sock->interface_addr.ss_family == AF_INET) {
uint32_t my_ip = ((struct sockaddr_in*)&e_sock->interface_addr)->sin_addr.s_addr;
if (my_ip != 0 && !is_local_subnet(my_ip)) {
e_sock->nat_type = NAT_VERIFIED_DIRECT;
struct sockaddr_in* nat_sin = (struct sockaddr_in*)&e_sock->nat_addr;
nat_sin->sin_family = AF_INET;
nat_sin->sin_addr.s_addr = my_ip;
nat_sin->sin_port = ((struct sockaddr_in*)&e_sock->interface_addr)->sin_port;
if (link->etcp->instance->topo_groups) topo_node_update_my_addresses(link->etcp->instance);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Self-detected PUBLIC: socket=%s ip=%s port=%u sock_id=%d",
e_sock->name, ip_to_str(&my_ip, AF_INET).str,
ntohs(nat_sin->sin_port), e_sock->sock_id);
}
memset(resp->peer_ipv4, 0, sizeof(resp->peer_ipv4));
memset(resp->peer_port, 0, sizeof(resp->peer_port));
}
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "INIT client address claim=%s:%u observed_peer=%s bytes=%zu server_socket=%s client_sid=%u",
ip_to_str(&req_src_ip, AF_INET).str, req_src_port, sockaddr_storage_to_str(addr).str, pkt_len,
e_sock->name, req->socket_id);
memcpy(resp->ed25519_pubkey, e_sock->instance->my_ed25519_pubkey, SC_PUBKEY_SIZE);
resp->device_type = e_sock->instance->client_type;
wire_write16(resp->keepalive,e_sock->instance->keepalive_interval);
@ -2017,6 +2032,9 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D
// ETCP_INIT_RESPONSE_NOINIT (0x05) - no reset
struct ETCP_INIT_RESPONSE_PKT* resp = (struct ETCP_INIT_RESPONSE_PKT*)pkt->data;
uint64_t server_node_id = wire_read64(resp->node_id);
if (server_node_id != sc_derive_node_id_from_pubkey(link->etcp->crypto_ctx.peer_public_key)) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "INIT_RESPONSE node ID does not match authenticated peer key"); return 46;
}
uint64_t resp_reset_id = wire_read64(resp->reset_id);
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "INIT_RESPONSE rid=%016llx", (unsigned long long)resp_reset_id);
etcp_conn_apply_peer_reset_id(link->etcp, resp_reset_id);
@ -2030,55 +2048,22 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D
link->remote_type = resp->type;
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] INIT_RESPONSE received (link=%d, mtu=%d, rid=%016llx)", link->etcp->log_name, link->remote_link_id, link->mtu, (unsigned long long)resp_reset_id);
// Parse NAT IP:port from response (new format includes 4+2 bytes)
if (pkt_len >= ETCP_INIT_RESP_SIZE) {
uint32_t new_nat_ip;
memcpy(&new_nat_ip, resp->peer_ipv4, 4);
uint16_t new_nat_port = wire_read16(resp->peer_port);
// Check if NAT address changed
if (link->nat_ip == 0 && link->nat_port == 0) {
// First time receiving NAT info
link->nat_ip = new_nat_ip;
link->nat_port = new_nat_port;
struct in_addr addr;
addr.s_addr = new_nat_ip;
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[%s] NAT address initialized: %s:%u",
link->etcp->log_name, ip_to_str(&addr, AF_INET).str, new_nat_port);
} else if (link->nat_ip != new_nat_ip || link->nat_port != new_nat_port) {
// NAT address changed
struct in_addr old_addr, new_addr;
old_addr.s_addr = link->nat_ip;
new_addr.s_addr = new_nat_ip;
link->nat_ip = new_nat_ip;
link->nat_port = new_nat_port;
if (!link->is_tcp && e_sock->local_addr.ss_family == AF_INET && wire_read16(resp->peer_port)) {
struct sockaddr_storage observed; memset(&observed, 0, sizeof(observed));
struct sockaddr_in *source = (struct sockaddr_in *)&observed;
source->sin_family = AF_INET; memcpy(&source->sin_addr, resp->peer_ipv4, 4);
memcpy(&source->sin_port, resp->peer_port, 2);
if (link->local_as_seen_by_peer.ss_family && !route_ping_addr_equal(&link->local_as_seen_by_peer, &observed, 1)) {
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Own endpoint observation changed peer=%s local=%s before=%s after=%s",
link->etcp->log_name, e_sock->name, sockaddr_storage_to_str(&link->local_as_seen_by_peer).str,
sockaddr_storage_to_str(&observed).str);
link->nat_changes_count++;
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[%s] NAT address changed: %s:%u -> %s:%u (change #%u)",
link->etcp->log_name, ip_to_str(&old_addr.s_addr, AF_INET).str, link->nat_port, ip_to_str(&new_addr.s_addr, AF_INET).str, new_nat_port,
link->nat_changes_count);
}
// Update socket NAT address (only if not already verified)
{
DEBUG_INFO(DEBUG_CATEGORY_NAT, "init_resp nat: socket=%s nat=%s new_ip=%s new_port=%u",
e_sock->name, nat_type_str(e_sock->nat_type),
ip_to_str(&new_nat_ip, AF_INET).str, new_nat_port);
if (e_sock->nat_type < NAT_VERIFIED_UNKNOWN) {
struct sockaddr_in* sin = (struct sockaddr_in*)&e_sock->nat_addr;
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = new_nat_ip;
sin->sin_port = htons(new_nat_port);
} else {
DEBUG_INFO(DEBUG_CATEGORY_NAT, "init_resp nat update SKIPPED (already verified): socket=%s nat=%s",
e_sock->name, nat_type_str(e_sock->nat_type));
}
}
} else {
// Legacy format without NAT info
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Received legacy INIT_RESPONSE without NAT info",
link->etcp->log_name);
} else link->nat_hits_count++;
link->local_as_seen_by_peer = observed;
if (e_sock->instance->nat_det) nat_detection_observed_by_peer(e_sock->instance->nat_det, link, &observed);
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "Own endpoint observed peer=%s socket=%s generation=%016llx address=%s",
link->etcp->log_name, e_sock->name, (unsigned long long)e_sock->generation,
sockaddr_storage_to_str(&observed).str);
}
memcpy(link->etcp->peer_ed25519_pubkey, resp->ed25519_pubkey, SC_PUBKEY_SIZE);
@ -2259,6 +2244,10 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
pkt->noencrypt_len=0;
uint8_t code = pkt->data[0];
uint64_t peer_id = wire_read64(pkt->data + 2);
if ((code == ETCP_PING || code == ETCP_PONG) && peer_id != sc_derive_node_id_from_pubkey(decrypted_pubkey)) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "UDP probe sender ID does not match authenticated key from=%s", sockaddr_storage_to_str(&addr).str);
errorcode = 7; goto ec_fr;
}
if (code == ETCP_PING) {
#ifdef UTUN_HAVE_STANDBY
{ char who[64]; snprintf(who, sizeof(who), "addr=%s", sockaddr_storage_to_str(&addr).str); standby_log_rx("udp ping", who); }
@ -2275,7 +2264,7 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
#endif
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "X25519 decrypted: PONG from peer=0x%016llx src=%s",
(unsigned long long)peer_id, sockaddr_storage_to_str(&addr).str);
int ret = handle_pong(e_sock, pkt, &addr, pkt_len);
int ret = handle_pong(e_sock, pkt, &addr, decrypted_pubkey, pkt_len);
if (ret) { errorcode = ret; goto ec_fr; }
return;
}

39
src/transport_layer/etcp_connections.h

@ -13,6 +13,7 @@ extern "C" {
#include "reality.h"
#include "utun_instance.h"
#include "etcp_bbr.h"
#include "../routing_layer/address_profile.h"
#include "../lib/socket_compat.h"
#include "../lib/ll_queue.h"
#include <stdint.h>
@ -46,6 +47,8 @@ extern "C" {
#define ETCP_PING_FLAG_WANT_RTT 0x01 // в PONG: хочу получить RTT от пингера
#define ETCP_PING_FLAG_SEND_RTT 0x02 // в PING: несу RTT для получателя
#define ETCP_PING_FLAG_OBSERVE 0x04 // PONG несёт фактически наблюдаемый IP:port
#define ETCP_PING_FLAG_ADDRESS_CHECK 0x08 // прямое входящее испытание endpoint
/* Адаптивный keepalive */
#define KA_PERIOD_MIN_MS 200
@ -126,6 +129,9 @@ struct PING_CONTEXT {
void* timeout_timer;
uint64_t send_time; // время отправки пинга в 0.1ms
uint8_t peer_pubkey[SC_PUBKEY_SIZE];
uint8_t socket_id;
uint64_t socket_generation;
struct sockaddr_storage peer_addr;
uint8_t* user_data;
size_t user_data_len;
};
@ -148,6 +154,8 @@ struct ETCP_SOCKET {
uint8_t type; // CFG_SERVER_TYPE_*
uint8_t sock_id; // unique id (0-255) for NAT matching
uint8_t is_tcp; // 1=TCP (STCP), 0=UDP
uint8_t probe_only; // вспомогательный UDP-порт наблюдателя, не транспорт NCD
uint64_t generation; // случайная эпоха endpoint; sock_id может переиспользоваться
uint8_t reality_enabled; // 1=TCP-порт с REALITY-камуфляжем
uint8_t only_local; // 1 = only local, no forwarding (UDP)
uint32_t netif_index; // для sin6_scope_id
@ -162,33 +170,12 @@ struct ETCP_SOCKET {
struct ll_queue* links_queue; // hash-очередь линков
int errorcode;
size_t pkt_format_errors;
uint8_t nat_type; // NAT_TYPE_*
struct address_profile address_profile;
uint32_t local_defaultroute_ip; // auto-detected IPv4 (network byte order)
uint8_t local_defaultroute_ip6[16];
struct socks_udp* socks_udp; // SOCKS5 UDP ASSOCIATE (NULL = напрямую)
};
// NAT check status
#define NAT_CHECK_NONE 0
#define NAT_CHECK_WAITING 1
#define NAT_CHECK_IN_PROGRESS 2
#define NAT_CHECK_EIM 3
#define NAT_CHECK_STRICT 4
// NAT type (detected by server during NAT check)
#define NAT_TYPE_UNKNOWN 0
#define NAT_TYPE_EIM 1 // Endpoint-Independent Mapping
#define NAT_TYPE_STRICT 2 // Address/Restricted or Symmetric
#define NAT_TYPE_DIRECT 3 // real public IP, no NAT (detected during INIT handshake)
// Verified NAT types (published in nodeinfo after server-side NAT detection)
// Values above CFG_SERVER_TYPE_PRIVATE to avoid collision with config types
#define NAT_VERIFIED_UNKNOWN 4 // detection failed or inconclusive
#define NAT_VERIFIED_EIM 5 // EIM NAT
#define NAT_VERIFIED_STRICT 6 // strict NAT
#define NAT_VERIFIED_DIRECT 7 // real public IP, no NAT
#define LINK_STATE_HANDSHAKE 1
#define LINK_STATE_TRY_RECONNECT 2
#define LINK_STATE_CONNECTED 3
@ -226,7 +213,6 @@ struct ETCP_LINK {
uint8_t remote_socket_id; // socket id peer
uint8_t remote_only_local; // only_local flag from peer
uint8_t remote_type; // CFG_SERVER_TYPE_* (type of peer's socket)
uint8_t nat_type; // NAT_TYPE_* (detected for this client link)
uint8_t recv_keepalive; // 1 - up, 0 - down (принимаются ли пакеты)
uint8_t remote_keepalive; // 1 - up, 0 - down (удаленная сторона сообщает - принимаются ли у нее пакеты)
uint8_t link_status; // 1 - up, 0 - down (итоговый статус - если есть проблемы на любой стороне - линк down)
@ -273,11 +259,9 @@ struct ETCP_LINK {
uint32_t bandwidth; // Link bandwidth in Kbits/sec
// NAT address tracking (from INIT_RESPONSE)
uint32_t nat_ip; // NAT IPv4 address (network byte order), 0 = not set
uint16_t nat_port; // NAT port (host byte order)
struct sockaddr_storage local_as_seen_by_peer; // только наш endpoint, наблюдаемый удалённым пиром
uint32_t nat_changes_count; // Counter of NAT address changes
uint32_t nat_hits_count; // Counter of NAT address matches (new init response with same IP:port)
uint8_t nat_check_status; // NAT_CHECK_*
// Keepalive state
void* keepalive_timer; // Таймер обычного keepalive (только в normal-режиме)
@ -422,6 +406,9 @@ int etcp_send_ping_to_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET*
const uint8_t* user_data, size_t user_data_len,
uint8_t flags);
/* Отмена без callback: вызывающий владеет arg и освобождает его после отмены. */
void etcp_cancel_pings(struct UTUN_INSTANCE *instance, etcp_ping_callback_t callback, void *arg);
// TCP-ping: подключение к TCP-адресу peer + хендшейк с флагом PING. RTT меряется
// по хендшейку (send→response). cb вызывается один раз (success=1/rtt или success=0).
// out_cli (опционально) — возвращает хендл stcp_client для досрочной отмены (stcp_client_destroy).

9
src/transport_layer/etcp_dump.c

@ -151,8 +151,8 @@ void etcp_dump_conn_state(struct ETCP_CONN* conn) {
link->encrypt_errors, link->decrypt_errors,
link->send_errors, link->recv_errors,
link->total_encrypted, link->total_decrypted, link->total_retransmissions);
DLOG("NAT: type=%d chk=%d chg=%u hits=%u",
link->nat_type, link->nat_check_status,
DLOG("OWN_ENDPOINT_OBSERVED: addr=%s changes=%u hits=%u",
sockaddr_storage_to_str(&link->local_as_seen_by_peer).str,
link->nat_changes_count, link->nat_hits_count);
DLOG("LAST_RECV: tb=%llu ts=%u",
(unsigned long long)link->last_recv_local_time, link->last_recv_timestamp);
@ -187,8 +187,9 @@ void etcp_dump_sockets(struct UTUN_INSTANCE* instance) {
int idx = 0;
while (s) {
char abuf[32]; sockaddr_to_str(&s->local_addr, abuf, sizeof(abuf));
DLOG("SOCK %d: '%s' fd=%d addr=%s type=%d nat=%d mtu=%d links=%d",
idx, s->name, (int)s->fd, abuf, s->type, s->nat_type,
DLOG("SOCK %d: '%s' fd=%d addr=%s config=%u mapping=%s inbound=%s mtu=%d links=%d",
idx, s->name, (int)s->fd, abuf, s->type, address_mapping_name(s->address_profile.mapping),
address_inbound_name(s->address_profile.inbound),
s->mtu, s->links_queue ? queue_entry_count(s->links_queue) : 0);
s = s->next; idx++;
}

41
src/transport_layer/node_conn_direct.c

@ -17,6 +17,7 @@
#include "topo_node_sqlite.h"
#include "sock_match.h"
#include "topo_group.h"
#include "ntp_time.h"
#include "config_parser.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
@ -258,7 +259,10 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
for (const struct TOPO_SOCKMETA4* mm = ni->v4_sock_meta; mm; mm = mm->next)
if (mm->id == a->socket_id) { m = mm; break; }
uint8_t tcfg = m ? m->config_type : CFG_SERVER_TYPE_UNKNOWN;
uint8_t tnat = m ? m->nat_type : NAT_TYPE_UNKNOWN;
const struct address_profile *profile = m ? &m->address_profile : NULL;
uint8_t target_class = sock_target_class(a->type, tcfg, profile, ip_is_public(tip),
nviews ? views[0].now_us : (uint64_t)ntp_time_get_us(inst));
if (target_class == SOCK_TC_UNREACH) { v4_skip++; continue; }
if (a->protocol & TOPO_PROTO_TCP) {
if ((a->protocol & TOPO_PROTO_REALITY) && !topo_node_find_reality_sock(ni, a->socket_id)) {
@ -270,7 +274,7 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
const struct sock_view* v = &views[vi];
if (!v->is_tcp) continue;
tcp_sockets++;
if (specific_sock ? v->sock != specific_sock : !sock_match(v, a->type, tcfg, tnat, tip)) continue;
if (specific_sock ? v->sock != specific_sock : !sock_match(v, a->type, tcfg, profile, tip)) continue;
if (check_inst) return 1;
int added = ncd_add_tcp_link(conn, v->sock, &sa, topo_node_find_reality_sock(ni, a->socket_id));
if (added > 0) link_count += added;
@ -288,7 +292,7 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
} else if (!specific_sock) {
for (int vi = 0; vi < nviews; vi++) {
const struct sock_view* v = &views[vi];
if (v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue;
if (v->is_tcp || !sock_match(v, a->type, tcfg, profile, tip)) continue;
if (check_inst) return 1;
link_count += ncd_add_udp_link(conn, v->sock, &sa);
}
@ -303,6 +307,14 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
int zero = 1; for (int j = 0; j < 16; j++) if (a->addr[j] != 0) { zero = 0; break; }
if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[ncd] v6 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; }
int is_ll = (a->addr[0] == 0xfe && (a->addr[1] & 0xc0) == 0x80);
if (a->addr[0] == 0xff) { v6_skip++; continue; }
const struct TOPO_SOCKMETA6 *metadata = ni->v6_sock_meta;
while (metadata && metadata->id != a->socket_id) metadata = metadata->next;
int global = (a->addr[0] & 0xe0) == 0x20;
int64_t current = ntp_time_get_us(inst);
uint8_t target_class = sock_target_class(a->type, metadata ? metadata->config_type : CFG_SERVER_TYPE_UNKNOWN,
metadata ? &metadata->address_profile : NULL, global, current > 0 ? (uint64_t)current : 0);
if (target_class == SOCK_TC_UNREACH) { v6_skip++; continue; }
struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6;
memcpy(&sin6.sin6_addr, a->addr, 16); sin6.sin6_port = htons(a->port);
@ -313,12 +325,12 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
}
int tcp_sockets = 0;
if (is_ll) { struct ETCP_SOCKET* sv = inst->etcp_sockets;
while (sv) { if (sv->local_addr.ss_family == AF_INET6 && sv->netif_index) { sin6.sin6_scope_id = sv->netif_index; break; } sv = sv->next; } }
while (sv) { if (!sv->probe_only && sv->local_addr.ss_family == AF_INET6 && sv->netif_index) { sin6.sin6_scope_id = sv->netif_index; break; } sv = sv->next; } }
struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6));
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) {
if (!s->is_tcp || s->local_addr.ss_family != AF_INET6) continue;
if (!s->is_tcp || s->probe_only || s->local_addr.ss_family != AF_INET6) continue;
tcp_sockets++;
if (specific_sock ? s != specific_sock : s->type == CFG_SERVER_TYPE_PRIVATE) continue;
if (specific_sock ? s != specific_sock : global && s->type == CFG_SERVER_TYPE_LOCAL) continue;
if (check_inst) return 1;
if (is_ll) { sin6.sin6_scope_id = s->netif_index; memcpy(&sa, &sin6, sizeof(sin6)); }
int added = ncd_add_tcp_link(conn, s, &sa, topo_node_find_reality_sock(ni, a->socket_id));
@ -338,7 +350,8 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
link_count += ncd_add_udp_link(conn, specific_sock, &sa);
} else if (!specific_sock) {
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) {
if (s->is_tcp || s->local_addr.ss_family != AF_INET6 || s->type == CFG_SERVER_TYPE_PRIVATE) continue;
if (s->is_tcp || s->probe_only || s->local_addr.ss_family != AF_INET6 ||
(global && s->type == CFG_SERVER_TYPE_LOCAL)) continue;
if (check_inst) return 1;
if (is_ll) sin6.sin6_scope_id = s->netif_index;
struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6));
@ -367,13 +380,17 @@ int node_conn_direct_compatible(struct UTUN_INSTANCE* inst, uint64_t node_id) {
* Это возможность попытки, не подтверждение маршрута/работоспособности скрытых сокетов пира. */
const struct TOPO_NODE* self = topo_node_registry_find(inst->topo_groups, inst->node_id);
if (!self) return 0;
int64_t current = ntp_time_get_us(inst);
uint64_t now_us = current > 0 ? (uint64_t)current : 0;
for (const struct TOPO_ADDR4* a = self->v4_addrs; a; a = a->next) {
uint32_t ip; memcpy(&ip, a->addr, 4);
if (!ip || !a->port) continue;
const struct TOPO_SOCKMETA4* meta = self->v4_sock_meta;
while (meta && meta->id != a->socket_id) meta = meta->next;
uint8_t cfg = meta ? meta->config_type : CFG_SERVER_TYPE_UNKNOWN, nat = meta ? meta->nat_type : NAT_TYPE_UNKNOWN;
if (sock_target_class(a->type, cfg, nat) != SOCK_TC_PUB) continue;
uint8_t cfg = meta ? meta->config_type : CFG_SERVER_TYPE_UNKNOWN;
const struct address_profile *profile = meta ? &meta->address_profile : NULL;
uint8_t target_class = sock_target_class(a->type, cfg, profile, ip_is_public(ip), now_us);
if (target_class == SOCK_TC_PRIV || target_class == SOCK_TC_UNREACH) continue;
if ((a->protocol & TOPO_PROTO_REALITY) && !topo_node_find_reality_sock(self, a->socket_id)) continue;
if (a->protocol & TOPO_PROTO_TCP) return 1;
if (!(a->protocol & TOPO_PROTO_UDP)) continue;
@ -381,11 +398,11 @@ int node_conn_direct_compatible(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!(remote->protocol & TOPO_PROTO_UDP)) continue;
const struct TOPO_SOCKMETA4* rm = node->v4_sock_meta;
while (rm && rm->id != remote->socket_id) rm = rm->next;
struct sock_view view = {.has_inet = 1};
view.is_eim = rm && (rm->nat_type == NAT_TYPE_EIM || rm->nat_type == NAT_VERIFIED_EIM);
struct sock_view view = {.has_inet = 1, .now_us = now_us};
if (rm) view.address_profile = rm->address_profile;
for (const struct TOPO_ADDR4* ra = node->v4_addrs; ra; ra = ra->next)
if (ra->socket_id == remote->socket_id && ra->type == TOPO_ADDR_NAT) memcpy(&view.public_ip4, ra->addr, 4);
if (sock_match(&view, a->type, cfg, nat, ip)) return 1;
if (sock_match(&view, a->type, cfg, profile, ip)) return 1;
}
}
for (const struct TOPO_ADDR6* a = self->v6_addrs; a; a = a->next) {

4
src/utun_instance.c

@ -432,6 +432,9 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
auto_socket_destroy(instance);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] E done — socket_monitor+auto_socket");
/* Детектор владеет таймерами и вспомогательными сокетами: остановить до групп/транспорта. */
if (instance->nat_det) { nat_detection_destroy(instance->nat_det); instance->nat_det = NULL; }
/* Phase F: BGP */
if (instance->topo_groups) {
etcp_router_unbind(instance, ETCP_RT_ID_CONN_MGR);
@ -542,7 +545,6 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
/* etcp_router_destroy откладывает queue_entry_free(rconn->ll) через uasync_call_soon
(router_close_finalize) — выкачать их, пока ua ещё жив. */
uasync_drain_immediate(instance->ua);
if (instance->nat_det) { nat_detection_destroy(instance->nat_det); instance->nat_det = NULL; }
fw_free(&instance->fw);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] N done — router/nat_det/fw");

4
tests/Makefile.am

@ -1,9 +1,9 @@
# Tests Makefile.am for utun - all tests with new ll_queue library
EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_net.h \
EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_net.h nat_fixture.h \
run_tests.py test_run_tests.py test_build_script.py stream_netns/topology.py stream_netns/network.py \
stream_netns/probe.py stream_netns/run.py stream_netns/scenario.py stream_netns/README.md stream_netns/TESTED.md \
stream_netns/test_isolation.py
stream_netns/test_isolation.py stream_netns/nat_scenario.py
# Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий.

82
tests/nat_fixture.h

@ -0,0 +1,82 @@
#ifndef NAT_FIXTURE_H
#define NAT_FIXTURE_H
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "test_net.h"
#include "utun_instance.h"
#include "config_parser.h"
#include "transport_layer/etcp.h"
#include "transport_layer/node_conn_direct.h"
#include "routing_layer/topo_group.h"
#include "routing_layer/topo_node_sqlite.h"
#include "routing_layer/nat_detection.h"
#include "routing_layer/route_ping.h"
#include "../lib/u_async.h"
#include "../lib/mem.h"
struct nat_fixture {
struct UASYNC *ua;
struct UTUN_INSTANCE *nodes[8];
struct NODE_CONN_DIRECT *handles[24];
unsigned node_count, handle_count;
size_t allocations;
};
static inline void nat_fixture_init(struct nat_fixture *fixture) {
memset(fixture, 0, sizeof(*fixture)); fixture->allocations = u_get_allocated_count();
fixture->ua = uasync_create(); assert(fixture->ua);
}
static inline struct UTUN_INSTANCE *nat_fixture_node(struct nat_fixture *fixture, const char *ip,
const struct SC_MYKEYS *keys, const char *extra) {
struct SC_MYKEYS generated;
if (!keys) { assert(sc_generate_keypair(&generated) == SC_OK); keys = &generated; }
char private_key[65], public_key[65], config[4096];
for (unsigned i = 0; i < 32; i++) {
snprintf(private_key + i * 2, 3, "%02x", keys->private_key[i]);
snprintf(public_key + i * 2, 3, "%02x", keys->public_key[i]);
}
snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\nauto_sockets=no\n"
"[ntp]\nenabled=no\n[server:udp]\naddr=%s:0\ntype=public\n[allowed_keys]\nallow_all=1\n%s",
private_key, public_key, ip, extra ? extra : "");
struct UTUN_INSTANCE *node = utun_instance_create_from_str(fixture->ua, config);
assert(node && fixture->node_count < 8 && utun_core_start(node) == 0);
nat_detection_set_allow_local(node->nat_det, 1);
fixture->nodes[fixture->node_count++] = node; return node;
}
/* Без групповой автосвязности: A контактирует только с явно указанным B.
* NCD переносит настоящую подписанную запись, handshake идёт по UDP. */
static inline struct ETCP_CONN *nat_fixture_connect(struct nat_fixture *fixture, struct UTUN_INSTANCE *from,
const char *source, struct UTUN_INSTANCE *to) {
assert(fixture->handle_count < 24);
struct TOPO_NODE *peer = topo_node_registry_find(to->topo_groups, to->node_id); assert(peer);
struct NODE_CONN_DIRECT **handle = &fixture->handles[fixture->handle_count++];
assert(node_conn_direct_open_node(from, to->node_id, NULL, NULL, handle, peer,
test_etcp_bound_socket(from, source)) != NCD_ERR);
uint64_t end = get_time_tb() + 50000;
struct ETCP_CONN *connection = NULL;
while (get_time_tb() < end) {
connection = node_conn_direct_get_conn(*handle);
if (connection && connection->links_up) return connection;
uasync_poll(fixture->ua, 10);
}
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "fixture connection timed out from=%016llx to=%016llx",
(unsigned long long)from->node_id, (unsigned long long)to->node_id);
abort();
}
static inline void nat_fixture_cleanup(struct nat_fixture *fixture) {
for (unsigned i = 0; i < fixture->handle_count; i++) node_conn_direct_force_close(fixture->handles[i]);
for (unsigned i = 0; i < fixture->node_count; i++) utun_instance_destroy(fixture->nodes[i]);
uasync_poll(fixture->ua, 0);
assert(fixture->ua->timer_alloc_count == fixture->ua->timer_free_count);
assert(fixture->ua->socket_alloc_count == fixture->ua->socket_free_count);
uasync_destroy(fixture->ua, 0);
if (u_get_allocated_count() != fixture->allocations) u_report_unfreed_blocks();
assert(u_get_allocated_count() == fixture->allocations);
}
#endif

27
tests/stream_netns/README.md

@ -16,6 +16,26 @@ python3 tests/stream_netns/run.py --network-only
python3 tests/stream_netns/run.py
```
Проверка детектора на всех 16 настоящих uTun, отдельно от стриминга:
```sh
python3 tests/stream_netns/run.py --nat-only --tcp
```
`--nat-check` выполняет ту же проверку перед обычными JOIN/Merkle/BGP/стримингом.
Контроллер сначала запускает только ядро с ручными NAT-раундами, переносит
подписанные снимки через RPC и создаёт нужные NCD-подключения. Наблюдения,
испытания входа и TCP-handshake идут по настоящей сети. Помощники измерения
не требуют готового собственного NAT-профиля. Измерение входа происходит
до первого исходящего контакта рабочего сокета с помощником.
Проверяются отдельно mapping и inbound каждого UDP endpoint, точный собственный
внешний IP:port, отсутствие ложного STRICT без помощника и корректность TCP listener.
Ожидания: public — UNCHANGED/OPEN, cone — EIM/OPEN, restricted — EIM/CONTACT_REQUIRED,
strict — DEPENDENT без универсального внешнего адреса. TCP за NAT без port forwarding
остаётся UNKNOWN/no_listener. Результаты: `nat-profiles.json`, `nat-result.json`;
обычная телеметрия также включает `address_profiles`, контакты и причину неполного измерения.
Полная baseline-проверка длится 120 секунд после старта доставки, плюс калибровка и JOIN. Она покрывает оборот 16-битного timestamp и несколько циклов проб. Короткая проверка:
```sh
@ -72,7 +92,12 @@ UDP NAT-профили:
- `g_c1`: EIM, вход только от ранее опрошенного IP/порта (APDF).
- `g_a2`, `g_b2`, `g_c2`: внешний UDP-порт зависит от **IP назначения** (ADM), возврат только по conntrack (APDF). Для известных внешних endpoints порты назначаются явно; это воспроизводимый строгий NAT, а не заявление, что обычный masquerade всегда симметричен.
Cone/restricted mapping разрешается только после исходящего пакета, время активности 60 секунд обновляется на каждом исходящем пакете. TCP использует обычный stateful NAT без проброса входящих портов. Проверка перед uTun измеряет реальный внешний адрес/порт на двух публичных получателях и отдельно пробует вход от третьего, включая отсутствие mapping до исходящего трафика. Для проб до/после открытия используются разные внешние tuples, чтобы не переиспользовать отрицательный conntrack.
Mapping разрешается после исходящего пакета, время активности/контакта 60 секунд обновляется исходящим трафиком.
Запрещённый вход отсекается в `prerouting priority raw` до conntrack. Иначе неуспешная
входящая проба могла закрепить tuple без DNAT и помешать последующему открытию фильтра.
TCP использует обычный stateful NAT без проброса входящих портов. Калибровка проверяет
внешний endpoint на разных назначениях, вход до mapping, незапрошенный вход и разрешённый
вход после исходящего контакта с тем же проверявшим узлом. После калибровки сеть пересоздаётся.
После калибровки ее namespace уничтожаются, затем создается новая сеть с теми же параметрами и пустыми NAT mappings/conntrack. Результаты проб не облегчают подключения реальных процессов. Снимки `network-calibration-initial.json` / `network-calibrated.json` относятся к пробам, `network-initial.json` / `network-final.json` — к uTun; команды содержат inode соответствующего namespace. PID в `namespaces.json` записаны относительно PID namespace контроллера.

19
tests/stream_netns/TESTED.md

@ -2,6 +2,25 @@
Стенд: 16 процессов uTun, 10 NAT-клиентов, 3 страны. Основной network namespace не изменился во всех перечисленных запусках.
Результаты прежних стриминговых прогонов ниже подтверждали сетевые правила шлюзов
и доставку данных. Они не проверяли правильность классификации NAT внутри uTun.
Новая проверка владельца endpoint: `--nat-only --tcp`, артефакты
`/tmp/nat-owner-netns-02/nat-result.json`. Проверены все 16 UDP-профилей и 16 TCP-профилей:
6 UNCHANGED/OPEN, 4 EIM/OPEN, 2 EIM/CONTACT_REQUIRED, 4 DEPENDENT/UNKNOWN;
TCP: 6 публичных OPEN, 10 UNKNOWN/no_listener. Точные внешние адреса совпали с ожидаемыми.
Полная проверка с новым детектором:
`python3 tests/stream_netns/run.py --nat-check --publisher n05 --tcp --duration 75 --rate 2000000 --faults --fault-duration 15 --output /tmp/nat-owner-stream-integration-02`.
PASS: сначала все 16 UDP/TCP-профилей, затем реальные JOIN/Merkle/BGP, 4041 пачка,
перегрузка WAN, recovery, две отписки и END у оставшихся 13 подписчиков.
Ошибок payload/checksum/порядка нет, GAP полностью учитывается. Главный namespace неизменён.
`make -C tests check -j4`: 144 passed, 0 failed, 0 skipped (`/tmp/nat-owner-regressions-02.log`).
Калибровка дополнена переходом «незапрошенный вход → исходящий контакт → вход».
`/tmp/nat-owner-calibration-before-guard` воспроизвёл блокировку последующего UDP
для restricted NAT даже без uTun. После фильтрации запрещённого входа до conntrack
`/tmp/nat-owner-calibration-after-guard` прошёл. Главный namespace не изменился.
| Сценарий | Команда из корня репозитория | Результат |
| --- | --- | --- |
| Публичный UDP-источник, длинный прогон и перегрузка | `python3 tests/stream_netns/run.py --duration 120 --rate 2000000 --faults --output /tmp/stream-netns-final-01` | PASS: 5996 пачек, 3 оборота timestamp, вытеснение из кольца, WAN recovery, unsubscribe, END |

107
tests/stream_netns/nat_scenario.py

@ -0,0 +1,107 @@
"""Проверка детектора uTun на настоящем NAT; контроль не создаёт контакт с помощником."""
import json
from topology import NATS, NODES
def profile(statistics, protocol):
return next(item for item in statistics["address_profiles"] if item["socket"] == protocol)
def check_profiles(scenario):
agents = scenario.agents
public = [node for node in NODES if not node.nat]
transport = "tcp" if scenario.args.tcp_only else "udp"
connected = set()
def connect(source, target):
pair = (source.name, target.name)
if pair in connected:
return
snapshot = agents[target.name].rpc("snapshot")["snapshot"]
agents[source.name].rpc("nat_link", snapshot=snapshot, socket=transport)
target_id = agents[target.name].identity
scenario.wait(f"nat-link-{source.name}-{target.name}",
lambda data: target_id in data[source.name]["direct_up"], 20)
connected.add(pair)
def store_owner(owner, observer):
agents[observer.name].rpc("nat_store", snapshot=agents[owner.name].rpc("snapshot")["snapshot"])
def measure(owner, observer, protocol):
before = profile(agents[owner.name].rpc("stats"), protocol)
agents[owner.name].rpc("nat_check", socket=protocol, observer=agents[observer.name].identity)
data = scenario.wait(f"nat-profile-{owner.name}-{protocol}", lambda statistics:
profile(statistics[owner.name], protocol)["rounds"] > before["rounds"] and
profile(statistics[owner.name], protocol)["phase"] == 0, 40)
return profile(data[owner.name], protocol)
# Отсутствие второго наблюдателя не превращается в STRICT/DIRECT.
first, second = public[:2]
connect(first, second)
if not scenario.args.tcp_only:
incomplete = measure(first, second, "udp")
if (incomplete["mapping"], incomplete["inbound"], incomplete["reason"]) != ("UNKNOWN", "UNKNOWN", "no_helper"):
raise RuntimeError(f"NAT no-helper test: {incomplete}")
verified = {}
for owner in public + [node for node in NODES if node.nat]:
observer = second if owner.name == first.name else first
if not scenario.args.tcp_only:
contacts = set(profile(agents[owner.name].rpc("stats"), "udp")["contacts"])
helper = next((node for node in public if node.name not in {owner.name, observer.name} and node.ip not in contacts), None)
if not helper:
raise RuntimeError(f"{owner.name}: no independent helper remains before cold measurement; contacts={contacts}")
connect(observer, helper)
connect(owner, observer)
store_owner(owner, observer)
protocols = ["tcp"] if scenario.args.tcp_only else ["udp", "tcp"] if scenario.args.tcp else ["udp"]
observed = {}
for protocol in protocols:
if protocol == "tcp" and owner.nat:
# Приватный TCP listener не имеет обнаруженного публичного endpoint.
agents[owner.name].rpc("nat_check", socket="tcp", observer=agents[observer.name].identity)
data = scenario.wait(f"nat-private-tcp-{owner.name}", lambda statistics:
profile(statistics[owner.name], "tcp")["reason"] == "no_listener", 5)
actual = profile(data[owner.name], "tcp")
expected = ("UNKNOWN", "UNKNOWN")
else:
actual = measure(owner, observer, protocol)
if protocol == "tcp" or not owner.nat:
expected = ("UNCHANGED", "OPEN")
else:
gateway = next(item for item in NATS if item.name == owner.nat)
expected = {"cone": ("EIM", "OPEN"), "restricted": ("EIM", "CONTACT_REQUIRED"),
"strict": ("DEPENDENT", "UNKNOWN")}[gateway.mode]
if (actual["mapping"], actual["inbound"]) != expected:
raise RuntimeError(f"{owner.name}/{protocol}: expected {expected}, detector returned {actual}")
if expected[0] == "EIM":
gateway = next(item for item in NATS if item.name == owner.nat)
external = f"{gateway.ip}:{owner.mapped_port}"
elif expected[0] == "UNCHANGED":
external = f"{owner.ip}:32000"
else:
external = ""
if actual["external"] != external:
raise RuntimeError(f"{owner.name}/{protocol}: foreign/invalid external endpoint: {actual}, expected {external}")
observed[protocol] = actual
print(f"NAT {owner.name}/{protocol}: {actual['mapping']} / {actual['inbound']}"
f" external={actual['external'] or '-'} reason={actual['reason']}", flush=True)
verified[owner.name] = observed
(scenario.network.directory / "nat-profiles.json").write_text(json.dumps(verified, indent=2) + "\n")
# Повтор на использовавшемся порту сохраняет mapping, но не обновляет OPEN
# по контактировавшим IP. Проверка запускается после всех холодных измерений.
first_profile = verified[first.name].get("udp")
if first_profile:
again = measure(first, second, "udp")
if again["mapping"] != "UNCHANGED" or again["inbound"] not in {"OPEN", "UNKNOWN"}:
raise RuntimeError(f"{first.name}: warm measurement produced invalid classification: {again}")
if again["inbound"] == "UNKNOWN" and again["reason"] != "contact_history":
raise RuntimeError(f"{first.name}: UNKNOWN without contact-history explanation: {again}")
result = {"nodes_checked": len(verified), "protocols": protocols, "no_helper_checked": not scenario.args.tcp_only,
"warm_recheck_checked": bool(first_profile), "verified": verified}
(scenario.network.directory / "nat-result.json").write_text(json.dumps(result, indent=2) + "\n")
return result

17
tests/stream_netns/network.py

@ -23,6 +23,23 @@ def nat_rules(gateway):
lines.append(f"{match} update @active {{ {node.mapped_port} timeout 60s }}")
elif gateway.mode == "restricted":
lines.append(f"{match} update @contacts {{ ip daddr . udp dport . {node.mapped_port} timeout 60s }}")
elif gateway.mode == "strict":
for index, destination in enumerate(destinations):
port = 40000 + node.number * 100 + index
lines.append(f"{match} ip daddr {destination} update @contacts {{ ip daddr . udp dport . {port} timeout 60s }}")
# Запрещённый вход отсекается ДО conntrack. Иначе первая неуспешная
# входящая проба закрепляет tuple без DNAT и блокирует последующее открытие.
lines += [" }", " chain ingress_guard { type filter hook prerouting priority raw; policy accept;"]
for node in clients:
ports = ([40000 + node.number * 100 + index for index in range(len(destinations))]
if gateway.mode == "strict" else [node.mapped_port])
for port in ports:
match = f' iifname "wan" udp dport {port}'
if gateway.mode == "cone":
lines.append(f"{match} udp dport @active accept")
else:
lines.append(f"{match} ip saddr . udp sport . udp dport @contacts accept")
lines.append(f"{match} counter drop")
lines += [" }", " chain prerouting { type nat hook prerouting priority dstnat; policy accept;"]
for node in clients:
match = f' iifname "wan" udp dport {node.mapped_port}'

155
tests/stream_netns/node.c

@ -21,6 +21,11 @@
#include "../../src/chat/invite_link.h"
#include "../../src/stream/stream_priv.h"
#include "../../src/stream/stream_member.h"
#include "../../src/routing_layer/nat_detection.h"
#include "../../src/routing_layer/route_ping.h"
#include "../../src/routing_layer/topo_node_sqlite.h"
#include "../../src/transport_layer/node_conn_direct.h"
#include "../../src/transport_layer/etcp.h"
#include "../../src/utun_instance.h"
#define LAB_STREAMS 4
@ -40,6 +45,9 @@ struct lab {
struct lab_stream streams[LAB_STREAMS];
struct chat_invite_build *invite;
uint64_t invite_id, group;
struct NODE_CONN_DIRECT *nat_handles[32];
uint64_t nat_records[32];
unsigned nat_handle_count, nat_record_count;
int errors;
};
static volatile sig_atomic_t stopping;
@ -168,7 +176,47 @@ static void statistics(struct lab *lab, uint64_t id, int loads) {
stream_member_load(lab->inst, lab->group, node->node_id, &state, NULL));
}
}
printf("},\"streams\":["); separator = 0;
printf("},\"address_profiles\":["); separator = 0;
for (struct ETCP_SOCKET *socket = lab->inst->etcp_sockets; socket; socket = socket->next) {
if (socket->probe_only) continue;
struct nat_check_status status = {0}; nat_detection_get_status(lab->inst->nat_det, socket, &status);
printf("%s{\"socket\":", separator++ ? "," : ""); json_string(socket->name);
printf(",\"socket_id\":%u,\"generation\":\"%" PRIu64 "\",\"protocol\":\"%s\",\"local\":",
socket->sock_id, socket->generation, socket->is_tcp ? "tcp" : "udp");
json_string(sockaddr_storage_to_str(socket->is_tcp ? &socket->interface_addr : &socket->local_addr).str);
printf(",\"mapping\":\"%s\",\"inbound\":\"%s\",\"evidence\":\"%" PRIu64 "\",\"checked_at\":\"%" PRIu64 "\",\"external\":",
address_mapping_name(socket->address_profile.mapping), address_inbound_name(socket->address_profile.inbound),
socket->address_profile.evidence_id, socket->address_profile.checked_at);
const struct sockaddr_storage *external = &socket->nat_addr;
if (!external->ss_family && socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED)
external = socket->is_tcp ? &socket->interface_addr : &socket->local_addr;
json_string(external->ss_family ? sockaddr_storage_to_str(external).str : "");
printf(",\"phase\":%u,\"reason\":\"%s\",\"observer\":\"%" PRIu64 "\",\"helper\":\"%" PRIu64 "\","
"\"rounds\":%u,\"helper_reselections\":%u,\"history_complete\":%u,\"contacts\":[", status.phase,
nat_check_reason_name(status.reason), status.observer, status.helper, status.rounds,
status.helper_reselections, status.history_complete);
uint8_t local[ROUTE_ADDR_WIRE_SIZE]; sqlite3_stmt *contacts = NULL; int contact_separator = 0;
if (!socket->is_tcp && !route_ping_addr_encode(local, &socket->local_addr) &&
sqlite3_prepare_v2(lab->inst->topo_sqlite_db, "SELECT remote FROM nat_contacts WHERE local=?",
-1, &contacts, NULL) == SQLITE_OK) {
sqlite3_bind_blob(contacts, 1, local, sizeof(local), SQLITE_TRANSIENT);
while (sqlite3_step(contacts) == SQLITE_ROW) {
const uint8_t *address = sqlite3_column_blob(contacts, 0); char text[64];
if (!address || sqlite3_column_bytes(contacts, 0) != 17 || (address[0] != 4 && address[0] != 6)) continue;
if (!inet_ntop(address[0] == 4 ? AF_INET : AF_INET6, address + 1, text, sizeof(text))) continue;
if (contact_separator++) printf(",");
json_string(text);
}
}
sqlite3_finalize(contacts); printf("]}");
}
printf("],\"direct_up\":["); separator = 0;
for (struct ll_entry *entry = lab->inst->connections->head; entry; entry = entry->next) {
const struct ETCP_CONN *connection = ((const struct conn_queue_entry *)entry->data)->conn;
if (connection && connection->links_up && !connection->close_requested && connection->state != 2)
printf("%s\"%" PRIu64 "\"", separator++ ? "," : "", connection->peer_node_id);
}
printf("],\"streams\":["); separator = 0;
for (unsigned i = 0; i < LAB_STREAMS; i++) {
struct lab_stream *stream = &lab->streams[i];
if (!stream->source) continue;
@ -242,6 +290,100 @@ static void invite_ready(void *arg, int result, const char *link) {
json_string(link ? link : ""); printf("}\n");
}
static struct TOPO_NODE *parse_snapshot(struct lab *lab, const char *json) {
char hex[4097]; uint8_t wire[2048]; size_t length;
if (json_flat_get(json, "snapshot", hex, sizeof(hex)) || !(length = strlen(hex)) || length % 2 || length / 2 > sizeof(wire)) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: invalid node snapshot size"); return NULL;
}
for (size_t i = 0; i < length / 2; i++) {
unsigned value;
if (sscanf(hex + i * 2, "%2x", &value) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: invalid snapshot hex"); return NULL; }
wire[i] = (uint8_t)value;
}
struct TOPO_GROUP group = {.instance = lab->inst};
struct TOPO_NODE *node = NULL;
if (topo_node_deserialize(&group, wire, length / 2, &node, NULL, NULL, NULL, NULL) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: snapshot decode failed"); return NULL;
}
if (node->node_id == lab->inst->node_id || topo_node_verify(node) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: snapshot signature/identity rejected");
topo_node_destroy(lab->inst->topo_groups, node); return NULL;
}
return node;
}
/* Стенд переносит подписанные снимки через RPC только до запуска группы.
* NCD handshake, наблюдения и весь трафик всё равно проходят по настоящей сети. */
static int nat_command(struct lab *lab, const char *json, const char *operation, uint64_t id) {
if (!strcmp(operation, "chat_start")) {
int result = chat_service_start(lab->inst);
if (!result) stream_set_event_callback(lab->inst, event, lab);
reply(id, result); return 1;
}
if (!strcmp(operation, "nat_release")) {
for (unsigned i = 0; i < lab->nat_handle_count; i++) node_conn_direct_close(lab->nat_handles[i]);
for (unsigned i = 0; i < lab->nat_record_count; i++) topo_node_registry_unref(lab->inst->topo_groups, lab->nat_records[i]);
lab->nat_handle_count = lab->nat_record_count = 0; reply(id, 0); return 1;
}
if (!strcmp(operation, "nat_auto")) {
uint64_t enabled;
if (number(json, "enabled", &enabled) || enabled > 1) { reply(id, -1); return 1; }
nat_detection_set_automatic(lab->inst->nat_det, enabled); reply(id, 0); return 1;
}
if (!strcmp(operation, "nat_check")) {
char name[32]; uint64_t observer;
if (!json_flat_get(json, "socket", name, sizeof(name)) && !number(json, "observer", &observer)) {
struct ETCP_SOCKET *socket = lab->inst->etcp_sockets;
while (socket && (socket->probe_only || strcmp(socket->name, name))) socket = socket->next;
reply(id, socket ? nat_detection_request_check_socket(lab->inst->nat_det, socket, observer) : -1);
} else { nat_detection_request_check_all(lab->inst->nat_det); reply(id, 0); }
return 1;
}
if (!strcmp(operation, "snapshot")) {
uint8_t wire[2048]; struct TOPO_GROUP_NODE nq = {0};
struct TOPO_NODE *node = topo_node_registry_find(lab->inst->topo_groups, lab->inst->node_id);
int length = topo_node_serialize(node, &nq, 0, 0, wire, sizeof(wire), 0);
if (length < 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: snapshot serialize failed"); reply(id, -1); return 1; }
printf("{\"id\":%" PRIu64 ",\"rc\":0,\"snapshot\":\"", id);
for (int i = 0; i < length; i++) printf("%02x", wire[i]);
printf("\"}\n"); return 1;
}
int store = !strcmp(operation, "nat_store"), link = !strcmp(operation, "nat_link");
if (!store && !link) return 0;
struct TOPO_NODE *node = parse_snapshot(lab, json);
if (!node) { reply(id, -1); return 1; }
int result = 0;
if (link) {
char source[32]; struct ETCP_SOCKET *socket = NULL;
if (lab->nat_handle_count < 32 && !json_flat_get(json, "socket", source, sizeof(source)))
for (socket = lab->inst->etcp_sockets; socket; socket = socket->next)
if (!socket->probe_only && !strcmp(socket->name, source)) break;
if (!socket) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: native link source/handle unavailable"); result = -1; }
else {
struct NODE_CONN_DIRECT *handle = NULL;
int opened = node_conn_direct_open_node(lab->inst, node->node_id, NULL, NULL, &handle, node, socket);
if (opened == NCD_ERR || !handle) result = -1;
else lab->nat_handles[lab->nat_handle_count++] = handle;
}
topo_node_destroy(lab->inst->topo_groups, node);
} else {
if (lab->nat_record_count == 32) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: imported snapshot limit reached"); result = -1;
topo_node_destroy(lab->inst->topo_groups, node);
} else {
struct TOPO_NODE *stored = topo_node_registry_store(lab->inst->topo_groups, node);
if (!stored) { topo_node_destroy(lab->inst->topo_groups, node); result = -1; }
else {
unsigned i;
for (i = 0; i < lab->nat_record_count; i++) if (lab->nat_records[i] == stored->node_id) break;
if (i == lab->nat_record_count) lab->nat_records[lab->nat_record_count++] = stored->node_id;
else topo_node_registry_unref(lab->inst->topo_groups, stored->node_id);
}
}
}
reply(id, result); return 1;
}
static void command(struct lab *lab, const char *json) {
uint64_t id = 0, source = 0, value = 0;
char operation[32];
@ -251,6 +393,7 @@ static void command(struct lab *lab, const char *json) {
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "lab: RPC id=%" PRIu64 " op=%s", id, operation);
if (!strcmp(operation, "stats")) { number(json, "loads", &value); statistics(lab, id, value != 0); return; }
if (!strcmp(operation, "quit")) { stopping = 1; reply(id, 0); return; }
if (nat_command(lab, json, operation, id)) return;
if (!strcmp(operation, "create")) {
if (lab->group) { reply(id, -1); return; }
chat_core_create_channel_auto(lab->inst, "stream-netns");
@ -354,7 +497,8 @@ int main(int argc, char **argv) {
for (unsigned i = 0; i < 32; i++) printf("%02x", public_key[i]);
printf("\",\"node\":\"%" PRIu64 "\"}\n", sc_derive_node_id_from_pubkey(public_key)); return 0;
}
if (argc != 3) { fprintf(stderr, "Usage: %s CONFIG LOG | --identity SEED\n", argv[0]); return 1; }
int core_only = argc == 4 && !strcmp(argv[3], "--core-only");
if (argc != 3 && !core_only) { fprintf(stderr, "Usage: %s CONFIG LOG [--core-only] | --identity SEED\n", argv[0]); return 1; }
setvbuf(stdout, NULL, _IOLBF, 0);
signal(SIGTERM, stop_signal); signal(SIGINT, stop_signal); signal(SIGPIPE, SIG_IGN);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_enable_console(0); debug_enable_file_output(argv[2], 1);
@ -363,11 +507,12 @@ int main(int argc, char **argv) {
struct lab *lab = u_calloc(1, sizeof(*lab));
if (!async || !lab) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: initialization allocation failed"); return 1; }
lab->inst = utun_instance_create(async, argv[1]);
if (!lab->inst || utun_core_start(lab->inst) || chat_service_start(lab->inst)) {
if (!lab->inst || utun_core_start(lab->inst) || (!core_only && chat_service_start(lab->inst))) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: service startup failed"); return 1;
}
if (core_only) nat_detection_set_automatic(lab->inst->nat_det, 0);
debug_enable_console(0);
stream_set_event_callback(lab->inst, event, lab);
if (!core_only) stream_set_event_callback(lab->inst, event, lab);
if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: stdin nonblocking failed: %s", strerror(errno)); return 1;
}
@ -397,6 +542,8 @@ int main(int argc, char **argv) {
if (lab->streams[i].errors) lab->errors++;
}
int errors = lab->errors;
for (unsigned i = 0; i < lab->nat_handle_count; i++) node_conn_direct_force_close(lab->nat_handles[i]);
for (unsigned i = 0; i < lab->nat_record_count; i++) topo_node_registry_unref(lab->inst->topo_groups, lab->nat_records[i]);
utun_instance_destroy(lab->inst); uasync_destroy(async, 1); u_free(lab);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "lab: stopped errors=%d", errors);
debug_disable_file_output(); return errors ? 1 : 0;

8
tests/stream_netns/probe.py

@ -121,9 +121,15 @@ class Probes:
expected = (gateway.mode != "strict", gateway.mode == "cone")
if (stable, inbound) != expected:
raise RuntimeError(f"NAT {node.name}/{gateway.mode}: stable={stable}, unsolicited={inbound}, expected={expected}")
opened = self.exchange(node.name, c.ip, count=3)
if not opened["observed"]:
raise RuntimeError(f"NAT {node.name}/{gateway.mode}: outgoing to previous unsolicited sender failed: {opened}")
after = self.exchange(c.name, gateway.ip, opened["observed"][1], count=3, timeout=0.4)
if not after["observed"]:
raise RuntimeError(f"NAT {node.name}/{gateway.mode}: incoming failed after contact: {after}")
results["nat"].append({"node": node.name, "gateway": gateway.name, "mode": gateway.mode,
"first": first["observed"], "second": second["observed"],
"stable": stable, "unsolicited": inbound})
"stable": stable, "unsolicited": inbound, "after_contact": bool(after["observed"])})
return results
def close(self):

6
tests/stream_netns/run.py

@ -92,6 +92,8 @@ def main():
parser.add_argument("--output", help="New directory for logs and results")
parser.add_argument("--seed", type=int, default=20261011)
parser.add_argument("--network-only", action="store_true", help="Only topology, WAN and NAT calibration")
parser.add_argument("--nat-check", action="store_true", help="Verify owned endpoint profiles before real JOIN/streaming")
parser.add_argument("--nat-only", action="store_true", help="Only real uTun endpoint detection after network calibration")
parser.add_argument("--binary", default=str(Path(__file__).parent / "stream_lab_node"))
parser.add_argument("--publisher", choices=[f"n{i:02d}" for i in range(1, 17)], default="n01")
parser.add_argument("--rate", type=int, default=1000000, help="Application bitrate in bits/s, before FEC")
@ -125,6 +127,10 @@ def main():
"--fault-duration", str(args.fault_duration)]
if args.network_only:
command.append("--network-only")
if args.nat_check:
command.append("--nat-check")
if args.nat_only:
command.append("--nat-only")
if args.faults:
command.append("--faults")
if args.tcp:

27
tests/stream_netns/scenario.py

@ -11,7 +11,7 @@ from topology import NODES, WANS
class Agent:
def __init__(self, network, node, binary, seed, tcp, tcp_only):
def __init__(self, network, node, binary, seed, tcp, tcp_only, core_only=False):
self.node = node
directory = network.directory / node.name
directory.mkdir()
@ -26,12 +26,15 @@ class Agent:
f"db_path={directory / 'db'}\ndebug_level=warn\n"
"[ntp]\nenabled=no\n[allowed_keys]\nallow_all=1\n"
"[chatserver]\ngroup_autoconnect=1\nstorage_autoload=0\nmember_files_enabled=0\n"
"[debug]\nstream=debug\nchat_sync=info\nmember_sync=info\nbgp=info\nconnection=info\n" + sockets)
"[debug]\nstream=debug\nnat=debug\nchat_sync=info\nmember_sync=info\nbgp=info\nconnection=info\n" + sockets)
path = directory / "node.conf"
path.write_text(config)
self.stderr = (directory / "stderr.log").open("w")
self.transcript = (directory / "rpc.jsonl").open("w", buffering=1)
self.process = network.spawn(node.name, [str(binary), str(path), str(directory / "utun.log")],
command = [str(binary), str(path), str(directory / "utun.log")]
if core_only:
command.append("--core-only")
self.process = network.spawn(node.name, command,
stdin=-1, stdout=-1, stderr=self.stderr, text=True, bufsize=1)
self.serial = 0
if self.read(10) != {"ready": True, "node": self.identity}:
@ -133,8 +136,19 @@ class Scenario:
def run(self):
binary = Path(self.args.binary).resolve()
nat_check = self.args.nat_check or self.args.nat_only
for node in NODES:
self.agents[node.name] = Agent(self.network, node, binary, self.args.seed, self.args.tcp, self.args.tcp_only)
self.agents[node.name] = Agent(self.network, node, binary, self.args.seed, self.args.tcp, self.args.tcp_only, nat_check)
nat_result = None
if nat_check:
from nat_scenario import check_profiles
nat_result = check_profiles(self)
if self.args.nat_only:
return {"pass": True, "nat": nat_result, "transport": "tcp" if self.args.tcp_only else
"udp+tcp" if self.args.tcp else "udp"}
for agent in self.agents.values():
agent.rpc("nat_auto", enabled=1)
agent.rpc("chat_start")
owner = self.agents["n01"]
group = owner.rpc("create")["group"]
print(f"Joining real CHAT group {group}", flush=True)
@ -146,6 +160,9 @@ class Scenario:
self.wait("join-merkle-bgp", lambda s: all(set(n["online"]) == identities and
set(n["profile_status"]) == identities and all(rc == 0 for rc in n["profile_status"].values())
for n in s.values()), 120)
if nat_check:
for agent in self.agents.values():
agent.rpc("nat_release")
source = self.agents[self.args.publisher]
source_id = source.identity
source.rpc("publish", seed=self.args.seed)
@ -207,7 +224,7 @@ class Scenario:
return {"pass": True, "group": group, "publisher": self.args.publisher, "generated": generated,
"phases": self.phases, "subscribers_checked": len(names), "source_timestamp_wraps": self.clock_wraps,
"transport": "tcp" if self.args.tcp_only else "udp+tcp" if self.args.tcp else "udp",
"rate_bps": self.args.rate}
"rate_bps": self.args.rate, "nat": nat_result}
def report(self, final, source):
rows = []

2
tests/test_auto_socket_dynamic.c

@ -192,7 +192,7 @@ static struct TOPO_GROUP_NODE* mk_srv_node(void) {
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(ctx.client->topo_groups->v4_sock_meta_pool);
if (!sm) { u_free(nq); u_free(ni); return NULL; }
memset(sm, 0, sizeof(*sm));
sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN;
sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0};
sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm;
struct TOPO_ADDR4* a_udp = memory_pool_alloc(ctx.client->topo_groups->v4_addr_pool);

2
tests/test_etcp_connect.c

@ -80,7 +80,7 @@ static struct TOPO_GROUP_NODE* mknode(struct UTUN_INSTANCE* inst, uint64_t nid,
nq->ll.size = sizeof(struct TOPO_GROUP_NODE) - sizeof(struct ll_entry);
nq->node_id = nid;
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(inst->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0}; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(inst->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0] = a; addr->addr[1] = b; addr->addr[2] = c; addr->addr[3] = d; addr->port = port; addr->type = TOPO_ADDR_INTERFACE; addr->socket_id = 0; addr->protocol = TOPO_PROTO_UDP; addr->next = ni->v4_addrs; ni->v4_addrs = addr; }
topo_node_registry_store(inst->topo_groups, ni);

24
tests/test_etcp_two_instances.c

@ -161,14 +161,15 @@ static void monitor_connections(void* arg) {
if (link->initialized) {
client_initialized = 1;
// Check NAT fields are populated
const struct sockaddr_in *observed = (const struct sockaddr_in *)&link->local_as_seen_by_peer;
// Наблюдение относится к нашему исходящему endpoint.
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] Checking NAT fields...");
if (link->nat_ip == 0) {
if (link->local_as_seen_by_peer.ss_family != AF_INET || observed->sin_addr.s_addr == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: nat_ip is 0 (should be set)");
test_completed = 2; // Failure
return;
}
if (link->nat_port == 0) {
if (ntohs(observed->sin_port) == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: nat_port is 0 (should be set)");
test_completed = 2; // Failure
return;
@ -177,33 +178,32 @@ static void monitor_connections(void* arg) {
// Convert nat_ip to string for display
char nat_ip_str[INET_ADDRSTRLEN];
struct in_addr nat_addr;
nat_addr.s_addr = link->nat_ip; // уже в network byte order
nat_addr.s_addr = observed->sin_addr.s_addr; // уже в network byte order
inet_ntop(AF_INET, &nat_addr, nat_ip_str, sizeof(nat_ip_str));
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] PASS: NAT address is set: %s:%u",
nat_ip_str, link->nat_port);
nat_ip_str, ntohs(observed->sin_port));
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] PASS: nat_changes_count=%u, nat_hits_count=%u",
link->nat_changes_count, link->nat_hits_count);
// Verify NAT IP is 127.0.0.1 (localhost) since client connects from localhost
// В network byte order: 127.0.0.1 = 0x0100007F
if (link->nat_ip != 0x0100007F) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: NAT IP is not 127.0.0.1 (expected 0x0100007F, got 0x%08X)",
link->nat_ip);
if (observed->sin_addr.s_addr != htonl(0x7f000001)) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: Observed IP is not 127.0.0.1 (got 0x%08X)",
observed->sin_addr.s_addr);
test_completed = 2; // Failure
return;
}
// Verify NAT port matches client's local port exactly (no NAT in test)
if (expected_client_port != 0 && link->nat_port != expected_client_port) {
if (expected_client_port != 0 && ntohs(observed->sin_port) != expected_client_port) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: NAT port %u does not match expected port %u",
link->nat_port, expected_client_port);
ntohs(observed->sin_port), expected_client_port);
test_completed = 2; // Failure
return;
}
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] PASS: NAT IP and port match exactly (127.0.0.1:%u)",
link->nat_port);
ntohs(observed->sin_port));
}
}
link = link->next;

3
tests/test_ipv6_sockets.c

@ -164,7 +164,8 @@ static int verify_ipv6_local_nodeinfo(const char* name, struct UTUN_INSTANCE* in
{ const struct TOPO_SOCKMETA6* m = ni->v6_sock_meta; int i = 0;
while (m) {
if (m->config_type != CFG_SERVER_TYPE_PUBLIC) { printf("FAIL [%s]: v6 socket meta[%d] config_type=%d expected=%d\n", name, i, m->config_type, CFG_SERVER_TYPE_PUBLIC); return 0; }
printf("PASS [%s]: v6 socket meta[%d] id=%d config=%d nat=%d\n", name, i, m->id, m->config_type, m->nat_type);
printf("PASS [%s]: v6 socket meta[%d] id=%d config=%d mapping=%s inbound=%s\n", name, i, m->id, m->config_type,
address_mapping_name(m->address_profile.mapping), address_inbound_name(m->address_profile.inbound));
i++; m = m->next;
}
}

640
tests/test_nat_detection.c

@ -1,527 +1,125 @@
/**
* @file test_nat_detection.c
* @brief Интеграционный тест детекции типа NAT через три узла
*
* Топология: C1 -> S <- C2
* - S получает NODEINFO от C1 (direct peer)
* - S использует C2 как третий узел для NAT-пинга C1
* - C2 пингует C1, результат возвращается S
* - S отправляет NAT_INFO C1
* - Проверяем корректность заполнения всех NAT-полей
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "test_utils.h"
#include "test_net.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "routing.h"
#include "topo_group.h"
#include "route_ping.h"
#include "nat_detection.h"
#include "topo_node.h"
#include "../src/tun_if.h"
#include "secure_channel.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#define TEST_TIMEOUT_MS 5000
static uint64_t g_node_id_s = 0;
static uint64_t g_node_id_c1 = 0;
static uint64_t g_node_id_c2 = 0;
static struct UTUN_INSTANCE* inst_s = NULL;
static struct UTUN_INSTANCE* inst_c1 = NULL;
static struct UTUN_INSTANCE* inst_c2 = NULL;
static struct UASYNC* ua = NULL;
static int test_timed_out = 0;
static void* test_timeout_id = NULL;
static char temp_dir[] = "/tmp/utun_nat_test_XXXXXX";
static char config_s[256];
static char config_c1[256];
static char config_c2[256];
static struct {
int done;
int success;
uint16_t avg_rtt;
uint8_t count_sent;
uint8_t count_ok;
} nat_ping_result;
static int write_config(const char* path, const char* content) {
FILE* f = fopen(path, "w");
if (!f) return -1;
fprintf(f, "%s", content);
fclose(f);
return 0;
}
static char* get_pubkey_from_config(const char* path) {
struct utun_config* cfg = parse_config(path);
if (!cfg) return NULL;
char* pub = strdup(cfg->global.my_public_key_hex);
free_config(cfg);
return pub;
}
static int create_temp_configs(void) {
if (test_mkdtemp(temp_dir) != 0) {
fprintf(stderr, "Failed to create temp directory\n");
return -1;
}
snprintf(config_s, sizeof(config_s), "%s/s.conf", temp_dir);
snprintf(config_c1, sizeof(config_c1), "%s/c1.conf", temp_dir);
snprintf(config_c2, sizeof(config_c2), "%s/c2.conf", temp_dir);
const char* tpl_s =
"[global]\n"
"tun_ip=10.100.0.1/24\n"
"tun_ifname=tun100\n"
"\n"
"[server:test_s]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n";
if (write_config(config_s, tpl_s) != 0) return -1;
if (config_ensure_keys_and_node_id(config_s) != 0) return -1;
char* pub_s = get_pubkey_from_config(config_s);
if (!pub_s) return -1;
const char* tpl_c2 =
"[global]\n"
"tun_ip=10.100.0.3/24\n"
"tun_ifname=tun103\n"
"\n"
"[server:test_c2]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[client:to_s]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_c2:127.0.0.1:0\n";
char tpl_c2_full[4096];
snprintf(tpl_c2_full, sizeof(tpl_c2_full), tpl_c2, pub_s);
if (write_config(config_c2, tpl_c2_full) != 0) { free(pub_s); return -1; }
if (config_ensure_keys_and_node_id(config_c2) != 0) { free(pub_s); return -1; }
char* pub_c2 = get_pubkey_from_config(config_c2);
if (!pub_c2) { free(pub_s); return -1; }
const char* tpl_c1 =
"[global]\n"
"tun_ip=10.100.0.2/24\n"
"tun_ifname=tun102\n"
"\n"
"[server:test_c1]\n"
"addr=127.0.0.1:0\n"
"type=nat\n"
"\n"
"[client:to_s]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_c1:127.0.0.1:0\n";
char tpl_c1_full[4096];
snprintf(tpl_c1_full, sizeof(tpl_c1_full), tpl_c1, pub_s);
free(pub_s);
if (write_config(config_c1, tpl_c1_full) != 0) { free(pub_c2); return -1; }
if (config_ensure_keys_and_node_id(config_c1) != 0) { free(pub_c2); return -1; }
free(pub_c2);
return 0;
}
static void cleanup_temp_configs(void) {
test_unlink(config_s);
test_unlink(config_c1);
test_unlink(config_c2);
test_rmdir(temp_dir);
}
static int has_initialized_link(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->connections) return 0;
struct ll_entry* entry = inst->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
struct ETCP_CONN* conn = ce->conn;
struct ETCP_LINK* link = conn->links;
while (link) {
if (link->initialized) return 1;
link = link->next;
}
entry = entry->next;
}
return 0;
/* Детектор владеет профилем сокета; помощник с IP наблюдателя заменяется.
* Настоящие ETCP/NCD/PING/STCP, без групповой автосвязности и без подмены времени. */
#include "nat_fixture.h"
#include "ntp_time.h"
static void wait_profile(struct nat_fixture *fixture, struct ETCP_SOCKET *socket, uint8_t mapping, uint8_t inbound) {
uint64_t end = get_time_tb() + 180000;
while (get_time_tb() < end) {
if (socket->address_profile.evidence_id && socket->address_profile.mapping == mapping &&
socket->address_profile.inbound == inbound) return;
uasync_poll(fixture->ua, 10);
}
struct nat_check_status status = {0};
nat_detection_get_status(socket->instance->nat_det, socket, &status);
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "profile wait failed socket=%s mapping=%s inbound=%s phase=%u reason=%s",
socket->name, address_mapping_name(socket->address_profile.mapping),
address_inbound_name(socket->address_profile.inbound), status.phase, nat_check_reason_name(status.reason));
abort();
}
static struct ETCP_LINK* find_link_to_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!inst || !inst->connections) return NULL;
struct ll_entry* entry = queue_find_data_by_index(inst->connections, (const uint8_t*)&node_id);
return entry ? ((struct conn_queue_entry*)entry->data)->conn->links : NULL;
}
static void test_timeout_cb(void* arg) {
(void)arg;
test_timed_out = 1;
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "test_nat_detection: overall test timeout");
}
static void nat_ping_resp_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) {
(void)arg;
nat_ping_result.done = 1;
nat_ping_result.success = success;
nat_ping_result.avg_rtt = avg_rtt;
nat_ping_result.count_sent = count_sent;
nat_ping_result.count_ok = count_ok;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "nat_ping_resp_cb: success=%d avg_rtt=%u sent=%u ok=%u",
success, (unsigned)avg_rtt, (unsigned)count_sent, (unsigned)count_ok);
static void check_snapshot(struct UTUN_INSTANCE *owner, struct ETCP_SOCKET *socket) {
struct TOPO_NODE *node = topo_node_registry_find(owner->topo_groups, owner->node_id);
assert(node && topo_node_verify(node) == 0);
struct TOPO_SOCKMETA4 *meta = node->v4_sock_meta;
while (meta && meta->id != socket->sock_id) meta = meta->next;
assert(meta && meta->address_profile.evidence_id == socket->address_profile.evidence_id &&
meta->address_profile.checked_at == socket->address_profile.checked_at &&
meta->address_profile.mapping == socket->address_profile.mapping &&
meta->address_profile.inbound == socket->address_profile.inbound);
struct address_profile saved = meta->address_profile;
meta->address_profile.checked_at++;
assert(topo_node_verify(node) < 0);
meta->address_profile = saved;
meta->address_profile.inbound = ADDRESS_IN_CONTACT;
assert(topo_node_verify(node) < 0);
meta->address_profile = saved; assert(topo_node_verify(node) == 0);
sqlite3_stmt *statement = NULL;
assert(sqlite3_prepare_v2(owner->topo_sqlite_db,
"SELECT nat_mapping,nat_inbound,nat_evidence,nat_checked FROM node_addresses WHERE node_id=? AND socket_id=?",
-1, &statement, NULL) == SQLITE_OK);
sqlite3_bind_int64(statement, 1, (sqlite3_int64)owner->node_id); sqlite3_bind_int(statement, 2, socket->sock_id);
assert(sqlite3_step(statement) == SQLITE_ROW);
assert(sqlite3_column_int(statement, 0) == saved.mapping && sqlite3_column_int(statement, 1) == saved.inbound);
assert((uint64_t)sqlite3_column_int64(statement, 2) == saved.evidence_id);
assert((uint64_t)sqlite3_column_int64(statement, 3) == saved.checked_at);
sqlite3_finalize(statement);
}
int main(void) {
int test_result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING);
utun_instance_set_tun_init_enabled(0);
if (create_temp_configs() != 0) {
fprintf(stderr, "Failed to create temp configs\n");
return 1;
}
ua = uasync_create();
if (!ua) {
cleanup_temp_configs();
return 1;
}
inst_s = utun_instance_create(ua, config_s);
inst_c1 = utun_instance_create(ua, config_c1);
inst_c2 = utun_instance_create(ua, config_c2);
if (!inst_s || !inst_c1 || !inst_c2) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create instances");
goto cleanup;
}
test_etcp_set_peer(inst_c1, "to_s", inst_s, "test_s");
test_etcp_set_peer(inst_c2, "to_s", inst_s, "test_s");
// Разрешаем NAT check для localhost (для теста)
if (utun_instance_init(inst_s) != 0 || utun_instance_init(inst_c1) != 0 || utun_instance_init(inst_c2) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to init connections");
goto cleanup;
}
if (topo_groups_get_default(inst_s->topo_groups)) topo_group_set_nat_check_local(topo_groups_get_default(inst_s->topo_groups), 1);
g_node_id_s = inst_s->node_id; g_node_id_c1 = inst_c1->node_id; g_node_id_c2 = inst_c2->node_id;
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout_cb, "test_nat");
// 1. Wait for links C1->S and C2->S to initialize
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Waiting for ETCP links to initialize...");
// Wait for links to be initialized instead of fixed 100 iterations
while (!test_timed_out) {
if (has_initialized_link(inst_c1) && has_initialized_link(inst_c2)) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Links C1->S and C2->S initialized");
break;
}
uasync_poll(ua, 10);
}
if (test_timed_out) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Timeout waiting for links");
goto cleanup;
}
// 2. Wait for BGP exchange so S learns about C1 and C2
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for BGP exchange (S learns C1 and C2)...");
int bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
if (topo_groups_get_default(inst_s->topo_groups) && topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1) != NULL &&
topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c2) != NULL) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "S learned about C1 and C2");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!topo_groups_get_default(inst_s->topo_groups) || topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1) == NULL ||
topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c2) == NULL) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "S did not learn about C1/C2 in time");
goto cleanup;
}
// Verify C1 socket has config type before NAT detection
{
struct ETCP_SOCKET* s = inst_c1->etcp_sockets;
if (!s || s->type != CFG_SERVER_TYPE_NAT) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C1 socket type=%d, expected CFG_SERVER_TYPE_NAT(%d) before detection",
s ? (int)s->type : -1, CFG_SERVER_TYPE_NAT);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C1 socket has config NAT type before detection");
}
// 3. Wait for NAT detection to complete for C1 - check link on server
struct ETCP_LINK* link_sc1 = find_link_to_node(inst_s, g_node_id_c1);
if (!link_sc1) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "No link to C1 on server");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for NAT detection to complete for C1...");
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 1500) {
if (link_sc1->nat_check_status == NAT_CHECK_EIM) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection completed for C1: EIM");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!link_sc1 || link_sc1->nat_check_status != NAT_CHECK_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NAT detection did not complete for C1");
goto cleanup;
}
// 4. Wait for C1 to receive NAT_INFO (verified type)
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C1 to receive NAT_INFO...");
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
struct ETCP_SOCKET* sock = inst_c1->etcp_sockets;
int found = 0;
while (sock) {
if (sock->nat_type == NAT_VERIFIED_EIM) { found = 1; break; }
sock = sock->next;
}
if (found) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C1 received NAT_INFO");
/* Даем время для получения NODEINFO с обновленным local_node */
for (int i = 0; i < 50 && !test_timed_out; i++) {
uasync_poll(ua, 10);
}
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
// 5. Verify all NAT fields on server
struct TOPO_GROUP_NODE* node_c1 = topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1);
if (!node_c1) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "TOPO_GROUP_NODE for C1 disappeared");
goto cleanup;
}
if (link_sc1->nat_check_status != NAT_CHECK_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_check_status=%d, expected EIM(%d)",
(int)link_sc1->nat_check_status, NAT_CHECK_EIM);
goto cleanup;
}
if (link_sc1->nat_type != NAT_TYPE_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_type=%d, expected EIM(%d)",
(int)link_sc1->nat_type, NAT_TYPE_EIM);
goto cleanup;
}
if (link_sc1->nat_ip == 0 || link_sc1->nat_port == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_ip/port not set (ip=%08x port=%u)",
(unsigned)link_sc1->nat_ip, (unsigned)link_sc1->nat_port);
goto cleanup;
}
// Verify link nat_type on server
struct ETCP_CONN* conn_sc1 = NULL;
{ struct ll_entry* e = inst_s->connections->head;
while (e) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (ce->conn->peer_node_id == g_node_id_c1) { conn_sc1 = ce->conn; break; }
e = e->next;
} }
if (!link_sc1 || link_sc1->nat_type != NAT_TYPE_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: link nat_type not EIM on server");
goto cleanup;
}
// Verify socket nat_type on C1
struct ETCP_SOCKET* sock_c1 = NULL;
struct ETCP_SOCKET* sock = inst_c1->etcp_sockets;
while (sock) {
if (sock->nat_type == NAT_VERIFIED_EIM) {
sock_c1 = sock;
break;
}
sock = sock->next;
}
if (!sock_c1) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no socket with NAT_VERIFIED_EIM on C1");
goto cleanup;
}
// Verify local_node socket update on C1 (received NAT_INFO updated nodeinfo)
if (topo_groups_get_default(inst_c1->topo_groups) && topo_groups_get_default(inst_c1->topo_groups)->local_node) {
struct TOPO_NODE* lni = topo_node_registry_find(inst_c1->topo_groups, topo_groups_get_default(inst_c1->topo_groups)->local_node->node_id);
if (lni) {
int found = 0;
for (const struct TOPO_SOCKMETA4* m = lni->v4_sock_meta; m; m = m->next) {
if (m->id == sock_c1->sock_id) {
if (m->nat_type != NAT_VERIFIED_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: local_node socket meta not updated on C1: nat_type=%u expected=%u", m->nat_type, NAT_VERIFIED_EIM);
goto cleanup;
}
found = 1; break;
}
}
if (!found) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: socket not found in local_node meta on C1"); goto cleanup; }
// Verify NAT addr in flat address list
int nat_found = 0;
for (const struct TOPO_ADDR4* a = lni->v4_addrs; a; a = a->next) {
if (a->type == TOPO_ADDR_NAT && a->socket_id == sock_c1->sock_id) {
uint32_t addr_ip; memcpy(&addr_ip, a->addr, 4);
if (addr_ip != link_sc1->nat_ip || a->port != link_sc1->nat_port) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: NAT addr mismatch in local_node: addr=%08x nat_ip=%08x port=%u nat_port=%u",
addr_ip, link_sc1->nat_ip, a->port, link_sc1->nat_port);
goto cleanup;
}
nat_found = 1; break;
}
}
if (!nat_found) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: NAT addr not found in local_node flat addr list on C1"); goto cleanup; }
DEBUG_INFO(DEBUG_CATEGORY_BGP, "local_node meta+addr check PASSED on C1");
}
}
// Wait for C2 to learn updated C1 nodeinfo (with verified NAT type)
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C2 to learn updated C1 nodeinfo...");
bgp_wait_cycles = 0;
int c2_verified_nat = 0;
struct TOPO_GROUP_NODE* node_c1_on_c2 = NULL;
struct TOPO_NODE* cni = NULL;
while (!test_timed_out && bgp_wait_cycles < 500) {
node_c1_on_c2 = topo_groups_get_default(inst_c2->topo_groups) ? topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) : NULL;
cni = node_c1_on_c2 ? topo_node_registry_find(inst_c2->topo_groups, node_c1_on_c2->node_id) : NULL;
if (node_c1_on_c2 && cni) {
for (const struct TOPO_SOCKMETA4* m = cni->v4_sock_meta; m; m = m->next) {
if (m->nat_type == NAT_VERIFIED_EIM) { c2_verified_nat = 1; break; }
}
if (c2_verified_nat) break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!c2_verified_nat) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C2 did not receive updated C1 nodeinfo with verified NAT");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C2 received updated C1 nodeinfo with verified NAT");
// 6. Separate STUN check via explicit route_ping_send_req_addr
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Performing explicit STUN ping from S via C2 to C1...");
struct ETCP_CONN* conn_sc2 = NULL;
struct ll_entry* entry = inst_s->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce->conn->peer_node_id == g_node_id_c2) { conn_sc2 = ce->conn; break; }
entry = entry->next;
}
if (!conn_sc2) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: S has no connection to C2");
goto cleanup;
}
memset(&nat_ping_result, 0, sizeof(nat_ping_result));
/* nat_ip уже в network byte order, API тоже принимает NBO */
uint32_t target_ip_net = link_sc1->nat_ip;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TEST: sending ping with pubkey, nat_ip_net=0x%08x nat_port=%u",
link_sc1->nat_ip, link_sc1->nat_port);
int ret = route_ping_send_req_addr(inst_s->nat_det, topo_groups_get_default(inst_s->topo_groups), conn_sc2,
target_ip_net, link_sc1->nat_port,
3, 10, 200, 3000, nat_ping_resp_cb, NULL,
node_c1_on_c2 ? topo_node_registry_find(inst_c2->topo_groups, node_c1_on_c2->node_id)->public_key : NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr failed: %d", ret);
goto cleanup;
}
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500 && !nat_ping_result.done) {
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!nat_ping_result.done) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: STUN ping timeout");
goto cleanup;
}
if (!nat_ping_result.success || nat_ping_result.count_ok == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: STUN ping failed success=%d ok=%u",
nat_ping_result.success, (unsigned)nat_ping_result.count_ok);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "STUN check PASSED");
// 7. Test route_ping_send_req_addr WITHOUT embedded pubkey (C2 should resolve pubkey from node_id)
// First wait for C2 to learn C1's nodeinfo via BGP exchange from S
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C2 to learn C1 nodeinfo...");
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
if (topo_groups_get_default(inst_c2->topo_groups) && topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) != NULL) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C2 learned C1 nodeinfo");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!topo_groups_get_default(inst_c2->topo_groups) || topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) == NULL) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C2 did not learn C1 nodeinfo in time");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Testing route ping without embedded pubkey...");
memset(&nat_ping_result, 0, sizeof(nat_ping_result));
/* nat_ip уже в network byte order, API тоже принимает NBO */
target_ip_net = link_sc1->nat_ip;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TEST: sending ping NO pubkey, nat_ip_net=0x%08x nat_port=%u",
link_sc1->nat_ip, link_sc1->nat_port);
ret = route_ping_send_req_addr(inst_s->nat_det, topo_groups_get_default(inst_s->topo_groups), conn_sc2,
target_ip_net, link_sc1->nat_port,
3, 10, 200, 3000, nat_ping_resp_cb, NULL,
cni->public_key);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr (no pubkey) failed: %d", ret);
goto cleanup;
}
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500 && !nat_ping_result.done) {
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!nat_ping_result.done) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no-pubkey ping timeout");
goto cleanup;
}
if (!nat_ping_result.success || nat_ping_result.count_ok == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no-pubkey ping failed success=%d ok=%u",
nat_ping_result.success, (unsigned)nat_ping_result.count_ok);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "No-pubkey ping check PASSED");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection test PASSED");
test_result = 0;
cleanup:
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id);
if (inst_s) utun_instance_destroy(inst_s);
if (inst_c1) utun_instance_destroy(inst_c1);
if (inst_c2) utun_instance_destroy(inst_c2);
if (ua) uasync_destroy(ua, 0);
cleanup_temp_configs();
return test_result;
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG);
struct nat_fixture fixture; nat_fixture_init(&fixture);
struct SC_MYKEYS first, second;
assert(sc_generate_keypair(&first) == SC_OK && sc_generate_keypair(&second) == SC_OK);
if (sc_derive_node_id_from_pubkey(first.public_key) > sc_derive_node_id_from_pubkey(second.public_key)) {
struct SC_MYKEYS swap = first; first = second; second = swap;
}
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL,
"[server:second]\naddr=127.0.0.5:0\ntype=public\n"
"[server:tcp]\naddr=127.0.0.2:0\ntype=public\ntransport=tcp\n");
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL);
struct UTUN_INSTANCE *same_ip = nat_fixture_node(&fixture, "127.0.0.3", &first, NULL);
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", &second, NULL);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp");
struct ETCP_SOCKET *second_socket = test_etcp_bound_socket(owner, "second");
struct ETCP_SOCKET *tcp = test_etcp_bound_socket(owner, "tcp");
struct ETCP_SOCKET *observer_socket = test_etcp_bound_socket(observer, "udp");
assert(socket->sock_id != observer_socket->sock_id);
/* B уже знает обоих помощниках, но A не отправлял UDP ни одному из них. */
nat_fixture_connect(&fixture, observer, "udp", same_ip);
nat_fixture_connect(&fixture, observer, "udp", helper);
struct ETCP_CONN *connection = nat_fixture_connect(&fixture, owner, "udp", observer);
assert(connection->links && connection->links->remote_socket_id == observer_socket->sock_id);
assert(connection->links->local_as_seen_by_peer.ss_family == AF_INET);
assert(route_ping_addr_equal(&connection->links->local_as_seen_by_peer, &socket->local_addr, 1));
/* Подписанная запись A нужна независимому испытанию настоящего TCP listener. */
nat_fixture_connect(&fixture, observer, "udp", owner);
owner->ntp.reachable = 0; /* ни измерение, ни публикация не требуют интернет-NTP */
nat_detection_request_check_all(owner->nat_det);
wait_profile(&fixture, socket, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
wait_profile(&fixture, second_socket, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
wait_profile(&fixture, tcp, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
struct nat_check_status status;
assert(nat_detection_get_status(owner->nat_det, socket, &status) == 0);
assert(status.helper_reselections > 0 && status.helper == helper->node_id && status.observer == observer->node_id);
assert(socket->address_profile.evidence_id != second_socket->address_profile.evidence_id);
check_snapshot(owner, socket);
check_snapshot(owner, second_socket);
check_snapshot(owner, tcp);
/* Профиль B не заимствует профиль A, даже когда транспорт переиспользуется в обе стороны. */
assert(!route_ping_addr_equal(&socket->local_addr, &observer_socket->local_addr, 1));
assert(!route_ping_addr_equal(&socket->local_addr, &test_etcp_bound_socket(helper, "udp")->local_addr, 1));
/* Повторная проверка не объявляет старый контакт независимым. */
uint64_t old_evidence = socket->address_profile.evidence_id;
nat_detection_request_check_all(owner->nat_det);
uint64_t end = get_time_tb() + 180000;
while (get_time_tb() < end && socket->address_profile.evidence_id == old_evidence) uasync_poll(fixture.ua, 10);
assert(socket->address_profile.evidence_id != old_evidence);
assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN);
/* Закрытие endpoint посреди запроса и переиспользование ID/порта: старые ответы не применяются. */
nat_detection_request_check_all(owner->nat_det);
end = get_time_tb() + 20000;
do { uasync_poll(fixture.ua, 0); nat_detection_get_status(owner->nat_det, socket, &status); }
while (!status.phase && get_time_tb() < end);
assert(status.phase);
uint8_t old_id = socket->sock_id; uint64_t old_generation = socket->generation;
struct CFG_SERVER recreated; memset(&recreated, 0, sizeof(recreated));
recreated.ip = socket->local_addr; recreated.type = CFG_SERVER_TYPE_PUBLIC; recreated.mtu = 1500; recreated.fib = -1;
snprintf(recreated.name, sizeof(recreated.name), "recreated");
etcp_socket_remove(socket);
owner->next_socket_id = old_id;
socket = etcp_socket_add(owner, &recreated);
assert(socket && socket->sock_id == old_id && socket->generation != old_generation);
end = get_time_tb() + 2000;
while (get_time_tb() < end) uasync_poll(fixture.ua, 10);
assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN);
nat_fixture_cleanup(&fixture);
return 0;
}

16
tests/test_node_conn_direct.c

@ -294,7 +294,7 @@ static void t8(void* arg) {
memcpy(ni->public_key, pubkey_b, SC_PUBKEY_SIZE);
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(g_a->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0}; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(g_a->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0]=127; addr->addr[1]=0; addr->addr[2]=0; addr->addr[3]=1; addr->port=1; addr->type=TOPO_ADDR_INTERFACE; addr->socket_id=0; addr->protocol=TOPO_PROTO_UDP; addr->next=ni->v4_addrs; ni->v4_addrs=addr; }
@ -343,7 +343,7 @@ static int inject_node(struct UTUN_INSTANCE* inst, uint64_t nid, const uint8_t p
ni->group_ref_count = 0; ni->node_id = nid; ni->ver = 0;
memcpy(ni->public_key, pk, SC_PUBKEY_SIZE);
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(inst->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0}; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(inst->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0]=127; addr->addr[1]=0; addr->addr[2]=0; addr->addr[3]=1; addr->port=port; addr->type=TOPO_ADDR_INTERFACE; addr->socket_id=0; addr->protocol=TOPO_PROTO_UDP; addr->next=ni->v4_addrs; ni->v4_addrs=addr; }
if (!topo_node_registry_store(inst->topo_groups, ni)) { u_free(ni); return -1; }
@ -377,13 +377,19 @@ static void test_compatibility(void) {
interface->sin_addr.s_addr = htonl(UINT32_C(0x08080808));
uint8_t saved_cfg = peer->v4_sock_meta->config_type, saved_self = self->v4_sock_meta->config_type;
peer->v4_sock_meta->config_type = CFG_SERVER_TYPE_LOCAL;
int reverse = node_conn_direct_compatible(g_a, nid_b);
int local = node_conn_direct_compatible(g_a, nid_b);
self->v4_sock_meta->config_type = CFG_SERVER_TYPE_LOCAL;
int local_both = node_conn_direct_compatible(g_a, nid_b);
uint8_t saved_protocol = peer->v4_addrs->protocol;
peer->v4_addrs->protocol = 0;
int incompatible = node_conn_direct_compatible(g_a, nid_b);
peer->v4_addrs->protocol = saved_protocol;
peer->v4_sock_meta->config_type = saved_cfg; self->v4_sock_meta->config_type = saved_self;
socket->interface_addr = saved_interface;
if (!reverse || incompatible || instance_find_conn(g_a, nid_b)) { fail("reverse/local compatibility or read-only contract"); return; }
fprintf(stderr, "[PASS] compatibility: direct, reverse-only, incompatible local and no transport mutation\n");
if (!local || !local_both || incompatible || instance_find_conn(g_a, nid_b)) {
fail("LAN candidate, unsupported protocol, or read-only compatibility contract"); return;
}
fprintf(stderr, "[PASS] compatibility: direct/LAN candidates, unsupported protocol and no transport mutation\n");
}
static void cleanup(void) { test_unlink(ca); test_unlink(cb); test_rmdir(tdir); }

415
tests/test_route_ping.c

@ -1,341 +1,98 @@
/**
* @file test_route_ping.c
* @brief Интеграционный тест route_ping через три инстанса
*
* Топология: A -> B -> C
* - A отправляет запрос route_ping на B для узла C
* - B пингует C через ETCP ping
* - C отвечает PONG
* - B отправляет BGP_PING_RESPONSE на A
* - A получает ответ через route_ping_send_req callback
*/
/* Настоящие UDP-наблюдения и отклонение PONG чужого ключа/поколения. */
#include "nat_fixture.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "test_utils.h"
#include "test_net.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "routing.h"
#include "topo_group.h"
#include "route_ping.h"
#include "../src/tun_if.h"
#include "secure_channel.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
struct result { int done, success; uint8_t data[128]; size_t length; };
#define TEST_TIMEOUT_MS 10000
static uint64_t g_node_id_a = 0;
static uint64_t g_node_id_b = 0;
static uint64_t g_node_id_c = 0;
static struct UTUN_INSTANCE* inst_a = NULL;
static struct UTUN_INSTANCE* inst_b = NULL;
static struct UTUN_INSTANCE* inst_c = NULL;
static struct UASYNC* ua = NULL;
static int test_timed_out = 0;
static void* test_timeout_id = NULL;
static char temp_dir[] = "/tmp/utun_route_ping_test_XXXXXX";
static char config_a[256];
static char config_b[256];
static char config_c[256];
static struct {
int done;
int success;
uint16_t avg_rtt;
uint8_t count_sent;
uint8_t count_ok;
} ping_result;
static int write_config(const char* path, const char* content) {
FILE* f = fopen(path, "w");
if (!f) return -1;
fprintf(f, "%s", content);
fclose(f);
return 0;
}
static char* get_pubkey_from_config(const char* path) {
struct utun_config* cfg = parse_config(path);
if (!cfg) return NULL;
char* pub = strdup(cfg->global.my_public_key_hex);
free_config(cfg);
return pub;
}
static int create_temp_configs(void) {
if (test_mkdtemp(temp_dir) != 0) {
fprintf(stderr, "Failed to create temp directory\n");
return -1;
}
snprintf(config_a, sizeof(config_a), "%s/a.conf", temp_dir);
snprintf(config_b, sizeof(config_b), "%s/b.conf", temp_dir);
snprintf(config_c, sizeof(config_c), "%s/c.conf", temp_dir);
// 1. Config C (no keys)
const char* tpl_c =
"[global]\n"
"tun_ip=10.100.0.3/24\n"
"tun_ifname=tun102\n"
"\n"
"[server:test_c]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n";
if (write_config(config_c, tpl_c) != 0) return -1;
if (config_ensure_keys_and_node_id(config_c) != 0) return -1;
char* pub_c = get_pubkey_from_config(config_c);
if (!pub_c) return -1;
// 2. Config B (no own keys, but needs peer_public_key of C)
char tpl_b[4096];
snprintf(tpl_b, sizeof(tpl_b),
"[global]\n"
"tun_ip=10.100.0.2/24\n"
"tun_ifname=tun101\n"
"\n"
"[server:test_b]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n"
"\n"
"[client:to_c]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_b:127.0.0.1:0\n",
pub_c);
free(pub_c);
if (write_config(config_b, tpl_b) != 0) return -1;
if (config_ensure_keys_and_node_id(config_b) != 0) return -1;
char* pub_b = get_pubkey_from_config(config_b);
if (!pub_b) return -1;
// 3. Config A (no own keys, but needs peer_public_key of B)
char tpl_a[4096];
snprintf(tpl_a, sizeof(tpl_a),
"[global]\n"
"tun_ip=10.100.0.1/24\n"
"tun_ifname=tun100\n"
"\n"
"[server:test_a]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n"
"\n"
"[client:to_b]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_a:127.0.0.1:0\n",
pub_b);
free(pub_b);
if (write_config(config_a, tpl_a) != 0) return -1;
if (config_ensure_keys_and_node_id(config_a) != 0) return -1;
return 0;
}
static void cleanup_temp_configs(void) {
test_unlink(config_a);
test_unlink(config_b);
test_unlink(config_c);
test_rmdir(temp_dir);
}
static int has_initialized_link(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->connections) return 0;
struct ll_entry* entry = inst->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
struct ETCP_CONN* conn = ce->conn;
struct ETCP_LINK* link = conn->links;
while (link) {
if (link->initialized) return 1;
link = link->next;
}
entry = entry->next;
}
return 0;
static void ping_done(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) {
(void)rtt; (void)nonce;
struct result *result = arg; result->done++; result->success = success; result->length = length;
assert(length <= sizeof(result->data)); if (length) memcpy(result->data, data, length);
}
static struct ETCP_CONN* find_conn_to_peer(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) {
if (!inst || !inst->connections) return NULL;
struct ll_entry* entry = queue_find_data_by_index(inst->connections, (const uint8_t*)&peer_node_id);
return entry ? ((struct conn_queue_entry*)entry->data)->conn : NULL;
static void wait_result(struct nat_fixture *fixture, struct result *result) {
uint64_t end = get_time_tb() + 30000;
while (!result->done && get_time_tb() < end) uasync_poll(fixture->ua, 10);
assert(result->done == 1);
}
static void test_timeout_cb(void* arg) {
(void)arg;
test_timed_out = 1;
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "test_route_ping: overall test timeout");
/* Аутентичный PONG от другого узла с известным nonce. Его source совпадает с
* target ожидающего запроса, поэтому ключ пира обязан проверяться отдельно. */
static void send_foreign_pong(struct UTUN_INSTANCE *sender, struct ETCP_SOCKET *destination, uint64_t nonce) {
uint8_t plain[23] = {0}, encrypted[256], salt[8], obfuscated[32];
plain[2] = 1; plain[3] = ETCP_PONG;
for (unsigned i = 0; i < 8; i++) {
plain[5 + i] = (uint8_t)(sender->node_id >> (56 - i * 8));
plain[13 + i] = (uint8_t)(nonce >> (56 - i * 8));
}
sc_context_t crypto; assert(sc_init_ctx(&crypto, &sender->my_keys) == SC_OK);
assert(sc_set_peer_public_key(&crypto, destination->instance->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK);
size_t length = 0; assert(sc_encrypt(&crypto, plain, sizeof(plain), encrypted, &length) == SC_OK);
assert(random_bytes(salt, sizeof(salt)) == 0);
sc_obfuscate_pubkey(salt, destination->instance->my_keys.public_key, sender->my_keys.public_key, obfuscated);
memcpy(encrypted + length, salt, sizeof(salt)); memcpy(encrypted + length + sizeof(salt), obfuscated, sizeof(obfuscated));
length += sizeof(salt) + sizeof(obfuscated);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(sender, "udp");
assert(socket_sendto(socket->fd, encrypted, length, (const struct sockaddr *)&destination->local_addr,
sizeof(struct sockaddr_in)) == (ssize_t)length);
}
static void ping_resp_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) {
(void)arg;
ping_result.done = 1;
ping_result.success = success;
ping_result.avg_rtt = avg_rtt;
ping_result.count_sent = count_sent;
ping_result.count_ok = count_ok;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "ping_resp_cb: success=%d avg_rtt=%u sent=%u ok=%u",
success, (unsigned)avg_rtt, (unsigned)count_sent, (unsigned)count_ok);
static void address_encoding(void) {
struct sockaddr_storage address = {0}, decoded;
struct sockaddr_in *v4 = (struct sockaddr_in *)&address;
v4->sin_family = AF_INET; assert(inet_pton(AF_INET, "198.51.100.7", &v4->sin_addr) == 1); v4->sin_port = htons(1300);
uint8_t wire[ROUTE_ADDR_WIRE_SIZE];
static const uint8_t expected[19] = {4,198,51,100,7,0,0,0,0,0,0,0,0,0,0,0,0,5,20};
assert(route_ping_addr_encode(wire, &address) == 0 && !memcmp(wire, expected, sizeof(wire)));
assert(route_ping_addr_decode(wire, &decoded) == 0 && route_ping_addr_equal(&address, &decoded, 1));
wire[5] = 1; assert(route_ping_addr_decode(wire, &decoded) < 0); /* скрытые байты IPv4 запрещены */
memset(&address, 0, sizeof(address));
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)&address;
v6->sin6_family = AF_INET6; v6->sin6_port = htons(0xabcd);
assert(inet_pton(AF_INET6, "2001:db8::1234", &v6->sin6_addr) == 1);
assert(route_ping_addr_encode(wire, &address) == 0 && wire[0] == 6 && wire[17] == 0xab && wire[18] == 0xcd);
assert(route_ping_addr_decode(wire, &decoded) == 0 && route_ping_addr_equal(&address, &decoded, 1));
wire[17] = wire[18] = 0; assert(route_ping_addr_decode(wire, &decoded) < 0);
}
int main(void) {
int test_result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING);
utun_instance_set_tun_init_enabled(0);
if (create_temp_configs() != 0) {
fprintf(stderr, "Failed to create temp configs\n");
return 1;
}
ua = uasync_create();
if (!ua) {
cleanup_temp_configs();
return 1;
}
inst_a = utun_instance_create(ua, config_a);
inst_b = utun_instance_create(ua, config_b);
inst_c = utun_instance_create(ua, config_c);
if (!inst_a || !inst_b || !inst_c) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create instances");
goto cleanup;
}
test_etcp_set_peer(inst_a, "to_b", inst_b, "test_b");
test_etcp_set_peer(inst_b, "to_c", inst_c, "test_c");
if (utun_instance_init(inst_a) != 0 || utun_instance_init(inst_b) != 0 || utun_instance_init(inst_c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to init connections");
goto cleanup;
}
g_node_id_a = inst_a->node_id; g_node_id_b = inst_b->node_id; g_node_id_c = inst_c->node_id;
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout_cb, "test_route_ping");
// 1. Wait for links A->B and B->C to initialize
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Waiting for ETCP links to initialize...");
while (!test_timed_out) {
if (has_initialized_link(inst_a) && has_initialized_link(inst_b)) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Links initialized");
break;
}
uasync_poll(ua, 10);
}
if (test_timed_out) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Timeout waiting for links");
goto cleanup;
}
// 2. Wait for BGP exchange so B learns C (registry has C's nodeinfo + addresses)
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for BGP exchange (B learns C)...");
int bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
struct TOPO_NODE* ni_c = topo_node_registry_find(inst_b->topo_groups, g_node_id_c);
if (ni_c && ni_c->v4_addrs) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "B learned about C");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!topo_node_registry_find(inst_b->topo_groups, g_node_id_c)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "B did not learn about C in time");
goto cleanup;
}
// 3. Get target info for C and send route ping request from A to B
struct ETCP_CONN* conn_ab = find_conn_to_peer(inst_a, g_node_id_b);
if (!conn_ab) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "A has no connection to B");
goto cleanup;
}
/* Получаем target IP:port из nodeinfo узла C */
/* addr[] в network byte order, port в host byte order */
uint32_t target_ip = 0;
uint16_t target_port = 0;
struct TOPO_GROUP_NODE* nq = topo_groups_get_default(inst_b->topo_groups) ? topo_node_find_by_id(topo_groups_get_default(inst_b->topo_groups), g_node_id_c) : NULL;
struct TOPO_NODE* ni = nq ? topo_node_registry_find(inst_b->topo_groups, nq->node_id) : NULL;
if (nq && ni) {
const struct TOPO_ADDR4* a = ni->v4_addrs;
if (a) {
memcpy(&target_ip, a->addr, 4);
target_port = a->port;
}
}
if (target_ip == 0 || target_port == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Cannot resolve target for node C");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Sending route ping request A->B for C (%s:%u)",
ip_to_str(&target_ip, AF_INET).str, (unsigned)target_port);
memset(&ping_result, 0, sizeof(ping_result));
int ret = route_ping_send_req_addr(inst_a->nat_det, topo_groups_get_default(inst_a->topo_groups), conn_ab, target_ip, target_port,
3, // count
10, // interval_ms
200, // timeout_ms
5000,// wait_timeout_ms
ping_resp_cb, NULL,
ni ? ni->public_key : NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr failed: %d", ret);
goto cleanup;
}
// 4. Wait for response
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for route ping response...");
while (!test_timed_out && !ping_result.done) {
uasync_poll(ua, 10);
}
if (test_timed_out) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Timeout waiting for ping response");
goto cleanup;
}
// 5. Verify results
if (!ping_result.success) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping reported failure (success=0)");
goto cleanup;
}
if (ping_result.count_ok == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping count_ok is zero");
goto cleanup;
}
if (ping_result.avg_rtt == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping avg_rtt is zero");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Route ping test PASSED: sent=%u ok=%u avg_rtt=%u",
(unsigned)ping_result.count_sent, (unsigned)ping_result.count_ok, (unsigned)ping_result.avg_rtt);
test_result = 0;
cleanup:
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id);
if (inst_a) utun_instance_destroy(inst_a);
if (inst_b) utun_instance_destroy(inst_b);
if (inst_c) utun_instance_destroy(inst_c);
if (ua) uasync_destroy(ua, 0);
cleanup_temp_configs();
return test_result;
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG);
address_encoding();
struct nat_fixture fixture; nat_fixture_init(&fixture);
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL);
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL);
struct UTUN_INSTANCE *foreign = nat_fixture_node(&fixture, "127.0.0.4", NULL, NULL);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp");
struct ETCP_SOCKET *peer = test_etcp_bound_socket(observer, "udp");
const uint64_t round = UINT64_C(0x0102030405060708);
struct result result = {0};
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round, 500, ping_done, &result) == 0);
wait_result(&fixture, &result);
struct sockaddr_storage observed;
assert(result.success && route_ping_observation(result.data, result.length, socket, round, &observed) == 0);
assert(route_ping_addr_equal(&observed, &socket->local_addr, 1));
assert(route_ping_observation(result.data, result.length, socket, round + 1, &observed) < 0);
assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN); /* один ответ не назначает NAT-тип */
memset(&result, 0, sizeof(result));
struct ETCP_SOCKET *untrusted = test_etcp_bound_socket(foreign, "udp");
assert(route_ping_observe(socket, observer->my_keys.public_key, &untrusted->local_addr,
round + 1, 300, ping_done, &result) == 0);
assert(owner->pending_pings);
send_foreign_pong(foreign, socket, owner->pending_pings->nonce);
wait_result(&fixture, &result);
assert(!result.success && !owner->pending_pings);
memset(&result, 0, sizeof(result));
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round + 2, 300, ping_done, &result) == 0);
uint64_t generation = socket->generation; socket->generation = generation == UINT64_MAX ? generation - 1 : generation + 1;
wait_result(&fixture, &result); assert(!result.success); socket->generation = generation;
memset(&result, 0, sizeof(result));
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round + 3, 500, ping_done, &result) == 0);
etcp_cancel_pings(owner, ping_done, &result); assert(!owner->pending_pings);
uint64_t end = get_time_tb() + 6000;
while (get_time_tb() < end) uasync_poll(fixture.ua, 10);
assert(!result.done);
nat_fixture_cleanup(&fixture);
return 0;
}

122
tests/test_sock_match.c

@ -1,88 +1,46 @@
/**
* @file test_sock_match.c
* @brief Unit-тесты классификации сокетов и сопоставления адресов (sock_match).
*
* Тестирует только чистую логику sock_role_of / sock_target_class / sock_match
* без сети и без UTUN_INSTANCE — с синтетическими struct sock_view и адресами.
*/
#include <stdio.h>
/* Политика не заменяет измерение; неопределённость разрешает ограниченную пробу. */
#include <assert.h>
#include <string.h>
#include "../src/routing_layer/sock_match.h"
#include "../src/config_parser.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../src/routing_layer/topo_node.h"
#include "routing_layer/sock_match.h"
#include "routing_layer/topo_node.h"
#include "config_parser.h"
#include "../lib/platform_compat.h"
static int fails = 0;
static int checks = 0;
#define CHECK(cond, msg) do { checks++; if (!(cond)) { fails++; printf("FAIL: %s\n", msg); } } while (0)
static struct sock_view mk_view(uint8_t role, uint8_t has_inet, uint8_t has_priv, uint8_t is_eim, uint32_t pub4) {
struct sock_view v; memset(&v, 0, sizeof(v));
v.role = role; v.has_inet = has_inet; v.has_priv = has_priv; v.is_eim = is_eim; v.public_ip4 = pub4;
return v;
}
/* Классификация IPv4-адреса (network byte order): публичный/маршрутизируемый или нет.
* Адрес задаём «логически» (старший байт = первый октет) и переводим в s_addr через htonl. */
static void test_ip_is_public(void) {
CHECK(ip_is_public(htonl(0x08080808u)) == 1, "8.8.8.8 public");
CHECK(ip_is_public(htonl(0x01020304u)) == 1, "1.2.3.4 public");
CHECK(ip_is_public(htonl(0xC0A80101u)) == 0, "192.168.1.1 private");
CHECK(ip_is_public(htonl(0x0A000001u)) == 0, "10.0.0.1 private");
CHECK(ip_is_public(htonl(0xAC100001u)) == 0, "172.16.0.1 private");
CHECK(ip_is_public(htonl(0xAC1F0001u)) == 0, "172.31.0.1 private");
CHECK(ip_is_public(htonl(0x64400001u)) == 0, "100.64.0.1 CGNAT");
CHECK(ip_is_public(htonl(0x7F000001u)) == 0, "127.0.0.1 loopback");
CHECK(ip_is_public(htonl(0xA9FE0101u)) == 0, "169.254.1.1 link-local");
CHECK(ip_is_public(htonl(0x00000000u)) == 0, "0.0.0.0");
CHECK(ip_is_public(htonl(0xE0000001u)) == 0, "224.0.0.1 multicast");
CHECK(ip_is_public(htonl(0xFFFFFFFFu)) == 0, "255.255.255.255 reserved");
}
static void test_target_class(void) {
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN) == SOCK_TC_PUB, "public INTERFACE -> PUB");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_UNKNOWN, NAT_TYPE_UNKNOWN) == SOCK_TC_PUB, "unknown -> PUB");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "local -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PRIVATE, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "private -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM) == SOCK_TC_PUB, "nat+EIM NAT-addr -> PUB");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM) == SOCK_TC_PRIV, "nat+EIM INTERFACE -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_DIRECT) == SOCK_TC_PUB, "nat+DIRECT NAT-addr -> PUB");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT) == SOCK_TC_UNREACH, "nat+STRICT NAT-addr -> UNREACH");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT) == SOCK_TC_PRIV, "nat+STRICT INTERFACE -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN) == SOCK_TC_UNREACH, "nat+unknown NAT-addr -> UNREACH");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "nat+unknown INTERFACE -> PRIV");
}
static void test_match(void) {
uint32_t pub = 0x01020304u; /* произвольный "публичный" IP */
struct sock_view direct = mk_view(SOCK_ROLE_DIRECT, 1, 0, 0, pub);
struct sock_view natout = mk_view(SOCK_ROLE_NAT, 1, 1, 0, 0);
struct sock_view local = mk_view(SOCK_ROLE_LOCAL, 0, 1, 0, 0);
/* наш DIRECT (публичный) */
CHECK(sock_match(&direct, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 1, "DIRECT->PUB ok");
CHECK(sock_match(&direct, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == 0, "DIRECT->PRIV нет приватного интерфейса");
/* наш NAT (outbound-only) */
CHECK(sock_match(&natout, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 1, "NAT->PUB ok");
CHECK(sock_match(&natout, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT, 0) == 1, "NAT->PRIV (есть приватный)");
/* наш LOCAL (без интернета) */
CHECK(sock_match(&local, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 0, "LOCAL->PUB нет интернета");
CHECK(sock_match(&local, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == 1, "LOCAL->PRIV ок (LAN)");
/* UNREACH цель */
CHECK(sock_match(&direct, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT, pub) == 0, "строгий NAT-адрес недостижим");
/* hairpin: оба EIM, одинаковый публичный IP */
struct sock_view eim = mk_view(SOCK_ROLE_DIRECT, 1, 1, 1, pub);
CHECK(sock_match(&eim, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, pub) == 0, "hairpin (одинаковый публичный IP)");
CHECK(sock_match(&eim, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, 0x05060708u) == 1, "EIM разные публичные IP -> ok");
}
#include "../lib/debug_config.h"
int main(void) {
test_ip_is_public();
test_target_class();
test_match();
if (fails) { printf("RESULT: %d/%d failed\n", fails, checks); return 1; }
printf("RESULT: all %d checks passed\n", checks);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
const uint64_t now = UINT64_C(1700000000000000);
const uint32_t public_ip = htonl(UINT32_C(0xc633640b));
const uint32_t private_ip = htonl(UINT32_C(0x0a400102));
struct address_profile unknown = {0};
struct address_profile open = {.mapping = ADDRESS_MAP_EIM, .inbound = ADDRESS_IN_OPEN, .evidence_id = 1, .checked_at = now};
struct address_profile contact = open; contact.inbound = ADDRESS_IN_CONTACT;
struct address_profile dependent = open; dependent.mapping = ADDRESS_MAP_DEPENDENT; dependent.inbound = ADDRESS_IN_UNKNOWN;
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, &unknown, 1, now) == SOCK_TC_PROBE);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, &open, 0, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now) == SOCK_TC_PUB);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &contact, 1, now) == SOCK_TC_CONTACT);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &unknown, 1, now) == SOCK_TC_PROBE);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &dependent, 1, now) == SOCK_TC_UNREACH);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, &dependent, 0, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, &open, 1, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PRIVATE, &open, 1, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now + ADDRESS_PROFILE_TTL_US + 1) == SOCK_TC_PROBE);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now - 1) == SOCK_TC_PROBE);
struct sock_view outgoing = {.has_inet = 1, .has_priv = 1, .now_us = now, .public_ip4 = public_ip};
struct sock_view local = {.has_priv = 1, .now_us = now};
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &contact, public_ip));
/* Совпадение публичного IP разрешает проверку hairpin. */
outgoing.address_profile = open;
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
assert(!sock_match(&local, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
assert(sock_match(&local, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, &dependent, private_ip));
assert(!sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &dependent, public_ip));
outgoing.now_us = now + ADDRESS_PROFILE_TTL_US + 1;
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
/* Смена времени/давности снижает уверенность, сохраняя возможность реального handshake. */
return 0;
}

2
tools/chatgui/src/accountlist.cpp

@ -35,7 +35,7 @@ static void memberDetailCb(const uint8_t* data, int len) {
}
static QString addrRoleStr(int t) {
switch (t) { case 0: return "local"; case 1: return "public"; case 2: return "nat_eim"; case 3: return "nat_strict"; case 4: return "nat"; default: return QString("?%1").arg(t); }
switch (t) { case 0: return "local"; case 1: return "public"; case 2: return "nat_eim"; case 3: return "nat_dependent"; case 4: return "unknown"; case 5: return "contact_required"; default: return QString("?%1").arg(t); }
}
QString AccountList::addrStr(const void* addr, int family) {

4
tools/chatgui/src/memberlistmodel.cpp

@ -61,8 +61,8 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
QString sub;
if (it->rtt >= 0) sub += QString("rtt=%1ms").arg(it->rtt / 10);
if (it->natType >= 0) {
static const char* nat[] = {"NET","DIR","EIM","STR","NAT"};
const char* nt = (it->natType <= 4) ? nat[it->natType] : "?";
static const char* nat[] = {"NET","DIR","EIM","DEP","?","CONTACT"};
const char* nt = (it->natType <= 5) ? nat[it->natType] : "?";
if (!sub.isEmpty()) sub += " ";
sub += nt;
}

2
tools/chatgui/src/nodespage.cpp

@ -49,7 +49,7 @@ QString NodesPage::protoStr(int protocol) {
}
static QString addrTypeStr(int t) {
switch (t) { case 0: return "NETIF"; case 1: return "DIRECT"; case 2: return "NAT_EIM"; case 3: return "NAT_STRICT"; case 4: return "NAT"; default: return QString("?%1").arg(t); }
switch (t) { case 0: return "NETIF"; case 1: return "DIRECT"; case 2: return "NAT_EIM"; case 3: return "NAT_DEPENDENT"; case 4: return "UNKNOWN"; case 5: return "CONTACT_REQUIRED"; default: return QString("?%1").arg(t); }
}
NodesPage::NodesPage(DbManager* db, QWidget* parent)

36
tools/etcpmon/etcpmon_gui.c

@ -1589,18 +1589,6 @@ static const char* config_type_to_string(uint8_t config_type) {
}
}
static const char* nat_type_to_string(uint8_t nat_type) {
switch (nat_type) {
case NAT_TYPE_EIM: return "EIM";
case NAT_TYPE_STRICT: return "Strict";
case NAT_TYPE_DIRECT: return "Direct";
case NAT_VERIFIED_EIM: return "Verified-EIM";
case NAT_VERIFIED_STRICT: return "Verified-Strict";
case NAT_VERIFIED_DIRECT: return "Verified-Direct";
case NAT_TYPE_UNKNOWN: return "Unknown";
default: return "?";
}
}
void etcpmon_gui_update_socket_list(struct etcpmon_app* app,
struct etcpmon_socket_info* list,
@ -1614,25 +1602,11 @@ void etcpmon_gui_update_socket_list(struct etcpmon_app* app,
if (list[i].name[0] == '\0') continue;
char display[256];
const char* cfg_str = config_type_to_string(list[i].config_type);
const char* nat_str = nat_type_to_string(list[i].nat_type);
if (list[i].nat_ip[0] != '\0') {
if (list[i].is_ipv6) {
snprintf(display, sizeof(display), "[%s]:%d | Type: %s | NAT: %s (%s:%d)",
list[i].ip, list[i].port, cfg_str, nat_str, list[i].nat_ip, list[i].nat_port);
} else {
snprintf(display, sizeof(display), "%s:%d | Type: %s | NAT: %s (%s:%d)",
list[i].ip, list[i].port, cfg_str, nat_str, list[i].nat_ip, list[i].nat_port);
}
} else {
if (list[i].is_ipv6) {
snprintf(display, sizeof(display), "[%s]:%d | Type: %s | NAT: %s",
list[i].ip, list[i].port, cfg_str, nat_str);
} else {
snprintf(display, sizeof(display), "%s:%d | Type: %s | NAT: %s",
list[i].ip, list[i].port, cfg_str, nat_str);
}
}
snprintf(display, sizeof(display), "%s%s%s:%u | %s policy=%s | mapping=%s inbound=%s | external=%s:%u",
list[i].is_ipv6 ? "[" : "", list[i].ip, list[i].is_ipv6 ? "]" : "", list[i].port,
list[i].is_tcp ? "TCP" : "UDP", config_type_to_string(list[i].config_type),
address_mapping_name(list[i].mapping), address_inbound_name(list[i].inbound),
list[i].nat_ip[0] ? list[i].nat_ip : "unknown", list[i].nat_port);
SendMessageA(app->hListSockets, LB_ADDSTRING, 0, (LPARAM)display);
}
}

17
tools/etcpmon/etcpmon_protocol.h

@ -11,6 +11,7 @@
#define ETCPMON_PROTOCOL_H
#include <stdint.h>
#include "../../src/routing_layer/address_profile.h"
#ifdef __cplusplus
@ -21,17 +22,7 @@ extern "C" {
* Protocol Constants
* ============================================================================ */
#define ETCPMON_PROTOCOL_VERSION 1
/* NAT types (mirrored from etcp_connections.h for monitor protocol) */
#define NAT_TYPE_UNKNOWN 0
#define NAT_TYPE_EIM 1 /* Endpoint-Independent Mapping */
#define NAT_TYPE_STRICT 2 /* Address/Restricted or Symmetric */
#define NAT_TYPE_DIRECT 3 /* Real public IP */
#define NAT_VERIFIED_UNKNOWN 4
#define NAT_VERIFIED_EIM 5 /* EIM NAT */
#define NAT_VERIFIED_STRICT 6 /* strict NAT */
#define NAT_VERIFIED_DIRECT 7
#define ETCPMON_PROTOCOL_VERSION 2
/* Server config types (mirrored from config_parser.h) */
#define CFG_SERVER_TYPE_UNKNOWN 0
@ -135,7 +126,9 @@ struct etcpmon_socket_info {
uint16_t port; /* Port number (host byte order) */
uint8_t is_ipv6; /* 1 = IPv6, 0 = IPv4 */
uint8_t config_type; /* CFG_SERVER_TYPE_PUBLIC/NAT/PRIVATE */
uint8_t nat_type; /* NAT_TYPE_* / NAT_VERIFIED_* */
uint8_t mapping, inbound; /* ADDRESS_MAP_* / ADDRESS_IN_* */
uint8_t is_tcp;
uint64_t generation, evidence_id, checked_at;
char nat_ip[46]; /* Null-terminated NAT IP address string */
uint16_t nat_port; /* NAT port (host byte order) */
};

Loading…
Cancel
Save