From 199839b273c0945ee4b19fae49dc4c1387d7a2e3 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 25 Oct 2026 17:58:18 +0100 Subject: [PATCH] Cover socket reuse and TCP LAN recovery and document lifecycle --- doc/node_snapshot.md | 28 +++++++++++++++ src/transport_layer/node_conn_direct.h | 2 +- tests/test_local_routes.c | 50 +++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/doc/node_snapshot.md b/doc/node_snapshot.md index 611e329a..50b95410 100644 --- a/doc/node_snapshot.md +++ b/doc/node_snapshot.md @@ -98,6 +98,34 @@ NAT-профиль и обязательные метаданные REALITY. IPv `node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более свежая подписанная запись принимается целиком, старая уступает уже известной. Bootstrap-адреса не перекрывают подписанную запись даже при DOWN. +LAN discovery предоставляет отдельные адреса подключения и не меняет подписанный снимок. +Открытое объявление рабочих сокетов и ответ на проверку имеют Ed25519-подпись; +публичные ключи в этих пакетах отсутствуют. Получатель использует известный ключ +из registry/SQLite, а неизвестное устройство остаётся только в списке discovery. + +Подписываются весь заголовок local_transport (magic/version/type, длина, IDs/эпохи, +request=0) и тело: nonce(16), family(4/6), IP(16), количество сокетов, +затем socket_id/protocol/port(BE)/generation(BE) каждого сокета. IPv4 дополнен +нулевыми байтами. Подписанный IP обязан совпасть с источником UDP; рабочий порт +отдельный от discovery-порта. Публикуются принимающие IP/interface обычные сокеты, +без PRIVATE, probe и SOCKS UDP; до 32 endpoint на один IP, без фрагментации. + +Объявление само не разрешает NCD использовать адрес. Только для узла без прямого UP +и живого BGP-пути во всех группах получатель отправляет случайный nonce; +подписанный ответ связывает его с обоими node_id и эпохами discovery. Ответ принимается +однократно в течение 3 секунд. Это проверка свежести без часов/NTP: старые подписи, +эпохи или случайные поколения сокетов сами по себе свежесть не доказывают. +После проверки адрес действует 30 секунд по монотонным часам и только на интерфейсе +приёма (включая IPv6 scope). Повтор объявления/ответа не продлевает срок без нового nonce. + +NCD использует эти кандидаты при проверке совместимости, открытии/обновлении +и появлении локального сокета. Если сохранены только ключи, идентичность загружается +из nodes без требования старого адреса. LAN не создаёт NCD handle автоматически. +Неподтверждённые ETCP-пробы очищаются после истечения кандидата/остановки discovery; +после успешного handshake линк живёт по обычным правилам транспорта. REALITY требует +метаданных из известного снимка; прямые LAN TCP-пробы при глобальном SOCKS выключены. +Диагностика: discovery для подписи/nonce/адресов, etcp для NCD-проб и их удаления. + ID узла проверяется по public_key. Ошибка запуска возвращает NULL handle и не снимает прежний FIN_WAIT. Отдельно принятый snapshot и уже добавленные линки при частичной ошибке могут сохраниться; новый handle публикуется после подготовки ресурсов. diff --git a/src/transport_layer/node_conn_direct.h b/src/transport_layer/node_conn_direct.h index ff346312..258c0cdb 100644 --- a/src/transport_layer/node_conn_direct.h +++ b/src/transport_layer/node_conn_direct.h @@ -49,7 +49,7 @@ struct NODE_CONN_DIRECT; /* Рабочий ETCP, совместимая исходящая пара либо возможность reverse на наш опубликованный адрес. * Без линков/handles/таймеров/проб. Исходящие пары — тот же обход, что open; reverse — sock_match/публикуемые адреса. - * Учитывает IPv6 и исходящий TCP fallback; 1 — кандидат есть, 0 — нет/ошибка. SQLite не загружает. + * Учитывает IPv6 и исходящий TCP fallback; 1 — кандидат есть, 0 — нет/ошибка. Обычный snapshot ищется только в registry. * Для подтверждённого LAN-кандидата может прочитать идентичность из SQLite без регистрации. * LAN TCP соблюдает глобальную SOCKS-политику: при включённом SOCKS прямые LAN TCP-пробы не создаются. * Возможность попытки не доказывает достижимость и не подтверждает готовность группы. */ diff --git a/tests/test_local_routes.c b/tests/test_local_routes.c index fd186320..b754a3bf 100644 --- a/tests/test_local_routes.c +++ b/tests/test_local_routes.c @@ -78,15 +78,42 @@ ssize_t __wrap_socket_sendto(socket_t fd, const void* data, size_t size, const s /* Объявление идёт с отдельного discovery-порта, пригодный ETCP-порт берётся из подписи. */ static int route(struct local_route* out) { return local_transport_route(a->local_transport, b->node_id, 0, out); } -static struct UTUN_INSTANCE* create_core(struct UASYNC* ua) { - struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, +static struct UTUN_INSTANCE* create_core(struct UASYNC* ua, int tcp) { + char config[512]; + snprintf(config, sizeof(config), "[global]\nclient_type=1\nauto_sockets=no\nkeepalive_interval=100\n" - "[server: lan]\naddr=127.0.0.1:0\ntype=local\n[allowed_keys]\nallow_all=1\n"); + "[server: lan]\naddr=127.0.0.1:0\ntype=local\ntransport=%s\n[allowed_keys]\nallow_all=1\n", tcp ? "tcp" : "udp"); + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); REQUIRE(inst && !utun_core_start(inst)); REQUIRE(inst->etcp_sockets && inst->etcp_sockets->generation); return inst; } +/* Один и тот же подписанный формат адресов обслуживает STCP без публичных ключей на wire. */ +static void tcp_routes(struct UASYNC* ua) { + a = create_core(ua, 1); b = create_core(ua, 1); + REQUIRE(!topo_node_sqlite_node_update_verified(a->topo_sqlite_db, b->node_id, "B", b->my_keys.public_key, + b->my_ed25519_pubkey, 1, 1)); + REQUIRE(!topo_node_sqlite_node_update_verified(b->topo_sqlite_db, a->node_id, "A", a->my_keys.public_key, + a->my_ed25519_pubkey, 1, 1)); + REQUIRE(!chat_local_start(a) && !chat_local_start(b)); test_clock_run_for(ua, 10000); + struct local_route found; REQUIRE(route(&found) && found.socket.protocol == LOCAL_ROUTE_TCP); + a->config->global.socks_enabled = 1; + REQUIRE(!node_conn_direct_compatible(a, b->node_id)); + a->config->global.socks_enabled = 0; + REQUIRE(node_conn_direct_compatible(a, b->node_id)); + struct NODE_CONN_DIRECT* handle = NULL; + REQUIRE(node_conn_direct_open(a, b->node_id, NULL, NULL, &handle, NULL) == NCD_NEW); + test_clock_run_for(ua, 30000); + struct ETCP_CONN* conn = node_conn_direct_get_conn(handle); + REQUIRE(conn && conn->links_up && conn->links->is_tcp); + chat_local_stop(a); test_clock_run_for(ua, 10000); + REQUIRE(conn->links_up); + node_conn_direct_force_close(handle); + chat_local_stop(b); utun_instance_destroy(a); a = NULL; utun_instance_destroy(b); b = NULL; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: LAN TCP/STCP handshake, SOCKS policy and discovery stop preserve live transport"); +} + /* Сбрасываем только discovery у получателя: ключи и ядро переживают его перезапуск. */ static void restart_discovery(struct UASYNC* ua) { chat_local_stop(a); REQUIRE(!chat_local_start(a)); test_clock_run_for(ua, 10000); @@ -127,11 +154,11 @@ static void bgp_gate(struct UASYNC* ua) { int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DISCOVERY, DEBUG_LEVEL_DEBUG); - debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO); test_clock_init(10000); interface_index = if_nametoindex("lo"); REQUIRE(interface_index); struct UASYNC* ua = uasync_create(); REQUIRE(ua); - a = create_core(ua); b = create_core(ua); + a = create_core(ua, 0); b = create_core(ua, 0); REQUIRE(!chat_local_start(a) && !chat_local_start(b)); test_clock_run_for(ua, 10000); struct local_route found; @@ -158,6 +185,18 @@ int main(void) { REQUIRE(node_conn_direct_compatible(a, b->node_id) && !instance_find_conn(a, b->node_id)); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: unknown key, bad signature/IP/nonce/destination, late proof and keyless wire format"); + /* Реально пересоздаём рабочий сокет: тот же ID, новое случайное поколение и новый порт. */ + uint8_t old_id = b->etcp_sockets->sock_id; + uint64_t old_generation = b->etcp_sockets->generation; + struct CFG_SERVER server = *b->config->servers; + etcp_socket_remove(b->etcp_sockets); b->next_socket_id = old_id; + REQUIRE(etcp_socket_add(b, &server) && b->etcp_sockets->generation != old_generation); + REQUIRE(!local_transport_set_name(b->local_transport, "Socket replaced")); + test_clock_run_for(ua, 10000); + REQUIRE(route(&found) && found.socket.socket_id == old_id && found.socket.generation == b->etcp_sockets->generation); + REQUIRE(found.socket.port == ss_get_port(&b->etcp_sockets->local_addr)); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: socket ID reuse replaces generation and endpoint after a fresh nonce"); + bgp_gate(ua); block_transport = 1; struct NODE_CONN_DIRECT* handle = NULL; @@ -192,6 +231,7 @@ int main(void) { node_conn_direct_force_close(handle); chat_local_stop(a); chat_local_stop(b); utun_instance_destroy(a); a = NULL; utun_instance_destroy(b); b = NULL; + tcp_routes(ua); uasync_destroy(ua, 0); test_clock_finish(); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: signed LAN route integration challenges=%d proofs=%d", challenges, proofs); return 0;