Align topology model documentation with current runtime contracts

This commit is contained in:
evgeny committed 2026-10-24 14:23:50 +02:00
1 parent c282d03f25
commit 94aaf3c8e3
2 files changed
+122 -159

No files matched your search

+4 -4
View File
@@ -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" и возвращает реальную длину.
+118 -155
View File
@@ -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.