Cover socket reuse and TCP LAN recovery and document lifecycle

This commit is contained in:
evgeny committed 2026-10-25 17:58:18 +01:00
1 parent 8dd28fb90a
commit 199839b273
3 files changed
+74 -6

No files matched your search

+28
View File
@@ -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 публикуется после подготовки ресурсов.
+1 -1
View File
@@ -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-пробы не создаются.
* Возможность попытки не доказывает достижимость и не подтверждает готовность группы. */
+45 -5
View File
@@ -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;