From 94aaf3c8e34703cc0ec3b5dff5e3d7398ff3c8e5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 14:23:50 +0200 Subject: [PATCH] Align topology model documentation with current runtime contracts --- src/routing_layer/topo_node.h | 8 +- src/routing_layer/topo_node_doc.md | 273 +++++++++++++---------------- 2 files changed, 122 insertions(+), 159 deletions(-) diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index e3cecd19..d0fd382c 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -209,7 +209,7 @@ struct TOPO_NODEPATH { uint64_t exchange; // local_epoch сессии, в которой путь подтверждён NODEINFO }; -/** Узел в контексте группы (per-group данные). Хранится в TOPO_GROUP->nodes (ll_queue). */ +/** Узел группы: удалённый хранится в TOPO_GROUP->nodes (ll_queue), локальный — отдельно в local_node. */ struct TOPO_GROUP_NODE { struct ll_entry ll; // queue entry, хеш-ключ 8 байт → node_id uint64_t node_id; // идентификатор узла в этой группе @@ -249,8 +249,8 @@ struct TOPO_GROUP_NODE* topo_node_find_by_id(struct TOPO_GROUP* group, uint64_t void topo_nodeq_free_group_fields(struct TOPO_GROUPS* groups, struct TOPO_GROUP_NODE* nq); /* Освободить принадлежащие узлу записи путей и их контейнер; ETCP-соединения заимствованы. */ void topo_nodeq_clear_paths(struct TOPO_GROUP_NODE* nq); -/* Полное удаление узла из группы: отменяет влётные пробы, освобождает paths/subnets, - * убирает из group->nodes и освобождает сам nq. Единственный корректный способ удаления узла. */ +/* Полное удаление узла из group->nodes: отменяет пробы, освобождает paths/subnets, + * убирает из очереди и освобождает nq. local_node освобождает владелец группы отдельно. */ void topo_nodeq_remove_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq); /* Размер переменной части NODEINFO или -1 при NULL/счётчиках подсетей без TOPO_FLAG_SEND_SUBNETS. */ @@ -289,7 +289,7 @@ int topo_node_init_self(struct UTUN_INSTANCE* instance); int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance); /* Обработчик изменений локального сокета: обновляет адреса узла. */ void topo_node_on_socket_changed(struct ETCP_SOCKET* sock, int event, void* arg); -/* Вывести диагностику узлов группы в лог категории BGP на уровне INFO. */ +/* Вывести group->nodes в лог категории BGP на уровне INFO; отдельный local_node не добавляется. */ void topo_node_dump_all(struct TOPO_GROUP* group); /* Записать дамп в buf: длина без NUL или -1 при ошибке форматирования. Ёмкость ограничена INT_MAX. * При усечении завершает запись сразу, заменяет хвост доступной частью "\n[TRUNCATED]\n" и возвращает реальную длину. diff --git a/src/routing_layer/topo_node_doc.md b/src/routing_layer/topo_node_doc.md index 4f76be08..292427ab 100644 --- a/src/routing_layer/topo_node_doc.md +++ b/src/routing_layer/topo_node_doc.md @@ -1,189 +1,152 @@ -# topo_node +# topo_node — модель, снимок и локальное состояние узла -## 1. Назначение +## Два уровня записи -Модель данных узла топологии — структуры для представления узла в памяти, сериализации/ -десериализации в бинарный wire-формат для BGP-обмена, глобальный реестр идентичностей -(refcounting) и диагностический дамп. +TOPO_NODE содержит общую идентичность: node_id, диагностический ver, timestamp, ключи, +Ed25519-подпись, имя, тип/активность, списки SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY. +Групповых flags, group_id, подсетей и путей в нём нет. -Модуль определяет два слоя: -- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) и две - структуры узла — `TOPO_NODE` (глобальная идентичность) и `TOPO_GROUP_NODE` (per-group данные). -- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети. +TOPO_GROUP_NODE содержит node_id, подсети, пути, last_ver/last_timestamp, состояние conn_mgr, +пробы связности, presence/up, radio/phone_status и заимствованный NCD handle. +Удалённые записи находятся в group->nodes; локальная запись хранится отдельно в group->local_node. +TOPO_NODEPATH содержит заимствованный ETCP_CONN, hop_count, cumulative_rtt, exchange +и массив uint64_t[hop_count] непосредственно после структуры. -Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только -данные и их упаковка/распаковка. Персистентность — в `topo_node_sqlite`. +Все операции выполняются в uasync-потоке ядра. BGP-сессией/обменом управляет topo_group, +хранением в общей SQLite — topo_node_sqlite. Этот модуль также готовит локальную запись, +обновляет адреса и инициирует рассылку после их изменения. -Два уровня представления узла: -- **TOPO_NODE** — глобальная идентичность (pubkeys, имя, адреса, self-signature). Хранится один - раз в глобальном реестре `TOPO_GROUPS->node_registry` (hash по node_id), разделяется между - группами через `group_ref_count`. -- **TOPO_GROUP_NODE** — узел в контексте группы: `node_id`, `paths`, `subnets`, `last_ver`, - `connectivity`, `conn_presence/conn_up`, `handle`. Хранится в `TOPO_GROUP->nodes`. +## Реестр и память -## 2. Как пользоваться +node_registry хранит отдельный ll_entry с node_id и указателем TOPO_NODE. +registry_find возвращает заимствованный указатель. Для удержания записи берётся registry_ref; +последний registry_unref удаляет registry entry, списки, имя и сам объект. -### Типовые сценарии +registry_store принимает входной объект при успехе и возвращает запись с одной новой ссылкой: -**Сборка информации о себе:** -```c -topo_group_update_my_nodeinfo(instance, group); -``` -Строит `group->local_node` (`TOPO_GROUP_NODE`) из конфига: сокеты, адреса (interface/NAT/real), -подсети; при изменениях инкрементирует `ver`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети -не включаются. +- Более свежий подписанный timestamp заменяет всю запись, включая пустые списки, но адрес существующего объекта сохраняется. +- Старый/равный вход освобождается, возвращается прежняя запись. Конфликт одинаковых timestamp логируется. +- Для двух bootstrap-записей с timestamp=0 сливаются присутствующие списки; полноценная идентичность может обновить скалярные поля. +- При новой регистрации более свежий/равный сохранённый снимок SQLite может заменить вход. +- При NULL-результате вход остаётся у вызывающего. -**Сериализация для отправки по BGP:** -```c -struct TOPO_NODE* ni = topo_node_registry_find(groups, nq->node_id); -uint8_t buf[4096]; -int len = topo_node_serialize(ni, nq, group_id, flags, buf, sizeof(buf), cumulative_rtt); -``` -Запаковывает `TOPO_NODE` + per-group данные (`TOPO_GROUP_NODE`) в буфер: заголовок -`TOPOMSG_NODE`, затем имя, SOCKMETA4, ADDR4, SOCKMETA6, ADDR6, REALITY-сокеты, -подсети (если `TOPO_FLAG_SEND_SUBNETS`) и hop-лист (через `topo_node_best_hop_list`). -Без флага подсети не записываются, а оба их счётчика в заголовке равны нулю. +У зарегистрированной записи используется registry_unref. Незарегистрированный результат parser/SQL +уничтожается topo_node_destroy(groups, ni). Списки сокетов/адресов/подсетей принадлежат пулам groups, +имя, REALITY, контейнер подсетей и отдельно возвращённый hop-list — u_malloc/u_calloc. +Для освобождения pooled-списков нужен тот же контейнер groups. -**Десериализация при получении NODEINFO:** -```c -struct TOPO_NODE* ni = NULL; -struct TOPO_NODESUBNETS* subnets = NULL; -uint64_t* hop_list = NULL; uint8_t hop_count = 0; -uint16_t cumulative_rtt = 0; -topo_node_deserialize(group, data, data_len, &ni, &subnets, &hop_list, &hop_count, &cumulative_rtt); -``` -При успехе (0) восстанавливает `TOPO_NODE` (через `u_calloc`) и запрошенные подсети/hop-лист. -`out_ni` обязателен; остальные выходы независимы и могут быть NULL. Все присутствующие -секции проходят проверку и занимают своё место в пакете, даже если их выход не запрошен. -Проверяются точная длина, согласованность флага/счётчиков подсетей, профили и длины префиксов. -Подпись и допустимость BGP-пути проверяются отдельно. При ошибке (-1) выходы обнулены, -частичные объекты освобождены. Освобождение успешного результата: +topo_nodeq_free_group_fields снимает ссылку реестра, освобождает подсети и обнуляет handle; +сам nq и пути остаются. topo_nodeq_clear_paths освобождает пути/контейнер, транспорт не закрывает. +Удалённый узел удаляется через topo_nodeq_remove_node: отмена проб, удаление IPv4-маршрутов, +удаление из очереди и освобождение полей/записи. local_node освобождает владелец группы отдельно. -- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя). -- Подсети передаются в `TOPO_GROUP_NODE`; `topo_nodeq_free_group_fields` освобождает их и снимает registry ref. - Контейнер/записи путей освобождает отдельный `topo_nodeq_clear_paths`. Для целого узла используют `topo_nodeq_remove_node`. -- `u_free(hop_list)` — освободить отдельно выделенный массив переходов. +## Timestamp и подпись -**Поиск узла по ID:** -```c -struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); // per-group -struct TOPO_NODE* ni = topo_node_registry_find(groups, node_id); // глобальная идентичность -``` +timestamp — Unix microseconds, версия всей подписанной записи; 0 обозначает bootstrap. +ver — циклический счётчик 1..255, не фильтр свежести. Групповой last_timestamp хранит последнюю +принятую версию в этой группе, last_ver используется для диагностики. -**Дамп всех узлов:** -```c -topo_node_dump_all(group); // в лог -topo_node_format_all(group, buf, size); // в строку -``` +topo_node_build_sig_msg создаёт канонические байты в таком порядке: -### Ключевые концепции +| Поле/секция | Представление подписи | +| --- | --- | +| node_id, ver | uint64 в нативном порядке, затем байт ver | +| timestamp | 8 байт BE | +| public_key | X25519, 32 байта | +| node_name | До 63 байт и NUL | +| client_type, client_activity | По одному байту | +| Счётчики | По байту: v4 sockets, v4 addrs, v6 sockets, v6 addrs, REALITY | +| SOCKMETA4, ADDR4, SOCKMETA6, ADDR6 | Полные списки в текущем порядке; адреса сетевыми байтами, порты LE | +| REALITY | socket_id, short_id[8], server_pubkey[32], version[3], SNI с NUL | -- **Глобальный реестр**: `TOPO_NODE` хранится один раз в `node_registry`; `topo_node_registry_store` - дедуплицирует по `node_id` (возвращает существующий при совпадении). `registry_ref/unref` - управляют `group_ref_count`; при 0 — `topo_node_destroy`. -- **Путь**: `TOPO_NODEPATH = {conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]` - сразу после структуры. Полный hop-лист для отправки — `topo_node_best_hop_list`. -- **Self-signature**: `x25519_self_sig` — Ed25519-подпись над каноническим сообщением - (`topo_node_build_sig_msg`: pubkey || имя || client_type || client_activity || адреса). - Ставится `topo_node_sign_self`, проверяется в `topo_group_process_nodeinfo`. -- **Адреса трёх типов**: `TOPO_ADDR_INTERFACE` (LAN), `TOPO_ADDR_NAT` (после детекции), - `TOPO_ADDR_REAL` (подтверждённый прямой). Для каждого сокета — до 3 адресов. -- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): probe_status - (NONE/IN_PROGRESS/DONE/DEFERRED), RTT и статусы interface/nat/real, `probe_deferred_wait`. -- **TOPO_FLAG_SEND_SUBNETS** — если установлен, подсети сериализуются; для chat-групп сброшен. - Ненулевые счётчики подсетей без флага недопустимы, в том числе при `out_subnets=NULL`. -- **Wire-протокол**: заголовок `TOPOMSG_NODE` (packed) + динамика: `node_name`, `SOCKMETA4*N`, - `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, REALITY-сокеты, опционально подсети, hop-лист. - `topo_node_dyn_size()` возвращает размер динамики либо -1 при некорректном сочетании флага/счётчиков. +SOCKMETA содержит id/config_type/mapping/inbound/source, каждый по байту. +Групповые flags/id, подсети, hops и cumulative_rtt не входят в подпись. +Ed25519-ключ передаётся в записи и используется при verify; он не добавляется отдельным полем канонического сообщения. -## 3. API +topo_node_sign_self назначает timestamp строго выше текущего и сохранённого в SQLite, +подписывает ключом instance->my_ed25519_privkey и сохраняет снимок, если БД есть. +При ошибке восстанавливаются timestamp/подпись; другие изменённые вызывающим поля не откатываются. +Поэтому локальные обновления подписывают временный candidate до замены общей записи. -### Основные структуры +topo_node_verify проверяет диапазон timestamp, node_id=derive(X25519 public_key) +и Ed25519-подпись предоставленным ключом; возвращает 0/-1. +Проверку права участия в группе и допустимости пути выполняет вызывающий BGP-слой. -| Структура | Назначение | -|-----------|-----------| -| `struct TOPO_NODE` | Глобальная идентичность узла: `flags`, `group_id`, `node_id`, `ver`, `public_key`, `ed25519_public_key`, `x25519_self_sig`, `client_type`, `client_activity`, `node_name`, списки v4/v6 sock_meta и addrs, `group_ref_count` | -| `struct TOPO_GROUP_NODE` | Per-group запись: `node_id`, `subnets`, `paths` (queue TOPO_NODEPATH), `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) | -| `struct TOPO_NODEPATH` | Путь через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[]` | -| `struct TOPO_NODESUBNETS` | v4/v6 подсети узла (отдельный malloc, может быть NULL) | -| `struct TOPO_CONNECTIVITY` | Связность: probe_status, RTT, статусы interface/nat/real, probe-тайминги | -| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата: счётчики, `hop_count`, `client_type/activity`, `cumulative_rtt` | -| `struct TOPOMSG_ADDR4/6` | Packed wire одного адреса (addr, port, type, socket_id, protocol) | -| `struct TOPOMSG_SOCKMETA4/6` | Packed wire сокет-метаданных (id, config_type, nat_type) | -| `struct TOPOMSG_SUBNET4/6` | Packed wire подсети (addr, prefix_length) | +## NODEINFO wire -### Вспомогательные типы в памяти (связанные списки) +Сериализация ручная, без lib/serialize. Packed-заголовок TOPOMSG_NODE содержит поля записи, +group_id/flags, счётчики, hop_count и cumulative_rtt. Далее идут: -| Тип | Назначение | -|-----|-----------| -| `struct TOPO_SOCKMETA4/6` | Элемент списка сокет-метаданных (next*, id, config_type, nat_type) | -| `struct TOPO_ADDR4/6` | Элемент списка адресов (next*, addr, port, type, socket_id, protocol) | -| `struct TOPO_SUBNET4/6` | Элемент списка подсетей (next*, addr, prefix_length) | +| Порядок | Секция | +| --- | --- | +| 1 | node_name_len байт имени без NUL | +| 2 | SOCKMETA4, затем ADDR4 | +| 3 | SOCKMETA6, затем ADDR6 | +| 4 | REALITY с фиксированным полем server_name[256] | +| 5 | SUBNET4, затем SUBNET6, только с TOPO_FLAG_SEND_SUBNETS | +| 6 | hop_count идентификаторов uint64_t | -Элементы списков аллоцируются из memory pool'ов (`topo_groups`) через `memory_pool_alloc/free`. +Числа wire копируются в нативном порядке; IP — сетевые байты. Это представление отличается +от канонического сообщения подписи (timestamp BE, порты LE, строки с NUL). +Счётчики ограничены 255, имя — 63 байтами. dyn_size возвращает размер динамики или -1. -### Константы +serialize получает ni и nq, оставляет владение у вызывающего и возвращает длину либо -1. +Входные поля должны быть пригодны для публикации; serialize не проверяет подпись. +Hop-list берётся один раз из выбранного живого пути. Radio и phone_status дополняют flags. +Без TOPO_FLAG_SEND_SUBNETS счётчики подсетей равны 0; секции не записываются. -| Константа | Значение | Назначение | -|-----------|---------|-----------| -| `TOPO_ADDR_INTERFACE` / `TOPO_ADDR_NAT` / `TOPO_ADDR_REAL` | 0/1/2 | Тип адреса | -| `TOPO_PROTO_UDP` / `TOPO_PROTO_TCP` | 0x01/0x02 | Транспорт | -| `ADDR_TYPE_NETIF/DIRECT/NAT_EIM/NAT_STRICT` | 0/1/2/3 | Классификация адреса | -| `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы | -| `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы | -| `CONN_TYPE_NONE/DIRECT/REVERSE/INDIRECT` | 0/1/2/3 | Стратегия соединения | -| `CLIENT_TYPE_SERVER/DESKTOP/MOBILE` | 0/1/2 | Тип клиента | -| `CLIENT_ACTIVITY_STANDBY/ACTIVE` | 0/1 | Режим | -| `NCONN_DIRECT/INDIRECT/BGP` | 1/2/4 | Биты наличия соединений | -| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Отправлять подсети | -| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID utun-группы по умолчанию | -| `TOPO_GROUP_TYPE_UTUN/CHAT` | 1/2 | Тип группы | -| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников | +deserialize требует group->instance->topo_groups с пулами и обязательный out_ni. +Остальные out_* независимы и необязательны. Проверяются точная длина, флаг/счётчики, +профили сокетов и IPv4 /0..32, IPv6 /0..128. Все секции обходятся и проверяются независимо +от запрошенных выходов. Подпись и допустимость BGP-пути проверяются отдельно. -### Функции +Перед разбором переданные выходы обнуляются, прежние объекты не освобождаются. +Успех — 0 и атомарная выдача результата; ошибка — -1 и NULL/0 без частичных объектов. +Пустые секции дают NULL/0. ni уничтожается topo_node_destroy, подсети передаются владельцу +TOPO_GROUP_NODE, hop-list освобождается u_free. -**Реестр и refcounting:** -- `topo_node_registry_find(groups, node_id)` — найти `TOPO_NODE` в глобальном реестре. -- `topo_node_registry_store(groups, ni)` — сохранить с дедупликацией (возвращает существующий при совпадении). -- `topo_node_registry_ref/unref(groups, node_id)` — инкремент/декремент `group_ref_count`. -- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя). +## Локальная публикация -**Per-group:** -- `topo_node_find_by_id(group, node_id)` — найти `TOPO_GROUP_NODE` по node_id (hash). -- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (subnets, paths) + unref глобального TOPO_NODE. +topo_node_init_self создаёт подписанную идентичность с отдельной ссылкой ядра; 0/-1. -**Сериализация:** -- `topo_node_dyn_size(msg)` — размер динамической части по счётчикам заголовка. -- `topo_node_serialize(ni, nq, group_id, flags, out, out_max, cumulative_rtt)` — сериализовать `TOPO_NODE` + per-group в буфер. -- `topo_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_hop_list, &out_hop_count, &out_cumulative_rtt)` — восстановить из wire-формата. +topo_group_update_my_nodeinfo готовит local_node из имени, ключей, типа/активности, +radio/phone_status и конфигурационных подсетей. Сравниваются адреса и префиксы, +включая замену подсети при прежнем количестве. CHAT не анонсирует подсети. -**Пути:** -- `topo_node_best_hop_list(nq, &out_count, &out_rtt)` — лучший hop-лист (по min hop_count). +Новая запись/подсети готовятся до удаления прежнего local_node. IPv4 проходит route_insert +с запретом пересечений; при конфликте или отказе подготовки/подписи прежние узел и маршруты сохраняются. +Общая подписанная идентичность заменяется после успешного sign_self. +Возврат — число IPv4-подсетей (CHAT: 0), при ошибке -1. Рассылку запускает вызывающий. -**Подпись:** -- `topo_node_build_sig_msg(ni, buf, buf_size)` — каноническое сообщение для Ed25519-подписи. -- `topo_node_sign_self(instance, ni)` — подписать свой NODEINFO (`x25519_self_sig`). +topo_node_update_my_addresses отдельно собирает адреса рабочих UDP/TCP-сокетов. +PRIVATE исключаются, UDP probe_only исключаются; LOCAL остаются доступными для локальных подключений. +UDP публикует интерфейс и допустимый наблюдаемый NAT endpoint, IPv6 интерфейс — отдельным списком. +TCP может публиковать внешний IP с port=0, если входящий порт неизвестен. +REALITY-параметры берутся из глобальной конфигурации и связываются с socket_id. -**Обновление о себе:** -- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `local_node`. -- `topo_node_update_my_addresses(instance)` — обновить адреса. -- `topo_node_on_socket_changed(sock, event, arg)` — обработчик изменений сокетов. +Изменение подписывается/сохраняется до замены списков и рассылается во все группы. +Неизменный набор не переподписывается, но SQL-проекция обновляется повторно. +Возврат 1/0/-1; ошибка проекции может дать -1 уже после применённой подписанной записи. +on_socket_changed инициирует этот пересчёт при событии сокета. -**Диагностика:** -- `topo_node_dump_all(group)` / `topo_node_format_all(group, buf, size)` — дамп узлов. +## Пути, RTT и диагностика -**Inline-аксессоры (.h):** -- `topo_v4_sock_meta/addr`, `topo_v6_sock_meta/addr`, `topo_v4_subnets/topo_v6_subnets`, `topo_list_count`. +best_path выбирает путь с conn->links_up, без close_requested, по минимуму hop_count, +при равенстве — меньшему peer_node_id. READY проверяется отдельно владельцем групповой сессии. +best_hop_list возвращает заимствованный inline-массив; out_count обязателен, +out_rtt необязателен и получает RTT без локального линка. При отсутствии пути — NULL/0. -### Зависимости +topo_get_chain_rtt складывает rtt_last локального соединения и cumulative_rtt. +UINT16_MAX обозначает неизвестный путь/локальный RTT либо насыщение суммы. +Единицы RTT и timebase-таймингов связности — 0.1 ms. -| Модуль | Использование | -|--------|--------------| -| `ll_queue.h` | `TOPO_GROUP_NODE` — запись в очереди (hash по node_id), `paths` — вложенная очередь | -| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) | -| `mem.h` | `u_malloc`, `u_calloc`, `u_free` | -| `memory_pool.h` | Элементы списков из пулов `topo_groups` | -| `debug_config.h` | Логирование (BGP) | -| `topo_group.h` | `struct TOPO_GROUP`, `TOPO_GROUPS` | -| `route_lib.h` | `route_insert/route_delete` | -| `utun_instance.h` | `struct UTUN_INSTANCE` | +ping_request_cbk сообщает, пора ли повторить ping хотя бы в одной группе (TTL 24 часа). +ping_update_rtt обновляет время ping и минимальные RTT во всех группах, SQL и callbacks; +сам по себе не переводит статусы достижимости в REACHABLE. + +dump_all пишет group->nodes в BGP/INFO. format_all форматирует ту же очередь в буфер: +реальная длина без NUL, -1 при ошибке форматирования, некорректные аргументы дают 0. +При наличии буфера строка завершена NUL; после первого усечения запись прекращается, +хвост заменяется доступной частью маркера TRUNCATED. Ёмкость ограничена INT_MAX. + +Полные объявления и контракты — в topo_node.h; общий жизненный цикл снимка — doc/node_snapshot.md.