From 469f95c5963c7b29daf2db011f359a39a7a05dfe Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 25 Oct 2026 17:47:08 +0100 Subject: [PATCH] Apply nonce-verified LAN endpoints to offline NCD transports --- src/chat/chat_local.c | 81 ++++++++++++++- src/chat/chat_local.h | 4 +- src/transport_layer/node_conn_direct.c | 136 ++++++++++++++++++++++++- 3 files changed, 217 insertions(+), 4 deletions(-) diff --git a/src/chat/chat_local.c b/src/chat/chat_local.c index 6feab908..7b8e3457 100644 --- a/src/chat/chat_local.c +++ b/src/chat/chat_local.c @@ -4,6 +4,13 @@ #include "chat_setting.h" #include "../utun_instance.h" #include "../local_transport/protocol.h" +#include "../routing_layer/topo_group.h" +#include "../routing_layer/topo_group_connect.h" +#include "../routing_layer/topo_node_sqlite.h" +#include "../routing_layer/sock_match.h" +#include "../transport_layer/etcp.h" +#include "../transport_layer/etcp_connections.h" +#include "../transport_layer/node_conn_direct.h" #include "../../lib/debug_config.h" #include "../../lib/mem.h" #include @@ -76,9 +83,81 @@ static void activity(struct UTUN_INSTANCE* inst, int active, void* arg) { DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "failed to apply discovery activity node=%016llx", (unsigned long long)inst->node_id); } +/* Только принимающие рабочие endpoints этого IP; скрытые и SOCKS/probe-сокеты не публикуются. */ +static int route_sockets(void* arg, const struct sockaddr_storage* address, uint32_t index, + struct local_route_socket* out, size_t capacity) { + struct UTUN_INSTANCE* inst = arg; + size_t count = 0; + for (struct ETCP_SOCKET* sock = inst->etcp_sockets; sock; sock = sock->next) { + if (sock->type == CFG_SERVER_TYPE_PRIVATE || !sock_matches_lan(sock, address, index)) continue; + uint16_t port = ss_get_port(&sock->local_addr); + if (!port || !sock->generation) { + DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN socket omitted name=%s id=%u port=%u generation=%016llx", + sock->name, sock->sock_id, port, (unsigned long long)sock->generation); continue; + } + if (count == capacity) { + DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "too many LAN sockets interface=%u capacity=%zu", index, capacity); return -1; + } + out[count++] = (struct local_route_socket){ .socket_id = sock->sock_id, .generation = sock->generation, + .port = port, .protocol = sock->is_tcp ? LOCAL_ROUTE_TCP | (sock->reality_enabled ? LOCAL_ROUTE_REALITY : 0) : LOCAL_ROUTE_UDP }; + } + return (int)count; +} + +/* Ключи не покидают ядро; wire magic/version/type отделяют эту подпись от других протоколов. */ +static int route_sign(void* arg, const uint8_t* data, size_t len, uint8_t signature[64]) { + struct UTUN_INSTANCE* inst = arg; + if (inst->node_id != sc_derive_node_id_from_pubkey(inst->my_keys.public_key) || + sc_ed25519_sign(inst->my_ed25519_privkey, data, len, signature) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN endpoint signing failed node=%016llx", (unsigned long long)inst->node_id); return -1; + } + return 0; +} + +/* Отсутствие ключа не мешает открытому discovery, но не даёт адрес для NCD. */ +static int route_verify(void* arg, uint64_t peer, const uint8_t* data, size_t len, const uint8_t signature[64]) { + struct UTUN_INSTANCE* inst = arg; + uint8_t key[32]; + struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, peer); + if (node && node->timestamp) memcpy(key, node->ed25519_public_key, sizeof(key)); + else if (!inst->topo_sqlite_db || topo_node_sqlite_get_ed25519_pubkey(inst->topo_sqlite_db, peer, key)) { + DEBUG_DEBUG(DEBUG_CATEGORY_DISCOVERY, "LAN route ignored peer=%016llx reason=unknown signing key", (unsigned long long)peer); return -1; + } + if (sc_ed25519_verify(key, data, len, signature) != SC_OK) { + DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN route signature rejected peer=%016llx", (unsigned long long)peer); return -1; + } + return 0; +} + +/* Общие адреса узла нельзя заменять, пока доступен прямой транспорт или любой BGP-путь. */ +static int route_offline(void* arg, uint64_t peer) { + struct UTUN_INSTANCE* inst = arg; + struct ETCP_CONN* conn = inst->connections ? instance_find_conn(inst, peer) : NULL; + if (conn && conn->links_up && !conn->close_requested && conn->state != 2) return 0; + for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL; + e; e = e->next) { + if (topo_node_best_path(topo_node_find_by_id((struct TOPO_GROUP*)e, peer))) return 0; + } + return 1; +} + +/* Обновление помогает уже удерживаемому NCD; право создавать соединение остаётся у сервисов. */ +static void routes_changed(void* arg, uint64_t peer) { + struct UTUN_INSTANCE* inst = arg; + if (inst->connections && node_conn_direct_update_local(inst, peer) < 0) + DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN address application failed peer=%016llx", (unsigned long long)peer); + for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL; + e; e = e->next) { + struct TOPO_GROUP* group = (struct TOPO_GROUP*)e; + if (group->group_type == TOPO_GROUP_TYPE_CHAT && !group->stopping) topo_group_connect_changed(group); + } +} + int chat_local_start(struct UTUN_INSTANCE* inst) { if (inst->local_transport || inst->client_type == CLIENT_TYPE_SERVER) return 0; - struct local_transport_callbacks callbacks = { peers_changed, received, delivered, activity_changed }; + struct local_transport_callbacks callbacks = { .peers_changed = peers_changed, .received = received, .delivery = delivered, + .activity_changed = activity_changed, .route_sockets = route_sockets, .route_sign = route_sign, + .route_verify = route_verify, .route_offline = route_offline, .routes_changed = routes_changed }; inst->local_transport = local_transport_create(inst->ua, inst->node_id, inst->name, &callbacks, inst); if (!inst->local_transport) return -1; if (chat_local_apply_settings(inst) < 0) { chat_local_stop(inst); return -1; } diff --git a/src/chat/chat_local.h b/src/chat/chat_local.h index 681b33b1..0820a402 100644 --- a/src/chat/chat_local.h +++ b/src/chat/chat_local.h @@ -1,4 +1,6 @@ -/* Адаптер локального транспорта: только доставка готовых invite-ссылок, без запуска JOIN. */ +/* Адаптер LAN discovery: готовые invite-ссылки и подписанные адреса известных узлов, без запуска JOIN. + * Адреса подтверждаются nonce-ответом только при отсутствии прямого UP и BGP-путей во всех группах. + * Ключи берутся из registry/SQLite и не передаются в открытых объявлениях; NODEINFO не изменяется. */ #ifndef CHAT_LOCAL_H #define CHAT_LOCAL_H diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index e75a6f34..a11a5829 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -20,6 +20,7 @@ #include "topo_group.h" #include "ntp_time.h" #include "config_parser.h" +#include "../local_transport/local_transport.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/u_async.h" @@ -53,6 +54,7 @@ struct ncd_entry { void* connect_timer; /* однократный таймер первого подъёма */ void* fin_wait_timer; /* таймер ожидания ответа на CLOSE */ + void* lan_timer; /* очистка неподтверждённых LAN-проб */ uint8_t up : 1; /* текущий статус: 1=есть живой линк */ uint8_t timed_out : 1; /* connect_timer уже сработал */ @@ -135,7 +137,11 @@ static struct TOPO_NODE* ncd_lookup_node(struct UTUN_INSTANCE* inst, uint64_t no } } if (inst->topo_sqlite_db && inst->topo_groups) { - struct TOPO_NODE* ni = topo_node_sqlite_node_load(inst->topo_sqlite_db, inst->topo_groups, node_id); + struct local_route route; + int lan = local_transport_route(inst->local_transport, node_id, 0, &route); + struct TOPO_NODE* ni = lan ? topo_node_sqlite_snapshot_load(inst->topo_sqlite_db, inst->topo_groups, node_id) : + topo_node_sqlite_node_load(inst->topo_sqlite_db, inst->topo_groups, node_id); + if (lan && !ni) ni = topo_node_sqlite_identity_load(inst->topo_sqlite_db, node_id); if (ni) { struct TOPO_NODE* stored = topo_node_registry_store(inst->topo_groups, ni); if (!stored) { @@ -250,6 +256,100 @@ static int ncd_add_tcp_link(struct ETCP_CONN* conn, struct ETCP_SOCKET* sock, } /* Проверка совместимости и создание используют один обход пар, включая TCP fallback и IPv6. */ +/* Сверить неподтверждённые линки с живым discovery-кешем; закрытие может вызвать callbacks NCD. */ +static int ncd_prune_lan(struct ncd_entry* entry, int force) { + struct ETCP_CONN* conn = entry->conn; + for (;;) { + struct ETCP_LINK* stale = NULL; + int pending = 0; + for (struct ETCP_LINK* link = conn->links; link; link = link->next) { + if (!link->lan_generation || link->closing) continue; + if (link->link_status) { link->lan_generation = 0; continue; } + int found = 0; + struct local_route route; + for (size_t i = 0; !force && local_transport_route(entry->inst->local_transport, entry->node_id, i, &route); i++) { + if (route.socket.generation == link->lan_generation && route.interface_index == link->lan_interface && + !!(route.socket.protocol & LOCAL_ROUTE_TCP) == !!link->is_tcp && + ncd_same_address(&route.address, &link->remote_addr) && sock_matches_lan(link->conn, &route.local_address, + route.interface_index)) { found = 1; break; } + } + if (!found) { stale = link; break; } + pending++; + } + if (!stale) return pending; + DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] retire LAN probe node=%016llx link=%u generation=%016llx interface=%u", + (unsigned long long)entry->node_id, stale->local_link_id, + (unsigned long long)stale->lan_generation, stale->lan_interface); + etcp_link_close(stale); + if (entry->closed || conn->close_requested) return 0; + } +} + +/* TTL относится к пробе: после успешного ETCP-handshake линк живёт по обычным правилам транспорта. */ +static void ncd_lan_timer(void* arg) { + struct ncd_entry* entry = arg; + entry->lan_timer = NULL; + struct ETCP_CONN* conn = entry->conn; + if (etcp_conn_ref_take(conn) < 0) return; + entry->active_calls++; + if (ncd_prune_lan(entry, 0) && !entry->closed && !conn->close_requested) { + entry->lan_timer = uasync_set_timeout(entry->ua, 10000, entry, ncd_lan_timer, "ncd_lan_probe"); + if (!entry->lan_timer) { + DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] LAN cleanup timer failed node=%016llx", (unsigned long long)entry->node_id); + ncd_prune_lan(entry, 1); + } + } + entry->active_calls--; ncd_collect(entry); etcp_conn_ref_free(conn); +} + +/* LAN-кандидаты имеют свою подпись/TTL и не подменяют содержимое или timestamp NODEINFO. */ +static int ncd_create_lan_links(struct ncd_entry* entry, const struct TOPO_NODE* ni, + struct ETCP_SOCKET* specific_sock, struct UTUN_INSTANCE* check_inst) { + struct UTUN_INSTANCE* inst = check_inst ? check_inst : entry->inst; + if (!inst->local_transport) return 0; + struct ETCP_CONN* conn = check_inst ? NULL : entry->conn; + uint64_t id = check_inst ? ni->node_id : entry->node_id; + if (conn && (conn->fin_wait || conn->close_requested)) return 0; + if (conn) ncd_prune_lan(entry, 0); + if (conn && (entry->closed || conn->close_requested)) return -1; + int count = 0; + struct local_route route; + for (size_t i = 0; local_transport_route(inst->local_transport, id, i, &route); i++) { + const struct TOPO_REALITY_SOCK* reality = NULL; + if (route.socket.protocol & LOCAL_ROUTE_REALITY) { + reality = topo_node_find_reality_sock(ni, route.socket.socket_id); + if (!reality) { + DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] LAN REALITY metadata unavailable node=%016llx socket=%u", + (unsigned long long)id, route.socket.socket_id); continue; + } + } + for (struct ETCP_SOCKET* sock = inst->etcp_sockets; sock; sock = sock->next) { + if ((specific_sock && sock != specific_sock) || !!sock->is_tcp != !!(route.socket.protocol & LOCAL_ROUTE_TCP) || + !sock_matches_lan(sock, &route.local_address, route.interface_index)) continue; + if (check_inst) return 1; + if (!entry->lan_timer) { + entry->lan_timer = uasync_set_timeout(entry->ua, 10000, entry, ncd_lan_timer, "ncd_lan_probe"); + if (!entry->lan_timer) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] LAN cleanup timer allocation failed"); return -1; } + } + int added = sock->is_tcp ? ncd_add_tcp_link(conn, sock, &route.address, reality) : + ncd_add_udp_link(conn, sock, &route.address); + if (added < 0 || entry->closed || conn->close_requested) return -1; + for (struct ETCP_LINK* link = conn->links; link; link = link->next) { + if (link->conn != sock || !!link->is_tcp != !!sock->is_tcp || !ncd_same_address(&link->remote_addr, &route.address)) continue; + if ((added || link->lan_generation) && !link->link_status) { + link->lan_generation = route.socket.generation; link->lan_interface = route.interface_index; + } + break; + } + if (added) DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] LAN probe node=%016llx socket=%u generation=%016llx interface=%u address=%s", + (unsigned long long)id, route.socket.socket_id, (unsigned long long)route.socket.generation, + route.interface_index, sockaddr_storage_to_str(&route.address).str); + count += added; + } + } + return count; +} + static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, struct ETCP_SOCKET* specific_sock, int force_socket, struct UTUN_INSTANCE* check_inst) { if (!ni) { DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] link matching without node snapshot"); return 0; } @@ -385,6 +485,10 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, } } + int lan = ncd_create_lan_links(entry, ni, specific_sock, check_inst); + if (lan < 0) return -1; + if (check_inst && lan) return 1; + link_count += lan; if (link_count > 0) { DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] create_links: node=0x%016llx v4=%d/skp=%d v6=%d/skp=%d → %d new links (UDP+TCP)", (unsigned long long)nid, v4_addrs, v4_skip, v6_addrs, v6_skip, link_count); @@ -397,7 +501,16 @@ int node_conn_direct_compatible(struct UTUN_INSTANCE* inst, uint64_t node_id) { struct ETCP_CONN* conn = instance_find_conn(inst, node_id); if (conn && conn->state != 2 && !conn->close_requested && conn->links_up) return 1; struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, node_id); - if (!node) { DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] compatibility unknown snapshot node=%016llx", (unsigned long long)node_id); return 0; } + if (!node) { + struct local_route route; + if (inst->topo_sqlite_db && local_transport_route(inst->local_transport, node_id, 0, &route)) { + struct TOPO_NODE* identity = topo_node_sqlite_identity_load(inst->topo_sqlite_db, node_id); + int possible = identity ? ncd_create_lan_links(NULL, identity, NULL, inst) : 0; + topo_node_destroy(inst->topo_groups, identity); + return possible > 0; + } + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] compatibility unknown snapshot node=%016llx", (unsigned long long)node_id); return 0; + } if (ncd_create_links(NULL, node, NULL, 0, inst) > 0) return 1; /* Входящее подключение тоже допустимо: REVERSE_REQ просит пира открыть наш опубликованный адрес. * TCP имеет собственный исходящий socket fallback; для UDP нужен известный UDP-сокет пира. @@ -461,6 +574,24 @@ int node_conn_direct_update_node(struct UTUN_INSTANCE* inst, uint64_t node_id) { return added; } +/* Адрес получен отдельно от BGP; нет транспорта — сохраняем только discovery-кеш. */ +int node_conn_direct_update_local(struct UTUN_INSTANCE* inst, uint64_t node_id) { + if (!inst || !node_id) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] invalid LAN update"); return -1; } + struct ncd_entry* entry = ncd_registry_find(inst, node_id); + if (!entry || entry->conn->fin_wait) return 0; + struct ETCP_CONN* conn = entry->conn; + if (etcp_conn_ref_take(conn) < 0) return -1; + entry->active_calls++; + struct TOPO_NODE* known = ncd_lookup_node(inst, node_id); + struct TOPO_NODE* copy = known ? ncd_copy_node(inst, known) : NULL; + if (known) topo_node_registry_unref(inst->topo_groups, node_id); + int result = copy ? ncd_create_lan_links(entry, copy, NULL, NULL) : 0; + if (!copy) ncd_prune_lan(entry, 0); + topo_node_destroy(inst->topo_groups, copy); + entry->active_calls--; ncd_collect(entry); etcp_conn_ref_free(conn); + return result; +} + /* ═══════════ единая диспетчеризация событий ═══════════ */ /* * Центральный диспетчер: принимает событие (UP/DOWN/TIMEOUT), проверяет что оно @@ -555,6 +686,7 @@ static void ncd_finish(struct ncd_entry* entry, int close_conn) { } if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; } if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; } + if (entry->lan_timer) { uasync_cancel_timeout(entry->ua, entry->lan_timer); entry->lan_timer = NULL; } struct ETCP_CONN* conn = entry->conn; entry->conn = NULL; if (conn) { if (conn->fin_wait_clear_arg == entry) {