From ff69552a61e9bc3a288ed2dc183cfbc7342b6422 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 11 Oct 2026 23:35:40 +0200 Subject: [PATCH] Measure owned endpoint mapping and inbound reachability separately --- Makefile.am | 2 + doc/etcp_protocol.txt | 8 +- doc/nat_detection.txt | 145 +++ doc/node_snapshot.md | 12 + src/Makefile.am | 2 +- src/chat/chat_member.c | 14 +- src/chat/chat_status.c | 38 +- src/control_server.c | 15 +- src/routing_layer/address_profile.h | 51 + src/routing_layer/conn_mgr_core.c | 34 +- src/routing_layer/nat_detection.c | 1300 ++++++++++++++++++------ src/routing_layer/nat_detection.h | 184 +--- src/routing_layer/route_connectivity.c | 18 +- src/routing_layer/route_ping.c | 461 +-------- src/routing_layer/route_ping.h | 63 +- src/routing_layer/route_ping_doc.md | 90 +- src/routing_layer/sock_match.c | 97 +- src/routing_layer/sock_match.h | 108 +- src/routing_layer/topo_node.c | 150 ++- src/routing_layer/topo_node.h | 19 +- src/routing_layer/topo_node_sqlite.c | 227 +++-- src/routing_layer/topo_node_sqlite.h | 2 +- src/transport_layer/etcp.c | 10 +- src/transport_layer/etcp_connections.c | 225 ++-- src/transport_layer/etcp_connections.h | 39 +- src/transport_layer/etcp_dump.c | 9 +- src/transport_layer/node_conn_direct.c | 41 +- src/utun_instance.c | 4 +- tests/Makefile.am | 4 +- tests/nat_fixture.h | 82 ++ tests/stream_netns/README.md | 27 +- tests/stream_netns/TESTED.md | 19 + tests/stream_netns/nat_scenario.py | 107 ++ tests/stream_netns/network.py | 17 + tests/stream_netns/node.c | 155 ++- tests/stream_netns/probe.py | 8 +- tests/stream_netns/run.py | 6 + tests/stream_netns/scenario.py | 27 +- tests/test_auto_socket_dynamic.c | 2 +- tests/test_etcp_connect.c | 2 +- tests/test_etcp_two_instances.c | 24 +- tests/test_ipv6_sockets.c | 3 +- tests/test_nat_detection.c | 640 +++--------- tests/test_node_conn_direct.c | 16 +- tests/test_route_ping.c | 415 ++------ tests/test_sock_match.c | 122 +-- tools/chatgui/src/accountlist.cpp | 2 +- tools/chatgui/src/memberlistmodel.cpp | 4 +- tools/chatgui/src/nodespage.cpp | 2 +- tools/etcpmon/etcpmon_gui.c | 36 +- tools/etcpmon/etcpmon_protocol.h | 17 +- 51 files changed, 2621 insertions(+), 2484 deletions(-) create mode 100644 doc/nat_detection.txt create mode 100644 src/routing_layer/address_profile.h create mode 100644 tests/nat_fixture.h create mode 100644 tests/stream_netns/nat_scenario.py diff --git a/Makefile.am b/Makefile.am index 911c71d1..202f20c6 100644 --- a/Makefile.am +++ b/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 \ diff --git a/doc/etcp_protocol.txt b/doc/etcp_protocol.txt index cf3e8f8b..de4620d4 100644 --- a/doc/etcp_protocol.txt +++ b/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 если он есть diff --git a/doc/nat_detection.txt b/doc/nat_detection.txt new file mode 100644 index 00000000..f16995d1 --- /dev/null +++ b/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 не меняется. diff --git a/doc/node_snapshot.md b/doc/node_snapshot.md index cdcca057..250bcf6f 100644 --- a/doc/node_snapshot.md +++ b/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 принимаются подписанные записи diff --git a/src/Makefile.am b/src/Makefile.am index 5a6cd104..ee88be26 100644 --- a/src/Makefile.am +++ b/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 \ diff --git a/src/chat/chat_member.c b/src/chat/chat_member.c index 6b826506..1d86ce78 100644 --- a/src/chat/chat_member.c +++ b/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); } diff --git a/src/chat/chat_status.c b/src/chat/chat_status.c index 0bf3c523..da1c059a 100644 --- a/src/chat/chat_status.c +++ b/src/chat/chat_status.c @@ -20,8 +20,38 @@ #include "../routing_layer/topo_group.h" #include -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, diff --git a/src/control_server.c b/src/control_server.c index 9a53aacf..47483ff1 100644 --- a/src/control_server.c +++ b/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; diff --git a/src/routing_layer/address_profile.h b/src/routing_layer/address_profile.h new file mode 100644 index 00000000..3f9f1a94 --- /dev/null +++ b/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 + +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 diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index 737f775f..4d994cac 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/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; } } diff --git a/src/routing_layer/nat_detection.c b/src/routing_layer/nat_detection.c index 886f6c40..1996f5f7 100644 --- a/src/routing_layer/nat_detection.c +++ b/src/routing_layer/nat_detection.c @@ -1,392 +1,1034 @@ -/** - * @file nat_detection.c - * @brief NAT-детекция: STUN-like проверка через третьего пира. - * - * Самодостаточный модуль: - * - Сам биндится на ETCP_ID_NAT_DETECTION (0x02) и принимает свои пакеты - * - Сам подписывается на conn_up/conn_down новых ETCP-соединений - * - Ищет третий узел прямо в instance->connections (без привязки к BGP-группе) - */ -#include -#include +/* NAT-детектор владельца endpoint. Один раунд на рабочий сокет: + * S->B observation, B->C ingress(S), S->C/C-alt observations, S->B repeat. + * Команды к C проходят через B: контроль не открывает фильтр S->C. */ +#include "nat_detection.h" + #include -#ifdef _WIN32 -#include -#include -#else -#include -#endif -#include "../lib/platform_compat.h" -#include "../lib/debug_config.h" -#include "../lib/mem.h" +#include #include "utun_instance.h" +#include "config_parser.h" #include "etcp_api.h" #include "etcp.h" #include "etcp_connections.h" -#include "config_parser.h" +#include "route_ping.h" #include "topo_node.h" #include "topo_group.h" -#include "route_ping.h" -#include "nat_detection.h" +#include "ntp_time.h" +#include "stcp_client.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" #include "../lib/u_async.h" +#define NAT_WIRE_VERSION 2 +#define NAT_MAX_HELPERS 32 +#define NAT_MAX_BINDINGS 128 +#define NAT_MAX_PROBES 64 +#define NAT_MAX_CONTACTS 256 +#define NAT_TICK_TB 1000 +#define NAT_ROUND_TB 300000 +#define NAT_REFRESH_TB 1800000 +#define NAT_RETRY_TB 300000 +#define NAT_PROBE_COUNT 3 +#define NAT_QUERY_TAG_SIZE (ROUTE_OBSERVE_TAG_SIZE + 9) -/* ====== Internal NAT check logic ====== */ +enum nat_command { NAT_FIND = 1, NAT_OFFER, NAT_RUN, NAT_RESULT }; +enum nat_phase { NAT_IDLE, NAT_OBSERVE_B, NAT_FIND_C, NAT_PRE, NAT_OBSERVE_C, NAT_OBSERVE_ALT, NAT_POST, NAT_REPEAT_B, NAT_TCP }; +enum nat_step { NAT_STEP_PRE = 1, NAT_STEP_POST, NAT_STEP_TCP }; -static struct ETCP_CONN* nat_check_find_third_node(struct NAT_DETECTION* nd, struct ETCP_CONN* exclude) { - if (!nd || !nd->inst || !nd->inst->connections) return NULL; - struct ll_entry* e = nd->inst->connections->head; - while (e) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; - if (ce->conn && ce->conn != exclude && ce->conn->links) - return ce->conn; - e = e->next; - } +/* Только байты: формат независим от alignment/endian платформы. */ +struct nat_wire { + uint8_t cmd, command, version, owner_socket, helper_socket, step, reason, excluded_count; + uint8_t owner[8], generation[8], round[8], token[8], helper[8], timeout_ms[2]; + uint8_t target[ROUTE_ADDR_WIRE_SIZE], primary[ROUTE_ADDR_WIRE_SIZE], secondary[ROUTE_ADDR_WIRE_SIZE]; + uint8_t owner_key[SC_PUBKEY_SIZE], helper_key[SC_PUBKEY_SIZE]; + uint8_t excluded[NAT_MAX_HELPERS][8]; + uint8_t sent, received, errors; +}; + +struct nat_socket { + struct nat_socket *next; + struct NAT_DETECTION *detector; + struct ETCP_SOCKET *socket, *auxiliary; + struct sockaddr_storage local, observer_address, helper_address, alternate_address; + struct sockaddr_storage baseline, helper_seen, alternate_seen; + uint8_t observer_key[32], helper_key[32]; + uint64_t round, observer, helper, token, deadline, due, completed_tb, preferred_observer; + uint64_t excluded[NAT_MAX_HELPERS]; + uint8_t contacts[NAT_MAX_CONTACTS][17]; + unsigned contact_count, excluded_count, phase, reason, retries; + uint8_t history_loaded, history_complete, history_incomplete, filter_clean, pre_received, post_received, pre_healthy; + uint8_t helper_socket, active_ping, forced; + uint32_t rounds, helper_reselections; + /* Уже контактировавший помощник годится для mapping после исчерпания свежих. */ + uint64_t fallback_helper; + uint8_t fallback_key[32], fallback_socket; + struct sockaddr_storage fallback_address; +}; + +struct nat_binding { + struct nat_binding *next; + uint64_t owner, generation, round, expires; + uint8_t owner_socket, owner_key[32]; + struct sockaddr_storage target; +}; + +struct nat_probe { + struct nat_probe *next; + struct NAT_DETECTION *detector; + struct nat_wire message; + uint64_t reply, source_generation, due, expires; + uint8_t source_socket, active, cancelled; + struct stcp_client *tcp; +}; + +struct NAT_DETECTION { + struct UTUN_INSTANCE *instance; + struct nat_socket *sockets; + struct nat_binding *bindings; + struct nat_probe *probes; + void *timer; + uint64_t publish_after; + uint8_t allow_local, stopping, history_table_ready, automatic, publish_pending; +}; + +static void owner_ping_cb(int, uint16_t, void *, uint64_t, const uint8_t *, size_t); +static void helper_ping_cb(int, uint16_t, void *, uint64_t, const uint8_t *, size_t); +static void detector_tick(void *); +static int owner_observe(struct nat_socket *); +static int owner_run(struct nat_socket *, uint8_t); +static void owner_finish(struct nat_socket *, uint8_t); +static void helper_remove(struct nat_probe *); + +static uint64_t read64(const uint8_t *data) { + uint64_t value = 0; + for (unsigned i = 0; i < 8; i++) value = (value << 8) | data[i]; + return value; +} +static void write64(uint8_t *data, uint64_t value) { + for (unsigned i = 0; i < 8; i++) data[i] = (uint8_t)(value >> (56 - i * 8)); +} +static int random_id(uint64_t *value) { + do { + if (random_bytes((uint8_t *)value, sizeof(*value))) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT round/token RNG failed"); return -1; + } + } while (!*value); + return 0; +} + +const char *nat_check_reason_name(uint8_t reason) { + static const char *const names[] = {"none", "no_observer", "no_helper", "control_lost", "send_error", "timeout", + "address_changed", "contact_history", "proxy", "no_listener", "cancelled", "invalid_observation", "dependent_mapping"}; + return reason < sizeof(names) / sizeof(names[0]) ? names[reason] : "invalid_reason"; +} + +static struct ETCP_CONN *live_connection(struct NAT_DETECTION *detector, uint64_t node) { + struct ETCP_CONN *connection = instance_find_conn(detector->instance, node); + return connection && connection->state != 2 && !connection->close_requested && connection->links_up && + connection->crypto_ctx.peer_key_set ? connection : NULL; +} + +static struct ETCP_SOCKET *find_socket(struct NAT_DETECTION *detector, uint8_t id, uint64_t generation) { + for (struct ETCP_SOCKET *socket = detector->instance->etcp_sockets; socket; socket = socket->next) + if (socket->sock_id == id && (!generation || socket->generation == generation)) return socket; return NULL; } -static void nat_link_check_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) { - (void)avg_rtt; (void)count_sent; (void)count_ok; - struct nat_check_arg* na = (struct nat_check_arg*)arg; - if (!na || !na->link) { u_free(na); return; } - struct ETCP_LINK* link = na->link; - if (link->nat_check_status != NAT_CHECK_IN_PROGRESS) { - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "nat check cb: link status=%d, skip (conn down?)", link->nat_check_status); - u_free(na); return; - } - link->nat_type = success ? NAT_TYPE_EIM : NAT_TYPE_STRICT; - link->nat_check_status = success ? NAT_CHECK_EIM : NAT_CHECK_STRICT; - if (link->etcp) - nat_detection_send_nat_info(link->etcp->instance->nat_det, - link->etcp, link->remote_socket_id, - na->nat_ip, na->nat_port, link->nat_type); - DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT check result: type=%s for conn=%s nat=%s:%u", - success ? "EIM" : "STRICT", link->etcp->log_name, - ip_to_str(&na->nat_ip, AF_INET).str, na->nat_port); - u_free(na); -} - -static void nat_detection_start_link_check(struct NAT_DETECTION* nd, struct ETCP_LINK* link) { - if (!nd || !link || !link->conn || !link->etcp) return; - if (link->nat_type == NAT_TYPE_DIRECT) return; - if (link->nat_check_status == NAT_CHECK_IN_PROGRESS) return; - if (link->conn->type == CFG_SERVER_TYPE_PRIVATE || link->conn->type == CFG_SERVER_TYPE_LOCAL) { - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "skip nat check for private/local socket link"); return; - } - struct ETCP_CONN* third_conn = nat_check_find_third_node(nd, link->etcp); - if (!third_conn) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "no third node found for link"); return; } - - uint32_t target_ip; uint16_t target_port; - if (link->nat_ip != 0 && link->nat_port != 0) { - target_ip = link->nat_ip; target_port = link->nat_port; - } else { - struct sockaddr_in* sin = (struct sockaddr_in*)&link->remote_addr; - target_ip = sin->sin_addr.s_addr; target_port = ntohs(sin->sin_port); - } - if (!nd->allow_nat_check_local && is_local_subnet(target_ip)) { - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "local connection: %s, skip nat check", - ip_to_str(&target_ip, AF_INET).str); return; - } - - struct TOPO_GROUP* group = nd->inst->topo_groups - ? topo_groups_get_default(nd->inst->topo_groups) : NULL; - struct nat_check_arg* arg = u_calloc(1, sizeof(struct nat_check_arg)); - if (!arg) return; - arg->link = link; arg->nat_ip = target_ip; arg->nat_port = target_port; - const uint8_t* pubkey = link->etcp->crypto_ctx.peer_key_set ? link->etcp->crypto_ctx.peer_public_key : NULL; - if (!pubkey) { u_free(arg); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "no peer pubkey for link=%p", (void*)link); return; } - int ret = route_ping_send_req_addr(nd, group, third_conn, target_ip, target_port, - 3, 500, 1000, 5000, nat_link_check_cb, arg, pubkey); - if (ret == 0) { - link->nat_check_status = NAT_CHECK_IN_PROGRESS; - DEBUG_INFO(DEBUG_CATEGORY_BGP, "ping remote request via=%s (%s:%d)", - third_conn->log_name, ip_to_str(&target_ip, AF_INET).str, target_port); - } else { - u_free(arg); - DEBUG_WARN(DEBUG_CATEGORY_BGP, "failed to start for link=%s (%s:%d)", - third_conn->log_name, ip_to_str(&target_ip, AF_INET).str, target_port); - } -} - - -/* ====== Handle incoming NAT messages (static, dispatched from receive_cbk) ====== */ - -static void nat_detection_handle_nat_info(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, - struct ETCP_CONN* from_conn, - const uint8_t* data, size_t len) { - if (!nd || !from_conn || !data || len < sizeof(struct NATDET_MSG_NAT_INFO)) return; - - const struct NATDET_MSG_NAT_INFO* info = (const struct NATDET_MSG_NAT_INFO*)data; - uint32_t nat_ip = (info->nat_ip[0] << 24) | (info->nat_ip[1] << 16) - | (info->nat_ip[2] << 8) | info->nat_ip[3]; - uint16_t nat_port = ntohs(info->nat_port); - uint8_t socket_id = info->socket_id; - - uint8_t verified_type; - if (info->nat_type == NAT_TYPE_EIM) verified_type = NAT_VERIFIED_EIM; - else if (info->nat_type == NAT_TYPE_STRICT) verified_type = NAT_VERIFIED_STRICT; - else if (info->nat_type == NAT_TYPE_DIRECT) verified_type = NAT_VERIFIED_DIRECT; - else verified_type = NAT_VERIFIED_UNKNOWN; - - /* Хозяин применяет свой NAT-тип: обновляем только сокет, а nodeinfo - перестраивается и переподписывается штатно через topo_node_update_my_addresses. */ - (void)group; - struct ETCP_SOCKET* es = nd->inst->etcp_sockets; - while (es) { - if (es->sock_id == socket_id) { - struct sockaddr_in* nat_sin = (struct sockaddr_in*)&es->nat_addr; - nat_sin->sin_family = AF_INET; - nat_sin->sin_addr.s_addr = nat_ip; - nat_sin->sin_port = htons(nat_port); - es->nat_type = verified_type; - DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT_INFO socket=%s id=%d type=%d addr=%s:%u", - es->name, socket_id, verified_type, - ip_to_str(&nat_ip, AF_INET).str, nat_port); - break; +static int public_address(struct NAT_DETECTION *detector, const struct sockaddr_storage *address) { + if (address->ss_family == AF_INET) { + uint32_t ip = ((const struct sockaddr_in *)address)->sin_addr.s_addr; + return ip && (ip_is_public(ip) || detector->allow_local); + } + if (address->ss_family == AF_INET6) { + const uint8_t *ip = ((const struct sockaddr_in6 *)address)->sin6_addr.s6_addr; + return (ip[0] & 0xe0) == 0x20 || (detector->allow_local && !memcmp(ip, &in6addr_loopback, 16)); + } + return 0; +} + +static void publish(struct NAT_DETECTION *detector) { + if (detector->stopping || !detector->instance->topo_groups) return; + detector->publish_pending = topo_node_update_my_addresses(detector->instance) < 0; + if (detector->publish_pending) detector->publish_after = get_time_tb() + 10000; + if (detector->publish_pending) DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT profile publication failed; retry scheduled"); +} + +static struct nat_socket *socket_state(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, int create) { + if (!detector || !socket || socket->probe_only || detector->stopping) return NULL; + for (struct nat_socket *state = detector->sockets; state; state = state->next) + if (state->socket == socket) return state; + if (!create) return NULL; + struct nat_socket *state = u_calloc(1, sizeof(*state)); + if (!state) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT endpoint state allocation failed socket=%s", socket->name); return NULL; } + state->detector = detector; state->socket = socket; state->local = socket->local_addr; + if (socket->is_tcp) state->local = socket->interface_addr; + state->due = get_time_tb() + 2500; + state->next = detector->sockets; detector->sockets = state; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT endpoint created socket=%s id=%u generation=%016llx protocol=%s local=%s", + socket->name, socket->sock_id, (unsigned long long)socket->generation, + socket->is_tcp ? "TCP" : "UDP", sockaddr_storage_to_str(&state->local).str); + return state; +} + +/* История сохраняется один раз на новый IP. Перезапуск на том же IP:port её не очищает. */ +static int history_table(struct NAT_DETECTION *detector) { + if (detector->history_table_ready) return 0; + sqlite3 *db = detector->instance->topo_sqlite_db; + if (!db) return -1; + if (sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS nat_contacts(local BLOB NOT NULL,remote BLOB NOT NULL," + "PRIMARY KEY(local,remote)) WITHOUT ROWID", NULL, NULL, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT contact history initialization failed: %s", sqlite3_errmsg(db)); return -1; + } + detector->history_table_ready = 1; return 0; +} + +static int history_insert(struct nat_socket *state, const uint8_t remote[17]) { + uint8_t local[ROUTE_ADDR_WIRE_SIZE]; + if (route_ping_addr_encode(local, &state->local) || history_table(state->detector)) return -1; + sqlite3_stmt *statement = NULL; + sqlite3 *db = state->detector->instance->topo_sqlite_db; + if (sqlite3_prepare_v2(db, "INSERT OR IGNORE INTO nat_contacts(local,remote) VALUES(?,?)", + -1, &statement, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT contact write failed: %s", sqlite3_errmsg(db)); return -1; + } + sqlite3_bind_blob(statement, 1, local, sizeof(local), SQLITE_TRANSIENT); + sqlite3_bind_blob(statement, 2, remote, 17, SQLITE_TRANSIENT); + int result = sqlite3_step(statement); sqlite3_finalize(statement); + if (result != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT contact insert failed: %s", sqlite3_errmsg(db)); return -1; } + return 0; +} + +static void history_mark_incomplete(struct nat_socket *state) { + static const uint8_t marker[17]; + state->history_complete = 0; state->history_incomplete = 1; + /* family=0 — постоянный маркер неполной истории, не адрес помощника. */ + if (history_insert(state, marker)) DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT incomplete history marker persistence failed"); +} + +static void history_load(struct nat_socket *state) { + if (state->history_loaded || state->socket->is_tcp) return; + struct NAT_DETECTION *detector = state->detector; + if (history_table(detector)) { state->history_complete = 0; return; } + uint8_t local[ROUTE_ADDR_WIRE_SIZE]; + if (route_ping_addr_encode(local, &state->local)) return; + sqlite3_stmt *statement = NULL; + sqlite3 *db = detector->instance->topo_sqlite_db; + if (sqlite3_prepare_v2(db, "SELECT remote FROM nat_contacts WHERE local=?", -1, &statement, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT history read failed: %s", sqlite3_errmsg(db)); return; + } + sqlite3_bind_blob(statement, 1, local, sizeof(local), SQLITE_TRANSIENT); + state->history_complete = !state->history_incomplete; + int result; + while ((result = sqlite3_step(statement)) == SQLITE_ROW) { + const uint8_t *remote = sqlite3_column_blob(statement, 0); + if (!remote || sqlite3_column_bytes(statement, 0) != 17 || (remote[0] != 4 && remote[0] != 6)) { + state->history_incomplete = 1; state->history_complete = 0; continue; } - es = es->next; + unsigned index; + for (index = 0; index < state->contact_count; index++) if (!memcmp(state->contacts[index], remote, 17)) break; + if (index < state->contact_count) continue; + if (state->contact_count == NAT_MAX_CONTACTS) { state->history_incomplete = 1; state->history_complete = 0; continue; } + memcpy(state->contacts[state->contact_count++], remote, 17); } - if (nd->inst->topo_groups) - topo_node_update_my_addresses(nd->inst); + if (result != SQLITE_DONE) { + state->history_complete = 0; + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT history query failed: %s", sqlite3_errmsg(db)); + } + sqlite3_finalize(statement); state->history_loaded = 1; + /* Сохраняем также отправки, которые случились до открытия общей SQLite. */ + for (unsigned i = 0; i < state->contact_count; i++) + if (history_insert(state, state->contacts[i])) { history_mark_incomplete(state); break; } + if (state->history_incomplete) history_mark_incomplete(state); + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT history loaded socket=%s contacts=%u complete=%u", + state->socket->name, state->contact_count, state->history_complete); +} - struct ETCP_LINK* l = from_conn->links; - while (l) { l->nat_type = info->nat_type; l = l->next; } +static int contacted(struct nat_socket *state, const struct sockaddr_storage *address) { + uint8_t remote[ROUTE_ADDR_WIRE_SIZE]; + if (!state->history_complete || route_ping_addr_encode(remote, address)) return 1; + for (unsigned i = 0; i < state->contact_count; i++) if (!memcmp(state->contacts[i], remote, 17)) return 1; + return 0; } -static void nat_detection_handle_nat_check_req(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, - struct ETCP_CONN* from_conn, - const uint8_t* data, size_t len) { - if (!nd || !from_conn || !data || len < sizeof(struct NATDET_MSG_NAT_CHECK_REQ)) return; - (void)group; - const struct NATDET_MSG_NAT_CHECK_REQ* req = (const struct NATDET_MSG_NAT_CHECK_REQ*)data; - uint8_t socket_id = req->socket_id; - struct ETCP_LINK* target_link = NULL; - struct ETCP_LINK* l = from_conn->links; - while (l) { - if (l->remote_socket_id == socket_id) { target_link = l; break; } - l = l->next; +void nat_detection_note_send(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, + const struct sockaddr_storage *destination) { + if (!detector || !socket || socket->is_tcp || socket->probe_only || socket->socks_udp || detector->stopping) return; + struct nat_socket *state = socket_state(detector, socket, 1); + if (!state) return; + history_load(state); + if (state->phase == NAT_PRE && !state->pre_received && + route_ping_addr_equal(destination, &state->helper_address, 0)) { + state->filter_clean = 0; + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT ingress contaminated by outgoing UDP socket=%s round=%016llx destination=%s", + socket->name, (unsigned long long)state->round, sockaddr_storage_to_str(destination).str); } - if (!target_link) return; - if (target_link->nat_check_status == NAT_CHECK_IN_PROGRESS) return; - nat_detection_start_link_check(nd, target_link); + uint8_t local[ROUTE_ADDR_WIRE_SIZE], remote[ROUTE_ADDR_WIRE_SIZE]; + if (route_ping_addr_encode(local, &state->local) || route_ping_addr_encode(remote, destination)) return; + for (unsigned i = 0; i < state->contact_count; i++) if (!memcmp(state->contacts[i], remote, 17)) return; + if (state->contact_count == NAT_MAX_CONTACTS) { + if (state->history_complete) DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT contact history full socket=%s", socket->name); + if (!state->history_incomplete) history_mark_incomplete(state); + return; + } + memcpy(state->contacts[state->contact_count++], remote, 17); + if (!state->history_loaded || !detector->instance->topo_sqlite_db) { state->history_complete = 0; return; } + if (history_insert(state, remote)) history_mark_incomplete(state); } +static int send_control(struct NAT_DETECTION *detector, uint64_t destination, struct nat_wire *message) { + struct ETCP_CONN *connection = live_connection(detector, destination); + if (!connection) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT control peer unavailable peer=%016llx", (unsigned long long)destination); return -1; + } + struct ll_entry *entry = queue_entry_new(0); + if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT control queue allocation failed"); return -1; } + entry->dgram = u_malloc(sizeof(*message)); + if (!entry->dgram) { queue_entry_free(entry); DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT control allocation failed"); return -1; } + message->cmd = ETCP_ID_NAT_DETECTION; message->version = NAT_WIRE_VERSION; + memcpy(entry->dgram, message, sizeof(*message)); entry->len = sizeof(*message); + if (etcp_send(connection, entry)) { + queue_dgram_free(entry); queue_entry_free(entry); + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT control send failed peer=%016llx command=%u", + (unsigned long long)destination, message->command); return -1; + } + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT control TX command=%u step=%u peer=%016llx owner=%016llx round=%016llx helper=%016llx", + message->command, message->step, (unsigned long long)destination, + (unsigned long long)read64(message->owner), (unsigned long long)read64(message->round), + (unsigned long long)read64(message->helper)); + return 0; +} -/* ====== ETCP callbacks (self-registering) ====== */ +static void make_message(struct nat_socket *state, struct nat_wire *message, uint8_t command) { + memset(message, 0, sizeof(*message)); message->command = command; + message->owner_socket = state->socket->sock_id; message->helper_socket = state->helper_socket; + write64(message->owner, state->detector->instance->node_id); + write64(message->generation, state->socket->generation); write64(message->round, state->round); + write64(message->helper, state->helper); write64(message->token, state->token); + memcpy(message->owner_key, state->detector->instance->my_keys.public_key, 32); + memcpy(message->helper_key, state->helper_key, 32); + route_ping_addr_encode(message->target, state->socket->is_tcp ? &state->local : &state->baseline); + if (state->helper_address.ss_family) route_ping_addr_encode(message->primary, &state->helper_address); + int timeout = 1000; + struct ETCP_CONN *observer = live_connection(state->detector, state->observer); + for (struct ETCP_LINK *link = observer ? observer->links : NULL; link; link = link->next) + if (link->rtt_last / 10 * 4 > timeout) timeout = link->rtt_last / 10 * 4; + if (timeout > 3000) timeout = 3000; + message->timeout_ms[0] = timeout >> 8; message->timeout_ms[1] = timeout; + message->excluded_count = state->excluded_count; + for (unsigned i = 0; i < state->excluded_count; i++) write64(message->excluded[i], state->excluded[i]); +} -static void nat_detection_trigger_checks(struct NAT_DETECTION* nd); +static struct nat_binding *binding_find(struct NAT_DETECTION *detector, uint64_t owner, uint64_t generation, uint64_t round) { + for (struct nat_binding *binding = detector->bindings; binding; binding = binding->next) + if (binding->owner == owner && binding->generation == generation && binding->round == round && + binding->expires > get_time_tb()) return binding; + return NULL; +} -static void nat_detection_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)event; - struct NAT_DETECTION* nd = (struct NAT_DETECTION*)arg; - if (!nd) return; - nat_detection_trigger_checks(nd); +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) { + if (!detector || detector->stopping) return; + if (length != ROUTE_OBSERVE_TAG_SIZE || !tag || memcmp(tag, "NAT2UDP!", 8)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation query has invalid tag length=%zu", length); return; + } + uint64_t owner = sc_derive_node_id_from_pubkey(peer_key), generation = read64(tag + 9), round = read64(tag + 17); + if (!generation || !round) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation query has zero generation/round"); return; } + struct nat_binding *binding = binding_find(detector, owner, generation, round); + if (!binding) { + unsigned count = 0; + for (struct nat_binding *item = detector->bindings; item; item = item->next) count++; + if (count >= NAT_MAX_BINDINGS) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation cache full"); return; } + binding = u_calloc(1, sizeof(*binding)); + if (!binding) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT observation cache allocation failed"); return; } + binding->next = detector->bindings; detector->bindings = binding; + } + binding->owner = owner; binding->generation = generation; binding->round = round; binding->owner_socket = tag[8]; + memcpy(binding->owner_key, peer_key, 32); binding->target = *source; binding->expires = get_time_tb() + NAT_ROUND_TB; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT observation owner=%016llx socket=%u generation=%016llx round=%016llx observed=%s via=%s", + (unsigned long long)owner, tag[8], (unsigned long long)generation, (unsigned long long)round, + sockaddr_storage_to_str(source).str, socket->name); } -static void nat_detection_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event; - struct NAT_DETECTION* nd = (struct NAT_DETECTION*)arg; - if (!nd || !conn) return; - route_ping_cancel_for_conn(nd, conn); - struct ETCP_LINK* lnk = conn->links; - while (lnk) { - if (lnk->nat_check_status == NAT_CHECK_IN_PROGRESS) lnk->nat_check_status = NAT_CHECK_NONE; - lnk = lnk->next; +static void clear_profile(struct nat_socket *state, uint8_t reason) { + int changed = state->socket->address_profile.mapping || state->socket->address_profile.inbound || + state->socket->nat_addr.ss_family; + memset(&state->socket->address_profile, 0, sizeof(state->socket->address_profile)); + memset(&state->socket->nat_addr, 0, sizeof(state->socket->nat_addr)); + state->completed_tb = 0; + if (changed) { + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT profile invalidated socket=%s generation=%016llx reason=%s", + state->socket->name, (unsigned long long)state->socket->generation, nat_check_reason_name(reason)); + publish(state->detector); } } -/* Закрытие ОДНОГО линка (без разрыва conn): etcp_fire_link_status_cbk вызывается - * из etcp_link_close до u_free(link) — здесь link ещё валиден. Гасим NAT-чек этого - * линка, иначе na->link останется висячим указателем. */ -static void nat_detection_on_link_status(struct ETCP_CONN* conn, struct ETCP_LINK* link, - int old_state, int old_status, void* arg) { - (void)conn; (void)old_state; (void)old_status; - struct NAT_DETECTION* nd = (struct NAT_DETECTION*)arg; - if (!nd || !link) return; - if (link->link_status == 0) route_ping_cancel_for_link(nd, link); +static void owner_finish(struct nat_socket *state, uint8_t reason) { + etcp_cancel_pings(state->detector->instance, owner_ping_cb, state); + state->active_ping = 0; state->phase = NAT_IDLE; state->reason = reason; + state->preferred_observer = 0; + state->due = get_time_tb() + (reason ? NAT_RETRY_TB : NAT_REFRESH_TB); + if (reason) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT round incomplete socket=%s generation=%016llx round=%016llx reason=%s", + state->socket->name, (unsigned long long)state->socket->generation, + (unsigned long long)state->round, nat_check_reason_name(reason)); + } } -static void nat_detection_new_conn_cbk(struct ETCP_CONN* conn, void* arg) { - if (!conn || !conn->instance) return; - struct NAT_DETECTION* nd = (struct NAT_DETECTION*)arg; - DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT detection set callbacks: %s", conn->log_name); - etcp_conn_add_cbk(conn, nat_detection_on_conn_up, nd, ETCP_CBK_EVENT_UP); - etcp_conn_add_cbk(conn, nat_detection_on_conn_down, nd, ETCP_CBK_EVENT_DOWN); +static void owner_apply(struct nat_socket *state, uint8_t mapping, uint8_t inbound) { + int64_t now = ntp_time_get_us(state->detector->instance); + if (now <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT profile time unavailable"); owner_finish(state, NAT_REASON_INVALID_OBSERVATION); return; } + struct address_profile profile = {.mapping = mapping, .inbound = inbound, .evidence_id = state->round, .checked_at = now}; + uint8_t partial_reason = inbound != ADDRESS_IN_UNKNOWN ? NAT_REASON_NONE : !state->filter_clean ? NAT_REASON_CONTACT_HISTORY : + mapping == ADDRESS_MAP_DEPENDENT ? NAT_REASON_DEPENDENT_MAPPING : NAT_REASON_TIMEOUT; + state->socket->address_profile = profile; + memset(&state->socket->nat_addr, 0, sizeof(state->socket->nat_addr)); + if (mapping == ADDRESS_MAP_EIM) state->socket->nat_addr = state->baseline; + state->completed_tb = get_time_tb(); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT profile socket=%s id=%u generation=%016llx evidence=%016llx mapping=%s inbound=%s" + " local=%s external=%s observer=%016llx helper=%016llx clean=%u partial_reason=%s", + state->socket->name, state->socket->sock_id, (unsigned long long)state->socket->generation, + (unsigned long long)state->round, address_mapping_name(mapping), address_inbound_name(inbound), + sockaddr_storage_to_str(&state->local).str, sockaddr_storage_to_str(&state->baseline).str, + (unsigned long long)state->observer, (unsigned long long)state->helper, state->filter_clean, + nat_check_reason_name(partial_reason)); + owner_finish(state, NAT_REASON_NONE); state->reason = partial_reason; publish(state->detector); } +static int owner_find(struct nat_socket *state) { + struct nat_wire message; make_message(state, &message, NAT_FIND); + state->phase = NAT_FIND_C; + return send_control(state->detector, state->observer, &message); +} -/* ====== Receive callback (self-binded on ETCP_ID_NAT_DETECTION) ====== */ +static int owner_run(struct nat_socket *state, uint8_t step) { + if (random_id(&state->token)) return -1; + struct nat_wire message; make_message(state, &message, NAT_RUN); message.step = step; + state->phase = step == NAT_STEP_TCP ? NAT_TCP : step == NAT_STEP_PRE ? NAT_PRE : NAT_POST; + return send_control(state->detector, state->observer, &message); +} -static const char* nat_subcmd_name(uint8_t subcmd) { - switch (subcmd) { - case NATDET_SUBCMD_PING_REQ: return "PING_REQ"; - case NATDET_SUBCMD_PING_RESP: return "PING_RESP"; - case NATDET_SUBCMD_NAT_INFO: return "NAT_INFO"; - case NATDET_SUBCMD_NAT_CHECK_REQ: return "NAT_CHECK_REQ"; - default: return "?"; +static int owner_observe(struct nat_socket *state) { + const struct sockaddr_storage *target = &state->observer_address; + const uint8_t *key = state->observer_key; + if (state->phase == NAT_OBSERVE_C || state->phase == NAT_OBSERVE_ALT) { + target = state->phase == NAT_OBSERVE_C ? &state->helper_address : &state->alternate_address; + key = state->helper_key; } + int timeout = 1000; + struct ETCP_CONN *connection = live_connection(state->detector, state->observer); + for (struct ETCP_LINK *link = connection ? connection->links : NULL; link; link = link->next) + if (link->rtt_last / 10 * 4 > timeout) timeout = link->rtt_last / 10 * 4; + if (timeout > 3000) timeout = 3000; + if (route_ping_observe(state->socket, key, target, state->round, timeout, owner_ping_cb, state)) return -1; + state->active_ping = 1; return 0; } -static void nat_detection_receive_cbk(struct ETCP_CONN* from_conn, struct ll_entry* entry) { - if (!from_conn || !entry || entry->len < 2) { - if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return; +static void owner_ping_cb(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) { + (void)rtt; (void)nonce; + struct nat_socket *state = arg; + state->active_ping = 0; + if (state->phase == NAT_IDLE) return; + struct sockaddr_storage observed; + if (!success || route_ping_observation(data, length, state->socket, state->round, &observed)) { + if (++state->retries < NAT_PROBE_COUNT) { state->due = get_time_tb() + 1000; return; } + owner_finish(state, success ? NAT_REASON_INVALID_OBSERVATION : NAT_REASON_TIMEOUT); return; } - struct UTUN_INSTANCE* instance = from_conn->instance; - if (!instance || !instance->nat_det) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid instance/nat_det"); - queue_dgram_free(entry); queue_entry_free(entry); return; + state->retries = 0; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT own observation socket=%s phase=%u round=%016llx observed=%s", + state->socket->name, state->phase, (unsigned long long)state->round, sockaddr_storage_to_str(&observed).str); + if (state->phase == NAT_OBSERVE_B) { + state->baseline = observed; + if (state->socket->nat_addr.ss_family && !route_ping_addr_equal(&state->socket->nat_addr, &observed, 1)) + clear_profile(state, NAT_REASON_ADDRESS_CHANGED); + if (owner_find(state)) owner_finish(state, NAT_REASON_CONTROL_LOST); + return; + } + if (state->phase == NAT_OBSERVE_C) { + state->helper_seen = observed; state->phase = NAT_OBSERVE_ALT; + } else if (state->phase == NAT_OBSERVE_ALT) { + state->alternate_seen = observed; + if (state->filter_clean && state->pre_healthy && !state->pre_received && + route_ping_addr_equal(&state->baseline, &state->helper_seen, 1)) { + if (owner_run(state, NAT_STEP_POST)) owner_finish(state, NAT_REASON_CONTROL_LOST); + return; + } + state->phase = NAT_REPEAT_B; + } else if (state->phase == NAT_REPEAT_B) { + if (!route_ping_addr_equal(&observed, &state->baseline, 1)) { + clear_profile(state, NAT_REASON_ADDRESS_CHANGED); owner_finish(state, NAT_REASON_ADDRESS_CHANGED); return; + } + int same = route_ping_addr_equal(&state->baseline, &state->helper_seen, 1) && + route_ping_addr_equal(&state->baseline, &state->alternate_seen, 1); + uint8_t mapping = same ? (route_ping_addr_equal(&state->local, &state->baseline, 1) ? + ADDRESS_MAP_UNCHANGED : ADDRESS_MAP_EIM) : ADDRESS_MAP_DEPENDENT; + uint8_t inbound = state->filter_clean && state->pre_received ? ADDRESS_IN_OPEN : + state->filter_clean && state->pre_healthy && state->post_received && same ? ADDRESS_IN_CONTACT : ADDRESS_IN_UNKNOWN; + owner_apply(state, mapping, inbound); return; + } else { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation in unexpected phase=%u", state->phase); + owner_finish(state, NAT_REASON_INVALID_OBSERVATION); return; } - struct TOPO_GROUP* group = instance->topo_groups - ? topo_groups_get_default(instance->topo_groups) : NULL; - struct NAT_DETECTION* nd = instance->nat_det; - uint8_t* data = entry->dgram; uint8_t subcmd = data[1]; + if (owner_observe(state)) owner_finish(state, NAT_REASON_SEND_ERROR); +} - DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT recv %s from %s len=%zu group=%s", - nat_subcmd_name(subcmd), from_conn->log_name, entry->len, group ? "yes" : "no"); +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) { + struct nat_socket *state = socket_state(detector, socket, 0); + if (!state || !tag || length != NAT_QUERY_TAG_SIZE) { + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT ingress ignored without pending endpoint/valid tag length=%zu", length); return; + } + uint8_t expected[ROUTE_OBSERVE_TAG_SIZE]; route_ping_make_tag(expected, socket, state->round); + uint8_t step = state->phase == NAT_PRE ? NAT_STEP_PRE : state->phase == NAT_POST ? NAT_STEP_POST : 0; + if (!step || memcmp(tag, expected, sizeof(expected)) || tag[25] != step || read64(tag + 26) != state->token || + sc_derive_node_id_from_pubkey(peer_key) != state->helper || memcmp(peer_key, state->helper_key, 32) || + !route_ping_addr_equal(source, &state->helper_address, 1) || + route_ping_addr_equal(source, &state->observer_address, 0)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT ingress correlation/source mismatch socket=%s phase=%u round=%016llx source=%s", + socket->name, state->phase, (unsigned long long)state->round, sockaddr_storage_to_str(source).str); return; + } + if (step == NAT_STEP_PRE) state->pre_received = 1; + else state->post_received = 1; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT direct ingress socket=%s round=%016llx step=%u source=%s clean=%u", + socket->name, (unsigned long long)state->round, step, sockaddr_storage_to_str(source).str, state->filter_clean); +} - if (subcmd == NATDET_SUBCMD_PING_REQ) - route_ping_handle_req(nd, group, from_conn, data, entry->len); - else if (subcmd == NATDET_SUBCMD_PING_RESP) - route_ping_handle_resp(nd, from_conn, data, entry->len); - else if (subcmd == NATDET_SUBCMD_NAT_INFO) - nat_detection_handle_nat_info(nd, group, from_conn, data, entry->len); - else if (subcmd == NATDET_SUBCMD_NAT_CHECK_REQ) - nat_detection_handle_nat_check_req(nd, group, from_conn, data, entry->len); +/* Дополнительный порт наблюдателя не публикуется и не участвует в NCD. */ +static struct ETCP_SOCKET *auxiliary_socket(struct NAT_DETECTION *detector, struct ETCP_SOCKET *source) { + struct nat_socket *state = socket_state(detector, source, 1); + if (!state) return NULL; + if (state->auxiliary) return state->auxiliary; + struct CFG_SERVER server; memset(&server, 0, sizeof(server)); + server.ip = source->local_addr; server.netif_index = source->netif_index; + server.type = CFG_SERVER_TYPE_PRIVATE; server.mtu = source->mtu; server.fib = -1; + snprintf(server.name, sizeof(server.name), "nat-observer-%u", source->sock_id); + if (server.ip.ss_family == AF_INET) ((struct sockaddr_in *)&server.ip)->sin_port = 0; + else ((struct sockaddr_in6 *)&server.ip)->sin6_port = 0; + state->auxiliary = etcp_socket_add(detector->instance, &server); + if (!state->auxiliary) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT alternate observer socket failed"); return NULL; } + state->auxiliary->probe_only = 1; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT alternate observer socket=%s primary=%s alternate=%s", + source->name, sockaddr_storage_to_str(&source->local_addr).str, + sockaddr_storage_to_str(&state->auxiliary->local_addr).str); + return state->auxiliary; +} - queue_dgram_free(entry); queue_entry_free(entry); +static void helper_remove(struct nat_probe *probe) { + struct NAT_DETECTION *detector = probe->detector; + struct nat_probe **cursor = &detector->probes; + while (*cursor && *cursor != probe) cursor = &(*cursor)->next; + if (*cursor) *cursor = probe->next; + probe->cancelled = 1; + etcp_cancel_pings(detector->instance, helper_ping_cb, probe); + if (probe->tcp) { struct stcp_client *tcp = probe->tcp; probe->tcp = NULL; stcp_client_destroy(tcp); } + u_free(probe); +} + +static void helper_finish(struct nat_probe *probe, uint8_t reason) { + probe->message.command = NAT_RESULT; probe->message.reason = reason; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT helper done owner=%016llx round=%016llx step=%u sent=%u received=%u errors=%u reason=%s", + (unsigned long long)read64(probe->message.owner), (unsigned long long)read64(probe->message.round), + probe->message.step, probe->message.sent, probe->message.received, probe->message.errors, nat_check_reason_name(reason)); + if (send_control(probe->detector, probe->reply, &probe->message)) + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper result delivery failed"); + helper_remove(probe); +} + +static void helper_ping_cb(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) { + (void)rtt; (void)nonce; (void)data; (void)length; + struct nat_probe *probe = arg; + if (probe->cancelled) return; + probe->active = 0; probe->tcp = NULL; + if (success) probe->message.received++; + if (probe->message.step == NAT_STEP_TCP || probe->message.sent >= NAT_PROBE_COUNT) + helper_finish(probe, NAT_REASON_NONE); + else probe->due = get_time_tb() + 1000; +} + +static int helper_send(struct nat_probe *probe) { + struct NAT_DETECTION *detector = probe->detector; + struct sockaddr_storage target; + if (route_ping_addr_decode(probe->message.target, &target)) return -1; + int timeout = ((unsigned)probe->message.timeout_ms[0] << 8) | probe->message.timeout_ms[1]; + if (timeout < 500 || timeout > 3000) return -1; + int result; + if (probe->message.step == NAT_STEP_TCP) { + struct reality_client_config reality, *camouflage = NULL; + const struct TOPO_NODE *owner = topo_node_registry_find(detector->instance->topo_groups, read64(probe->message.owner)); + const struct TOPO_REALITY_SOCK *parameters = topo_node_find_reality_sock(owner, probe->message.owner_socket); + if (parameters) { topo_node_reality_config(&reality, parameters); camouflage = &reality; } + result = etcp_send_tcp_ping(detector->instance, probe->message.owner_key, &target, timeout, + helper_ping_cb, probe, &probe->tcp, camouflage); + } else { + struct ETCP_SOCKET *socket = find_socket(detector, probe->source_socket, probe->source_generation); + if (!socket || socket->is_tcp || socket->socks_udp) return -1; + uint8_t tag[NAT_QUERY_TAG_SIZE]; + memcpy(tag, "NAT2UDP!", 8); tag[8] = probe->message.owner_socket; + memcpy(tag + 9, probe->message.generation, 8); memcpy(tag + 17, probe->message.round, 8); + tag[25] = probe->message.step; memcpy(tag + 26, probe->message.token, 8); + result = etcp_send_ping_to_socket(detector->instance, socket, probe->message.owner_key, &target, timeout, + helper_ping_cb, probe, tag, sizeof(tag), ETCP_PING_FLAG_ADDRESS_CHECK); + } + if (result) { probe->message.errors++; return -1; } + probe->message.sent++; probe->active = 1; return 0; +} + +static void handle_find(struct NAT_DETECTION *detector, struct ETCP_CONN *from, struct nat_wire *message) { + struct nat_binding *binding = binding_find(detector, from->peer_node_id, read64(message->generation), read64(message->round)); + struct sockaddr_storage target; + if (!binding || read64(message->owner) != from->peer_node_id || + route_ping_addr_decode(message->target, &target) || !route_ping_addr_equal(&binding->target, &target, 1) || + memcmp(binding->owner_key, message->owner_key, 32)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper selection rejected: no matching own observation peer=%016llx", + (unsigned long long)from->peer_node_id); return; + } + message->command = NAT_OFFER; message->reason = NAT_REASON_NO_HELPER; memset(message->helper, 0, 8); + uint64_t selected = UINT64_MAX; + struct ETCP_LINK *selected_link = NULL; + for (struct ll_entry *entry = detector->instance->connections->head; entry; entry = entry->next) { + struct ETCP_CONN *candidate = ((struct conn_queue_entry *)entry->data)->conn; + if (!candidate || candidate == from || !live_connection(detector, candidate->peer_node_id) || + candidate->peer_node_id >= selected) continue; + int excluded = 0; + for (unsigned i = 0; i < message->excluded_count; i++) + if (read64(message->excluded[i]) == candidate->peer_node_id) excluded = 1; + if (excluded) continue; + for (struct ETCP_LINK *link = candidate->links; link; link = link->next) { + if (link->is_tcp || !link->link_status || !link->initialized || + link->remote_addr.ss_family != target.ss_family || !public_address(detector, &link->remote_addr) || + (link->remote_type != CFG_SERVER_TYPE_PUBLIC && !detector->allow_local)) continue; + selected = candidate->peer_node_id; selected_link = link; break; + } + } + if (selected_link) { + write64(message->helper, selected); message->helper_socket = selected_link->remote_socket_id; + memcpy(message->helper_key, selected_link->etcp->crypto_ctx.peer_public_key, 32); + route_ping_addr_encode(message->primary, &selected_link->remote_addr); + message->reason = NAT_REASON_NONE; + } + if (send_control(detector, from->peer_node_id, message)) + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper offer delivery failed"); } +static void handle_offer(struct nat_socket *state, struct nat_wire *message) { + uint64_t helper = read64(message->helper); + if (!helper) { + if (state->fallback_helper) { + state->helper = state->fallback_helper; state->helper_socket = state->fallback_socket; + memcpy(state->helper_key, state->fallback_key, 32); state->helper_address = state->fallback_address; + state->filter_clean = 0; + if (owner_run(state, NAT_STEP_PRE)) owner_finish(state, NAT_REASON_CONTROL_LOST); + } else owner_finish(state, NAT_REASON_NO_HELPER); + return; + } + struct sockaddr_storage address; + if (route_ping_addr_decode(message->primary, &address) || sc_derive_node_id_from_pubkey(message->helper_key) != helper) { + owner_finish(state, NAT_REASON_INVALID_OBSERVATION); return; + } + int same_ip = route_ping_addr_equal(&state->observer_address, &address, 0); + int old_contact = contacted(state, &address); + if (same_ip || old_contact) { + if (!same_ip && !state->fallback_helper) { + state->fallback_helper = helper; state->fallback_socket = message->helper_socket; + state->fallback_address = address; memcpy(state->fallback_key, message->helper_key, 32); + } + if (state->excluded_count < NAT_MAX_HELPERS) { + state->excluded[state->excluded_count++] = helper; state->helper_reselections++; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT request another helper socket=%s round=%016llx rejected=%016llx reason=%s ip=%s", + state->socket->name, (unsigned long long)state->round, (unsigned long long)helper, + same_ip ? "same_external_ip" : "previous_udp_contact", sockaddr_storage_to_str(&address).str); + if (owner_find(state)) owner_finish(state, NAT_REASON_CONTROL_LOST); + } else owner_finish(state, NAT_REASON_NO_HELPER); + return; + } + state->helper = helper; state->helper_socket = message->helper_socket; + state->helper_address = address; memcpy(state->helper_key, message->helper_key, 32); + state->filter_clean = 1; + if (owner_run(state, NAT_STEP_PRE)) owner_finish(state, NAT_REASON_CONTROL_LOST); +} -/* ====== NAT check trigger ====== */ +static int tcp_target_advertised(struct NAT_DETECTION *detector, const struct nat_wire *message, + const struct sockaddr_storage *target) { + const struct TOPO_NODE *owner = topo_node_registry_find(detector->instance->topo_groups, read64(message->owner)); + if (!owner || memcmp(owner->public_key, message->owner_key, 32)) return 0; + uint8_t address[ROUTE_ADDR_WIRE_SIZE]; + if (route_ping_addr_encode(address, target)) return 0; + if (target->ss_family == AF_INET) { + for (const struct TOPO_ADDR4 *item = owner->v4_addrs; item; item = item->next) + if (item->socket_id == message->owner_socket && (item->protocol & TOPO_PROTO_TCP) && + !memcmp(item->addr, address + 1, 4) && item->port == (((unsigned)address[17] << 8) | address[18])) return 1; + } else { + for (const struct TOPO_ADDR6 *item = owner->v6_addrs; item; item = item->next) + if (item->socket_id == message->owner_socket && (item->protocol & TOPO_PROTO_TCP) && + !memcmp(item->addr, address + 1, 16) && item->port == (((unsigned)address[17] << 8) | address[18])) return 1; + } + return 0; +} -static void nat_detection_trigger_checks(struct NAT_DETECTION* nd) { - if (!nd || !nd->inst) return; - struct ll_entry* entry = nd->inst->connections->head; - while (entry) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; - struct ETCP_LINK* l = ce->conn->links; - while (l) { - if (l->initialized && l->conn && l->nat_check_status < NAT_CHECK_IN_PROGRESS) - nat_detection_start_link_check(nd, l); - l = l->next; +static void handle_run(struct NAT_DETECTION *detector, struct ETCP_CONN *from, struct nat_wire *message) { + uint64_t owner = read64(message->owner), helper = read64(message->helper); + struct sockaddr_storage target; + if (route_ping_addr_decode(message->target, &target) || sc_derive_node_id_from_pubkey(message->owner_key) != owner) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT run has invalid owner/target"); return; + } + if (owner == from->peer_node_id && helper != detector->instance->node_id) { + struct nat_binding *binding = binding_find(detector, owner, read64(message->generation), read64(message->round)); + if (!binding || !route_ping_addr_equal(&binding->target, &target, 1) || + memcmp(binding->owner_key, message->owner_key, 32) || !live_connection(detector, helper)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT run forwarding rejected: stale observation/helper"); return; } - entry = entry->next; - } -} - - -/* ====== Lifecycle ====== */ - -struct NAT_DETECTION* nat_detection_create(struct UTUN_INSTANCE* inst) { - if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "nat_detection_create: inst is NULL"); return NULL; } - struct NAT_DETECTION* nd = u_calloc(1, sizeof(struct NAT_DETECTION)); - if (!nd) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "nat_detection_create: alloc failed"); return NULL; } - nd->inst = inst; - nd->next_ping_req_id = 1; - nd->allow_nat_check_local = 0; - etcp_bind(inst, ETCP_ID_NAT_DETECTION, nat_detection_receive_cbk); - etcp_add_new_conn_cbk(inst, nat_detection_new_conn_cbk, nd); - etcp_add_link_status_cbk(inst, nat_detection_on_link_status, nd); - DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection initialized"); - return nd; -} - -void nat_detection_destroy(struct NAT_DETECTION* nd) { - if (!nd) return; - DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection destroy"); - etcp_remove_new_conn_cbk(nd->inst, nat_detection_new_conn_cbk, nd); - etcp_remove_link_status_cbk(nd->inst, nat_detection_on_link_status, nd); - etcp_unbind(nd->inst, ETCP_ID_NAT_DETECTION); - route_ping_destroy_pending(nd); - u_free(nd); -} - - -/* ====== Link lifecycle ====== */ - -void nat_detection_link_ready(struct NAT_DETECTION* nd, struct ETCP_LINK* link) { - if (!nd || !link) return; - if (link->nat_check_status < NAT_CHECK_IN_PROGRESS) - nat_detection_start_link_check(nd, link); -} - -void nat_detection_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn) { - if (!nd || !conn) return; - route_ping_cancel_for_conn(nd, conn); -} - - -/* ====== Send NAT messages ====== */ - -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) { - (void)nd; - if (!to_conn) return; - struct NATDET_MSG_NAT_INFO* pkt = u_calloc(1, sizeof(struct NATDET_MSG_NAT_INFO)); - if (!pkt) return; - pkt->cmd = ETCP_ID_NAT_DETECTION; pkt->subcmd = NATDET_SUBCMD_NAT_INFO; - pkt->socket_id = socket_id; - pkt->nat_ip[0] = (nat_ip >> 24) & 0xFF; pkt->nat_ip[1] = (nat_ip >> 16) & 0xFF; - pkt->nat_ip[2] = (nat_ip >> 8) & 0xFF; pkt->nat_ip[3] = nat_ip & 0xFF; - pkt->nat_port = htons(nat_port); pkt->nat_type = nat_type; - struct ll_entry* e = queue_entry_new(0); - if (!e) { u_free(pkt); return; } - e->dgram = (uint8_t*)pkt; e->len = sizeof(struct NATDET_MSG_NAT_INFO); - if (etcp_send(to_conn, e) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "etcp_send NAT_INFO failed"); - u_free(pkt); queue_entry_free(e); - } -} - -static void nat_detection_send_nat_check_req(struct NAT_DETECTION* nd, struct ETCP_CONN* to_conn, - uint8_t socket_id) { - (void)nd; - if (!to_conn) return; - struct NATDET_MSG_NAT_CHECK_REQ* pkt = u_calloc(1, sizeof(struct NATDET_MSG_NAT_CHECK_REQ)); - if (!pkt) return; - pkt->cmd = ETCP_ID_NAT_DETECTION; pkt->subcmd = NATDET_SUBCMD_NAT_CHECK_REQ; - pkt->socket_id = socket_id; - struct ETCP_LINK* l = to_conn->links; - while (l) { - if (l->remote_socket_id == socket_id) { - struct sockaddr_in* sin = (struct sockaddr_in*)&l->conn->interface_addr; - if (sin->sin_family == AF_INET) { - pkt->interface_ip = sin->sin_addr.s_addr; - pkt->interface_port = sin->sin_port; - } break; + if (send_control(detector, helper, message)) DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT run forwarding failed"); + return; + } + if (helper != detector->instance->node_id || (message->step == NAT_STEP_TCP && !tcp_target_advertised(detector, message, &target))) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper run rejected helper=%016llx step=%u", (unsigned long long)helper, message->step); + return; + } + unsigned count = 0; + for (struct nat_probe *probe = detector->probes; probe; probe = probe->next) { + count++; + if (read64(probe->message.owner) == owner && read64(probe->message.round) == read64(message->round) && + read64(probe->message.token) == read64(message->token)) { + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT duplicate helper run ignored"); return; } - l = l->next; } - struct ll_entry* e = queue_entry_new(0); - if (!e) { u_free(pkt); return; } - e->dgram = (uint8_t*)pkt; e->len = sizeof(struct NATDET_MSG_NAT_CHECK_REQ); - if (etcp_send(to_conn, e) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "etcp_send NAT_CHECK_REQ failed"); - u_free(pkt); queue_entry_free(e); + if (count >= NAT_MAX_PROBES) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper probe limit reached"); return; } + struct ETCP_SOCKET *source = NULL; + if (message->step != NAT_STEP_TCP) { + struct sockaddr_storage primary; + source = find_socket(detector, message->helper_socket, 0); + if (!source || source->is_tcp || source->probe_only || source->socks_udp || + route_ping_addr_decode(message->primary, &primary) || !route_ping_addr_equal(&primary, &source->local_addr, 1)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper source is not the requested native UDP endpoint"); return; + } + struct ETCP_SOCKET *alternate = auxiliary_socket(detector, source); + if (!alternate || route_ping_addr_encode(message->secondary, &alternate->local_addr)) return; } + struct nat_probe *probe = u_calloc(1, sizeof(*probe)); + if (!probe) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT helper probe allocation failed"); return; } + probe->detector = detector; probe->message = *message; probe->reply = from->peer_node_id; + if (source) { probe->source_socket = source->sock_id; probe->source_generation = source->generation; } + probe->expires = get_time_tb() + NAT_ROUND_TB; + probe->next = detector->probes; detector->probes = probe; + if (helper_send(probe)) helper_finish(probe, NAT_REASON_SEND_ERROR); } +static void handle_result(struct nat_socket *state, const struct nat_wire *message) { + uint8_t step = state->phase == NAT_PRE ? NAT_STEP_PRE : state->phase == NAT_POST ? NAT_STEP_POST : + state->phase == NAT_TCP ? NAT_STEP_TCP : 0; + if (!step || message->step != step || read64(message->token) != state->token || read64(message->helper) != state->helper) { + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT stale helper result ignored socket=%s phase=%u", state->socket->name, state->phase); return; + } + if (message->reason || message->errors || !message->sent || message->received > message->sent) { + owner_finish(state, message->reason ? message->reason : NAT_REASON_SEND_ERROR); return; + } + if (step == NAT_STEP_TCP) { + if (message->received) owner_apply(state, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN); + else owner_finish(state, NAT_REASON_TIMEOUT); + return; + } + if (step == NAT_STEP_PRE) { + if (route_ping_addr_decode(message->secondary, &state->alternate_address) || + !route_ping_addr_equal(&state->alternate_address, &state->helper_address, 0) || + route_ping_addr_equal(&state->alternate_address, &state->helper_address, 1)) { + owner_finish(state, NAT_REASON_INVALID_OBSERVATION); return; + } + state->pre_healthy = message->sent == NAT_PROBE_COUNT; + state->phase = NAT_OBSERVE_C; + } else state->phase = NAT_REPEAT_B; + state->retries = 0; + if (owner_observe(state)) owner_finish(state, NAT_REASON_SEND_ERROR); +} -/* ====== Control API ====== */ +static void receive_control(struct ETCP_CONN *from, struct ll_entry *entry) { + struct NAT_DETECTION *detector = from && from->instance ? from->instance->nat_det : NULL; + if (!detector || !entry || entry->len != sizeof(struct nat_wire)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT control invalid size=%zu", entry ? entry->len : 0); + if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return; + } + struct nat_wire message; memcpy(&message, entry->dgram, sizeof(message)); + queue_dgram_free(entry); queue_entry_free(entry); + if (message.cmd != ETCP_ID_NAT_DETECTION || message.version != NAT_WIRE_VERSION || + message.excluded_count > NAT_MAX_HELPERS || !read64(message.generation) || !read64(message.round) || + message.command < NAT_FIND || message.command > NAT_RESULT || message.reason > NAT_REASON_INVALID_OBSERVATION || + message.step > NAT_STEP_TCP) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT control malformed command=%u version=%u", message.command, message.version); return; + } + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT control RX command=%u step=%u from=%016llx owner=%016llx round=%016llx", + message.command, message.step, (unsigned long long)from->peer_node_id, + (unsigned long long)read64(message.owner), (unsigned long long)read64(message.round)); + if (message.command == NAT_FIND) { handle_find(detector, from, &message); return; } + if (message.command == NAT_RUN) { handle_run(detector, from, &message); return; } + uint64_t owner = read64(message.owner); + if (message.command == NAT_RESULT && owner != detector->instance->node_id) { + struct nat_binding *binding = binding_find(detector, owner, read64(message.generation), read64(message.round)); + if (!binding || read64(message.helper) != from->peer_node_id) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper result has no matching delegation"); return; + } + if (send_control(detector, owner, &message)) DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper result forwarding failed"); + return; + } + struct ETCP_SOCKET *socket = find_socket(detector, message.owner_socket, read64(message.generation)); + struct nat_socket *state = socket_state(detector, socket, 0); + if (!state || owner != detector->instance->node_id || state->round != read64(message.round) || + state->observer != from->peer_node_id || state->phase == NAT_IDLE) { + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT stale/foreign owner response ignored"); return; + } + if (message.command == NAT_OFFER) { + if (state->phase == NAT_FIND_C) handle_offer(state, &message); + else DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT stale helper offer ignored"); + } else if (message.command == NAT_RESULT) handle_result(state, &message); +} -void nat_detection_request_check_all(struct NAT_DETECTION* nd) { - if (!nd || !nd->inst) return; - struct ll_entry* entry = nd->inst->connections->head; - while (entry) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; - struct ETCP_LINK* link = ce->conn->links; - while (link) { - if (link->nat_check_status < NAT_CHECK_IN_PROGRESS) - nat_detection_start_link_check(nd, link); - link = link->next; +static int select_observer(struct nat_socket *state) { + struct NAT_DETECTION *detector = state->detector; + uint64_t selected = UINT64_MAX; + struct ETCP_LINK *selected_link = NULL; + for (struct ll_entry *entry = detector->instance->connections->head; entry; entry = entry->next) { + struct ETCP_CONN *connection = ((struct conn_queue_entry *)entry->data)->conn; + if (!connection || !live_connection(detector, connection->peer_node_id) || connection->peer_node_id >= selected) continue; + if (state->preferred_observer && state->preferred_observer != connection->peer_node_id) continue; + for (struct ETCP_LINK *link = connection->links; link; link = link->next) { + if (!link->link_status || !link->initialized) continue; + if (state->socket->is_tcp) { + if (!public_address(detector, &link->remote_addr)) continue; + } else { + if (link->is_tcp || link->remote_addr.ss_family != state->local.ss_family || + !public_address(detector, &link->remote_addr) || + (link->remote_type != CFG_SERVER_TYPE_PUBLIC && !detector->allow_local)) continue; + } + selected = connection->peer_node_id; selected_link = link; break; } - entry = entry->next; } + if (!selected_link) return -1; + state->observer = selected; state->observer_address = selected_link->remote_addr; + memcpy(state->observer_key, selected_link->etcp->crypto_ctx.peer_public_key, 32); return 0; } +static void owner_start(struct nat_socket *state) { + struct NAT_DETECTION *detector = state->detector; + if (state->socket->socks_udp) { + state->reason = NAT_REASON_PROXY; state->due = UINT64_MAX; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT native endpoint not measured through SOCKS5 socket=%s", state->socket->name); return; + } + if (state->socket->type == CFG_SERVER_TYPE_LOCAL || state->socket->type == CFG_SERVER_TYPE_PRIVATE) { + state->due = UINT64_MAX; return; + } + if (state->socket->is_tcp && !public_address(detector, &state->local)) { + state->reason = NAT_REASON_NO_LISTENER; state->due = UINT64_MAX; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "TCP inbound profile remains UNKNOWN: no public listener candidate socket=%s local=%s", + state->socket->name, sockaddr_storage_to_str(&state->local).str); return; + } + if (select_observer(state)) { + if (state->reason != NAT_REASON_NO_OBSERVER) + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT profile remains UNKNOWN: no live observer socket=%s", state->socket->name); + state->reason = NAT_REASON_NO_OBSERVER; state->due = get_time_tb() + NAT_RETRY_TB; return; + } + if (random_id(&state->round)) { state->reason = NAT_REASON_SEND_ERROR; state->due = get_time_tb() + NAT_RETRY_TB; return; } + history_load(state); + state->rounds++; state->excluded_count = 0; state->fallback_helper = 0; state->helper = 0; + state->retries = 0; state->pre_received = 0; state->post_received = 0; state->filter_clean = 0; state->pre_healthy = 0; + state->deadline = get_time_tb() + NAT_ROUND_TB; state->reason = NAT_REASON_NONE; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT round start socket=%s id=%u generation=%016llx round=%016llx observer=%016llx protocol=%s", + state->socket->name, state->socket->sock_id, (unsigned long long)state->socket->generation, + (unsigned long long)state->round, (unsigned long long)state->observer, state->socket->is_tcp ? "TCP" : "UDP"); + if (state->socket->is_tcp) { + state->baseline = state->local; + state->helper = state->observer; + if (owner_run(state, NAT_STEP_TCP)) owner_finish(state, NAT_REASON_CONTROL_LOST); + } else { + state->phase = NAT_OBSERVE_B; + if (owner_observe(state)) owner_finish(state, NAT_REASON_SEND_ERROR); + } +} -/* ====== Test helper ====== */ +static void detector_tick(void *arg) { + struct NAT_DETECTION *detector = arg; detector->timer = NULL; + if (detector->publish_pending && get_time_tb() >= detector->publish_after) publish(detector); + uint64_t now = get_time_tb(); + for (struct ETCP_SOCKET *socket = detector->instance->etcp_sockets; socket; socket = socket->next) + if (!socket->probe_only) socket_state(detector, socket, 1); + for (struct nat_socket *state = detector->sockets; state; state = state->next) { + if (state->completed_tb && now - state->completed_tb > ADDRESS_PROFILE_TTL_US / 100) + clear_profile(state, NAT_REASON_TIMEOUT); + if (state->phase && now >= state->deadline) { owner_finish(state, NAT_REASON_TIMEOUT); continue; } + if (!state->phase && now >= state->due && (detector->automatic || state->forced)) { + state->forced = 0; owner_start(state); + } + else if ((state->phase == NAT_OBSERVE_B || state->phase == NAT_OBSERVE_C || + state->phase == NAT_OBSERVE_ALT || state->phase == NAT_REPEAT_B) && !state->active_ping && now >= state->due) { + if (owner_observe(state)) owner_finish(state, NAT_REASON_SEND_ERROR); + } + } + struct nat_probe *probe = detector->probes; + while (probe) { + struct nat_probe *next = probe->next; + if (now >= probe->expires) helper_finish(probe, NAT_REASON_TIMEOUT); + else if (!probe->active && now >= probe->due && helper_send(probe)) helper_finish(probe, NAT_REASON_SEND_ERROR); + probe = next; + } + struct nat_binding **cursor = &detector->bindings; + while (*cursor) { + struct nat_binding *binding = *cursor; + if (now < binding->expires) { cursor = &binding->next; continue; } + *cursor = binding->next; u_free(binding); + } + detector->timer = uasync_set_timeout(detector->instance->ua, NAT_TICK_TB, detector, detector_tick, "nat_detection"); + if (!detector->timer) DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT detector timer scheduling failed"); +} + +void nat_detection_cancel_for_conn(struct NAT_DETECTION *detector, struct ETCP_CONN *connection) { + if (!detector || !connection || detector->stopping) return; + for (struct nat_socket *state = detector->sockets; state; state = state->next) + if (state->phase && (state->observer == connection->peer_node_id || state->helper == connection->peer_node_id)) + owner_finish(state, NAT_REASON_CONTROL_LOST); + struct nat_probe *probe = detector->probes; + while (probe) { + struct nat_probe *next = probe->next; + if (probe->reply == connection->peer_node_id) helper_remove(probe); + probe = next; + } +} + +static void connection_status(struct ETCP_CONN *connection, int status, void *arg) { + struct NAT_DETECTION *detector = arg; + if (status == ETCP_CONN_STATUS_DOWN || status == ETCP_CONN_STATUS_DELETE || status == ETCP_CONN_STATUS_REINIT) + nat_detection_cancel_for_conn(detector, connection); + else if (status == ETCP_CONN_STATUS_UP) { + for (struct nat_socket *state = detector->sockets; state; state = state->next) + if (!state->phase && (state->reason == NAT_REASON_NO_OBSERVER || state->reason == NAT_REASON_NO_HELPER)) + state->due = get_time_tb() + 2500; + } +} + +void nat_detection_link_ready(struct NAT_DETECTION *detector, struct ETCP_LINK *link) { + if (!detector || !link || !link->conn || link->conn->probe_only || detector->stopping) return; + struct nat_socket *state = socket_state(detector, link->conn, 1); + if (state && !state->phase && !state->completed_tb) state->due = get_time_tb() + 2500; +} + +void nat_detection_observed_by_peer(struct NAT_DETECTION *detector, struct ETCP_LINK *link, + const struct sockaddr_storage *observed) { + if (!detector || !link || !link->conn || link->is_tcp || link->conn->socks_udp || !observed->ss_family) return; + struct nat_socket *state = socket_state(detector, link->conn, 1); + if (!state) return; + if ((link->conn->address_profile.mapping == ADDRESS_MAP_UNCHANGED && + !route_ping_addr_equal(&state->local, observed, 1)) || + (link->conn->nat_addr.ss_family && !route_ping_addr_equal(&link->conn->nat_addr, observed, 1))) + clear_profile(state, NAT_REASON_ADDRESS_CHANGED); +} + +void nat_detection_socket_removed(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket) { + if (!detector || !socket || detector->stopping) return; + if (!socket->probe_only) { detector->publish_pending = 1; detector->publish_after = get_time_tb(); } + struct nat_probe *probe = detector->probes; + while (probe) { + struct nat_probe *next = probe->next; + if (probe->source_socket == socket->sock_id && probe->source_generation == socket->generation) helper_remove(probe); + probe = next; + } + struct nat_socket **cursor = &detector->sockets; + while (*cursor && (*cursor)->socket != socket) cursor = &(*cursor)->next; + if (!*cursor) return; + struct nat_socket *state = *cursor; *cursor = state->next; + etcp_cancel_pings(detector->instance, owner_ping_cb, state); + struct ETCP_SOCKET *auxiliary = state->auxiliary; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT endpoint removed socket=%s generation=%016llx", socket->name, + (unsigned long long)socket->generation); + u_free(state); + if (auxiliary) etcp_socket_remove(auxiliary); +} + +static void socket_changed(struct ETCP_SOCKET *socket, int event, void *arg) { + struct NAT_DETECTION *detector = arg; + struct nat_socket *state = socket_state(detector, socket, 0); + if (!state || !(event & ETCP_SOCKET_EVENT_ADDR_CHANGED)) return; + clear_profile(state, NAT_REASON_ADDRESS_CHANGED); + nat_detection_socket_removed(detector, socket); + if (random_id(&socket->generation)) return; + socket_state(detector, socket, 1); +} + +struct NAT_DETECTION *nat_detection_create(struct UTUN_INSTANCE *instance) { + if (!instance || instance->api_bindings.callbacks[ETCP_ID_NAT_DETECTION]) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT detector invalid instance/binding collision"); return NULL; + } + struct NAT_DETECTION *detector = u_calloc(1, sizeof(*detector)); + if (!detector) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT detector allocation failed"); return NULL; } + detector->instance = instance; detector->automatic = 1; + if (etcp_bind(instance, ETCP_ID_NAT_DETECTION, receive_control)) { u_free(detector); return NULL; } + etcp_add_conn_status_cbk(instance, connection_status, detector); + etcp_add_socket_cbk(instance, socket_changed, detector, ETCP_SOCKET_EVENT_ADDR_CHANGED); + detector->timer = uasync_set_timeout(instance->ua, NAT_TICK_TB, detector, detector_tick, "nat_detection"); + if (!detector->timer) { + etcp_remove_conn_status_cbk(instance, connection_status, detector); + etcp_remove_socket_cbk(instance, socket_changed, detector); + etcp_unbind(instance, ETCP_ID_NAT_DETECTION); u_free(detector); + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT detector initial timer failed"); return NULL; + } + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT endpoint detector started: UDP mapping/inbound measured separately, TCP listener probes"); + return detector; +} + +void nat_detection_destroy(struct NAT_DETECTION *detector) { + if (!detector) return; + detector->stopping = 1; + if (detector->timer) uasync_cancel_timeout(detector->instance->ua, detector->timer); + etcp_remove_conn_status_cbk(detector->instance, connection_status, detector); + etcp_remove_socket_cbk(detector->instance, socket_changed, detector); + etcp_unbind(detector->instance, ETCP_ID_NAT_DETECTION); + while (detector->probes) helper_remove(detector->probes); + while (detector->sockets) { + struct nat_socket *state = detector->sockets; detector->sockets = state->next; + etcp_cancel_pings(detector->instance, owner_ping_cb, state); + struct ETCP_SOCKET *auxiliary = state->auxiliary; u_free(state); + if (auxiliary) etcp_socket_remove(auxiliary); + } + while (detector->bindings) { + struct nat_binding *binding = detector->bindings; detector->bindings = binding->next; u_free(binding); + } + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT detector stopped; all endpoint rounds and helper probes cancelled"); + u_free(detector); +} + +void nat_detection_request_check_all(struct NAT_DETECTION *detector) { + if (!detector || detector->stopping) return; + for (struct ETCP_SOCKET *socket = detector->instance->etcp_sockets; socket; socket = socket->next) { + struct nat_socket *state = socket_state(detector, socket, 1); + if (!state) continue; + if (state->phase) owner_finish(state, NAT_REASON_CANCELLED); + state->due = get_time_tb(); state->forced = 1; state->preferred_observer = 0; + } + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT endpoint recheck requested"); +} + +int nat_detection_request_check_socket(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, uint64_t observer) { + struct nat_socket *state = socket_state(detector, socket, 1); + if (!state || !observer || !live_connection(detector, observer)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT endpoint recheck has no live observer"); return -1; + } + if (state->phase) owner_finish(state, NAT_REASON_CANCELLED); + state->preferred_observer = observer; state->due = get_time_tb(); state->forced = 1; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT endpoint recheck socket=%s observer=%016llx", socket->name, (unsigned long long)observer); + return 0; +} + +void nat_detection_set_allow_local(struct NAT_DETECTION *detector, int allow) { + if (detector) detector->allow_local = !!allow; +} + +void nat_detection_set_automatic(struct NAT_DETECTION *detector, int automatic) { + if (!detector) return; + detector->automatic = !!automatic; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT automatic endpoint measurements %s", automatic ? "enabled" : "disabled"); +} -void nat_detection_set_allow_local(struct NAT_DETECTION* nd, int allow) { - if (!nd) return; - nd->allow_nat_check_local = allow ? 1 : 0; +int nat_detection_get_status(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, struct nat_check_status *status) { + struct nat_socket *state = socket_state(detector, socket, 0); + if (!state || !status) return -1; + *status = (struct nat_check_status){.generation = socket->generation, .round = state->round, .observer = state->observer, + .helper = state->helper, .rounds = state->rounds, .helper_reselections = state->helper_reselections, + .phase = state->phase, .reason = state->reason, .history_complete = state->history_complete}; + return 0; } diff --git a/src/routing_layer/nat_detection.h b/src/routing_layer/nat_detection.h index 1b4cf2aa..98a602e2 100644 --- a/src/routing_layer/nat_detection.h +++ b/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 +#include +#include "../lib/socket_compat.h" + #ifdef __cplusplus extern "C" { #endif -#include -#include -#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 diff --git a/src/routing_layer/route_connectivity.c b/src/routing_layer/route_connectivity.c index 11c46b88..1f7a14a1 100644 --- a/src/routing_layer/route_connectivity.c +++ b/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; diff --git a/src/routing_layer/route_ping.c b/src/routing_layer/route_ping.c index 94c0beaf..5a495625 100644 --- a/src/routing_layer/route_ping.c +++ b/src/routing_layer/route_ping.c @@ -1,428 +1,77 @@ -#include +#include "route_ping.h" + #include -#include -#ifdef _WIN32 -#include -#include -#else -#include -#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); } diff --git a/src/routing_layer/route_ping.h b/src/routing_layer/route_ping.h index 44ec8f68..6c81b397 100644 --- a/src/routing_layer/route_ping.h +++ b/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 +#include +#include "etcp_connections.h" + #ifdef __cplusplus extern "C" { #endif -#include -#include -#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 diff --git a/src/routing_layer/route_ping_doc.md b/src/routing_layer/route_ping_doc.md index ed126cf5..9729b733 100644 --- a/src/routing_layer/route_ping_doc.md +++ b/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`. diff --git a/src/routing_layer/sock_match.c b/src/routing_layer/sock_match.c index 288a4bee..1013179b 100644 --- a/src/routing_layer/sock_match.c +++ b/src/routing_layer/sock_match.c @@ -1,4 +1,3 @@ -/* sock_match.c — классификация сокетов и сопоставление адресов при подключении */ #include "sock_match.h" #include @@ -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; } diff --git a/src/routing_layer/sock_match.h b/src/routing_layer/sock_match.h index e134710e..a635d602 100644 --- a/src/routing_layer/sock_match.h +++ b/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 +#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 */ diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index 9e1a8c3b..cedb1936 100644 --- a/src/routing_layer/topo_node.c +++ b/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 изменения сокета (адрес/статус) — инициирует пересборку адресов своего узла. */ diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index 2ae850a7..ea5c3265 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -19,6 +19,7 @@ extern "C" { #include #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. */ diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index ce7962c5..2bcdc305 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/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; } diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index a86c41a7..db6a8f5b 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/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_ (реальный мембер с ключами). */ diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index b0e40ad2..6ce3005f 100644 --- a/src/transport_layer/etcp.c +++ b/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); diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 06a2f588..61578e7b 100644 --- a/src/transport_layer/etcp_connections.c +++ b/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; } diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 2f816e64..7c802016 100644 --- a/src/transport_layer/etcp_connections.h +++ b/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 @@ -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). diff --git a/src/transport_layer/etcp_dump.c b/src/transport_layer/etcp_dump.c index e190d8ff..32fcf832 100644 --- a/src/transport_layer/etcp_dump.c +++ b/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++; } diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index 39ccf238..df073e1e 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/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) { diff --git a/src/utun_instance.c b/src/utun_instance.c index 24a7f174..8f04652f 100644 --- a/src/utun_instance.c +++ b/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"); diff --git a/tests/Makefile.am b/tests/Makefile.am index 3a9ce5bc..cc4516e3 100644 --- a/tests/Makefile.am +++ b/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-условий. diff --git a/tests/nat_fixture.h b/tests/nat_fixture.h new file mode 100644 index 00000000..bf8b7ac6 --- /dev/null +++ b/tests/nat_fixture.h @@ -0,0 +1,82 @@ +#ifndef NAT_FIXTURE_H +#define NAT_FIXTURE_H + +#include +#include +#include +#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 diff --git a/tests/stream_netns/README.md b/tests/stream_netns/README.md index 07418792..6d9a3d34 100644 --- a/tests/stream_netns/README.md +++ b/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 контроллера. diff --git a/tests/stream_netns/TESTED.md b/tests/stream_netns/TESTED.md index 5e576f92..f9832a3b 100644 --- a/tests/stream_netns/TESTED.md +++ b/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 | diff --git a/tests/stream_netns/nat_scenario.py b/tests/stream_netns/nat_scenario.py new file mode 100644 index 00000000..8f326b79 --- /dev/null +++ b/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 diff --git a/tests/stream_netns/network.py b/tests/stream_netns/network.py index 5a28028a..9c71639b 100644 --- a/tests/stream_netns/network.py +++ b/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}' diff --git a/tests/stream_netns/node.c b/tests/stream_netns/node.c index 65361d46..00e6a7d9 100644 --- a/tests/stream_netns/node.c +++ b/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; diff --git a/tests/stream_netns/probe.py b/tests/stream_netns/probe.py index 4e0ecaa8..ef903491 100644 --- a/tests/stream_netns/probe.py +++ b/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): diff --git a/tests/stream_netns/run.py b/tests/stream_netns/run.py index 3349255e..394788f7 100644 --- a/tests/stream_netns/run.py +++ b/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: diff --git a/tests/stream_netns/scenario.py b/tests/stream_netns/scenario.py index c97a5a2b..7e1b6a6d 100644 --- a/tests/stream_netns/scenario.py +++ b/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 = [] diff --git a/tests/test_auto_socket_dynamic.c b/tests/test_auto_socket_dynamic.c index 45fec032..7da4d28d 100644 --- a/tests/test_auto_socket_dynamic.c +++ b/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); diff --git a/tests/test_etcp_connect.c b/tests/test_etcp_connect.c index 786735e3..191047c7 100644 --- a/tests/test_etcp_connect.c +++ b/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); diff --git a/tests/test_etcp_two_instances.c b/tests/test_etcp_two_instances.c index 5d0dcaca..c77fa7fe 100644 --- a/tests/test_etcp_two_instances.c +++ b/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; diff --git a/tests/test_ipv6_sockets.c b/tests/test_ipv6_sockets.c index 3a0347ae..8d671d7e 100644 --- a/tests/test_ipv6_sockets.c +++ b/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; } } diff --git a/tests/test_nat_detection.c b/tests/test_nat_detection.c index 7e70558f..c47ffd23 100644 --- a/tests/test_nat_detection.c +++ b/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 -#include -#include -#include -#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; } diff --git a/tests/test_node_conn_direct.c b/tests/test_node_conn_direct.c index 8b624b3c..27727b28 100644 --- a/tests/test_node_conn_direct.c +++ b/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); } diff --git a/tests/test_route_ping.c b/tests/test_route_ping.c index 78f6412f..989c02da 100644 --- a/tests/test_route_ping.c +++ b/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 -#include -#include -#include -#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; } diff --git a/tests/test_sock_match.c b/tests/test_sock_match.c index a6eb1d67..ecaa07f3 100644 --- a/tests/test_sock_match.c +++ b/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 +/* Политика не заменяет измерение; неопределённость разрешает ограниченную пробу. */ +#include #include -#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; } diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index df3d2b2a..0c7f3e18 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/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) { diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index 1dd89a70..a229ab9a 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/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; } diff --git a/tools/chatgui/src/nodespage.cpp b/tools/chatgui/src/nodespage.cpp index 32439e5c..871b56d1 100644 --- a/tools/chatgui/src/nodespage.cpp +++ b/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) diff --git a/tools/etcpmon/etcpmon_gui.c b/tools/etcpmon/etcpmon_gui.c index fbe53e7d..bb036013 100644 --- a/tools/etcpmon/etcpmon_gui.c +++ b/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); } } diff --git a/tools/etcpmon/etcpmon_protocol.h b/tools/etcpmon/etcpmon_protocol.h index 7d2abfb2..bb99bb7e 100644 --- a/tools/etcpmon/etcpmon_protocol.h +++ b/tools/etcpmon/etcpmon_protocol.h @@ -11,6 +11,7 @@ #define ETCPMON_PROTOCOL_H #include +#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) */ };