Browse Source

topo: invite вынесен в topo_group_invite, conn_mgr упрощён до 3-фазного open; member_sync: полный пересчёт merkle-дерева при старте

- topo_group_invite.c/h: invite/join через DIRECT + INVITE_INFO_REQ/RESP (из conn_mgr)
- conn_mgr: conn_mgr_open_invite → conn_mgr_open, убран invite-режим (-480 строк)
- topo_group_connect: подключение через node_conn_direct вместо conn_mgr
- member_sync: _rebuild_all_trees сносит merkle_tree_hash канала и собирает заново из peers (чистка осиротевших листов)
- member_sync: хеш блока A/B (update_sig/adm_tags) в док-комментариях
- обновлены doc-файлы и тесты
topo_upd
evgeny 2 months ago
parent
commit
aee44943fa
  1. 18
      AGENTS.md
  2. BIN
      db_a/chats.db
  3. BIN
      db_a/chats.db-shm
  4. BIN
      db_a/chats.db-wal
  5. BIN
      db_b/chats.db
  6. BIN
      db_b/chats.db-shm
  7. BIN
      db_b/chats.db-wal
  8. 2
      src/Makefile.am
  9. 7
      src/chat/member_sync.c
  10. 24
      src/chat/member_sync.h
  11. 149
      src/chat/member_sync_doc.md
  12. 10
      src/chat/merkle_sync.h
  13. 158
      src/chat/merkle_sync_doc.md
  14. 6
      src/chat/merkle_tz.md
  15. 4
      src/media_delivery/media_delivery.c
  16. 4
      src/media_delivery/media_download.c
  17. 2
      src/media_delivery/sync.md
  18. 32
      src/routing_layer/conn_mgr.h
  19. 470
      src/routing_layer/conn_mgr_core.c
  20. 57
      src/routing_layer/conn_mgr_priv.h
  21. 2
      src/routing_layer/route6_lib_doc.md
  22. 2
      src/routing_layer/route_connectivity_doc.md
  23. 4
      src/routing_layer/route_lib_doc.md
  24. 55
      src/routing_layer/topo_group_connect.c
  25. 396
      src/routing_layer/topo_group_doc.md
  26. 535
      src/routing_layer/topo_group_invite.c
  27. 73
      src/routing_layer/topo_group_invite.h
  28. 4
      src/routing_layer/topo_node.h
  29. 208
      src/routing_layer/topo_node_doc.md
  30. 2
      src/routing_layer/topo_node_sqlite_doc.md
  31. 137
      src/routing_layer/topo_recovery.c
  32. 18
      src/routing_layer/topo_recovery.h
  33. 130
      src/transport_layer/node_conn_direct.c
  34. 6
      src/utun_instance.c
  35. 6
      tests/test_conn_mgr.c
  36. 8
      tests/test_conn_mgr_already_connected.c
  37. 21
      tests/test_invite_group_create.c

18
AGENTS.md

@ -220,12 +220,13 @@ debug - всё что нужно для понимание сути происх
info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения
warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться. warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться.
### Debug Categories (27 категорий) ### Debug Categories (28 категорий)
``` ```
NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6, NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6,
TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13, TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13,
SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20, SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20,
KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26 KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26,
MEMBER_SYNC=27
``` ```
### Настройка отладки ### Настройка отладки
@ -313,11 +314,12 @@ KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26
- `route6_lib.c/h` - IPv6 routing - `route6_lib.c/h` - IPv6 routing
- `route_ping.c/h` - Route ping probing (NAT check, liveness) - `route_ping.c/h` - Route ping probing (NAT check, liveness)
- `route_connectivity.c/h` - Route connectivity checks - `route_connectivity.c/h` - Route connectivity checks
- `topo_group.c/h` - BGP-like route exchange between nodes - `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT)
- `topo_group_connect.c/h` - BGP group connections - `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл)
- `topo_node.c/h` - Node management (peer management) - `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP)
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
- `topo_node_sqlite.c/h` - SQLite persistence for nodes - `topo_node_sqlite.c/h` - SQLite persistence for nodes
- `topo_recovery.c/h` - Topology recovery - `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND) - `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding) - `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
- `nat_detection.c/h` - NAT type detection - `nat_detection.c/h` - NAT type detection
@ -329,8 +331,8 @@ KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26
- `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик - `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик
- `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей - `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей
- `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок - `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок
- `member_sync.c/h` - Списки участников каналов в актуальном состоянии у всех. Добавил/удалил участника — изменение разойдётся по всем узлам. Работает поверх merkle_sync - `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
- `merkle_sync.c/h` - Быстрое сравнение и синхронизация данных между двумя узлами. Обмениваются контрольными суммами и передают только различия. Универсальный движок: не привязан к конкретному типу данных - `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core) - `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core) - `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core)
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core) - `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)

BIN
db_a/chats.db

Binary file not shown.

BIN
db_a/chats.db-shm

Binary file not shown.

BIN
db_a/chats.db-wal

Binary file not shown.

BIN
db_b/chats.db

Binary file not shown.

BIN
db_b/chats.db-shm

Binary file not shown.

BIN
db_b/chats.db-wal

Binary file not shown.

2
src/Makefile.am

@ -20,6 +20,7 @@ utun_CORE_SOURCES = \
routing_layer/conn_mgr_monitor.c \ routing_layer/conn_mgr_monitor.c \
routing_layer/topo_recovery.c \ routing_layer/topo_recovery.c \
routing_layer/topo_group_connect.c \ routing_layer/topo_group_connect.c \
routing_layer/topo_group_invite.c \
chat/db_sync.c \ chat/db_sync.c \
routing_layer/routing.c \ routing_layer/routing.c \
tun_if.c \ tun_if.c \
@ -101,6 +102,7 @@ libutun_a_SOURCES = \
routing_layer/conn_mgr_monitor.c \ routing_layer/conn_mgr_monitor.c \
routing_layer/topo_recovery.c \ routing_layer/topo_recovery.c \
routing_layer/topo_group_connect.c \ routing_layer/topo_group_connect.c \
routing_layer/topo_group_invite.c \
chat/db_sync.c \ chat/db_sync.c \
routing_layer/routing.c \ routing_layer/routing.c \
tun_if.c \ tun_if.c \

7
src/chat/member_sync.c

@ -735,6 +735,13 @@ static void _rebuild_all_trees(struct UTUN_INSTANCE* inst) {
const char* tbl = (const char*)sqlite3_column_text(st, 0); const char* tbl = (const char*)sqlite3_column_text(st, 0);
if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue; if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue;
const char* ch_id = tbl + 6; const char* ch_id = tbl + 6;
/* полный пересчёт: снести всё дерево канала, затем собрать заново из peers */
sqlite3_stmt* del = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE namespace=?", -1, &del, NULL) == SQLITE_OK) {
sqlite3_bind_text(del, 1, ch_id, -1, SQLITE_STATIC);
sqlite3_step(del);
sqlite3_finalize(del);
}
sqlite3_stmt* ns = NULL; sqlite3_stmt* ns = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\"", tbl); char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\"", tbl);
if (sqlite3_prepare_v2(db, sql, -1, &ns, NULL) == SQLITE_OK) { if (sqlite3_prepare_v2(db, sql, -1, &ns, NULL) == SQLITE_OK) {

24
src/chat/member_sync.h

@ -3,7 +3,7 @@
* *
* Тонкая прослойка над merkle_sync, адаптированная под мемберов каналов. * Тонкая прослойка над merkle_sync, адаптированная под мемберов каналов.
* Данные хранятся в таблицах: nodes, node_addresses, peers_<channel_id>. * Данные хранятся в таблицах: nodes, node_addresses, peers_<channel_id>.
* Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || addrs). * Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig).
* *
* ── Использование ── * ── Использование ──
* *
@ -20,12 +20,15 @@
* } * }
* *
* // добавление/обновление мембера (из chat_core_create_channel, cs_handle_welcome) * // добавление/обновление мембера (из chat_core_create_channel, cs_handle_welcome)
* member_sync_put(inst, ch_id, node_id, x25519, ed25519, join_sig, join_ts, addrs, ac); * member_sync_put(inst, ch_id, node_id, x25519, ed25519,
* join_sig, join_ts, update_sig, update_ts, userinfo,
* adm_tags, adm_tags_sig, storage);
* // дерево автоматически пересчитано * // дерево автоматически пересчитано
* *
* // онлайн-статус (из cs_on_conn_up / cs_on_conn_down) * // онлайн-статус (из cs_on_conn_up / cs_on_conn_down)
* member_sync_set_online(inst, node_id, 1); // online * // только локальная запись; возвращает 1 если изменилось — тогда рассылка:
* member_sync_set_online(inst, node_id, 0); // offline * if (member_sync_set_online(inst, node_id, 1))
* merkle_sync_push_update(inst, ch_id, node_id, 0x01, (uint8_t[]){1}, 1);
* *
* // отмена синхронизации (коллбэк НЕ вызывается) * // отмена синхронизации (коллбэк НЕ вызывается)
* member_sync_cancel(inst, peer, ch_id); * member_sync_cancel(inst, peer, ch_id);
@ -99,7 +102,8 @@ int member_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer,
void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id); void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id);
/* /*
* Добавить/обновить мембера и пересчитать дерево. * Добавить/обновить мембера (локальная запись, доверенная) и пересчитать дерево.
* Делегирует в member_sync_apply_record() с from_peer = inst->node_id.
* *
* ch_id — ID канала * ch_id — ID канала
* member_id — node_id участника * member_id — node_id участника
@ -108,13 +112,15 @@ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* c
* join_sig — Ed25519 подпись join-сообщения (64 байта) * join_sig — Ed25519 подпись join-сообщения (64 байта)
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE) * сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE)
* join_ts — NTP-синхронизированное время подписи (unix epoch seconds) * join_ts — NTP-синхронизированное время подписи (unix epoch seconds)
* update_sig — подпись блока A (64 байта), update_ts — версия блока A
* userinfo — метаданные узла (имя) в канале * userinfo — метаданные узла (имя) в канале
* adm_tags — атрибуты владельца (JSON с ver/storage), adm_tags_sig — подпись канальным ключом
* storage — производная от adm_tags.storage
* *
* Пишет только в таблицу peers_<ch_id> (INSERT OR REPLACE). * Пишет блоки через topo_node_sqlite_member_block_put/member_owner_put, узел —
* nodes обновляется отдельно через topo_node_sqlite_node_update_verified(). * через topo_node_sqlite_node_update_verified, затем merkle_sync_recompute_path().
* Вызывает merkle_sync_recompute_path() для пересчёта дерева.
* *
* Возвращает 0 при успехе, -1 при ошибке. * Возвращает маску MS_APPLY_* (0 = no-op) или -1 при ошибке.
*/ */
int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t member_id, const uint8_t* x25519, uint64_t member_id, const uint8_t* x25519,

149
src/chat/member_sync_doc.md

@ -0,0 +1,149 @@
# member_sync — синхронизация участников канала
## 1. Назначение
Тонкая прослойка над `merkle_sync`, адаптированная под участников (мемберов) чат-каналов.
Хранит мемберов в SQLite (`peers_<ch_id>`, `nodes`, `node_addresses`), пересчитывает Merkle-дерево
при каждом изменении и через merkle_sync рассылает дельту другим узлам.
Данные мембера защищены двумя независимо подписанными блоками:
- **Блок A (мембер)** — идентичность и профиль: `node_id`, `x25519`, `ed25519`, `userinfo` (имя),
подпись `update_sig` **ключом самого мембера** (`ed25519`); `ver = update_ts` внутри эпохи `join_ts`.
- **Блок B (владелец)** — атрибуты, управляемые владельцем канала: `adm_tags` (JSON с `ver`,
`storage`), подпись `adm_tags_sig` **канальным ключом** (`ed25519` канала); `ver = adm_tags.ver`.
Оба блока обновляются независимо — только если их `ver` увеличился. Устаревший блок помечается
stale и автору отправляется свежая запись (send-back).
## 2. Как пользоваться
```c
// ── разово (из chat_sync_init) ──
member_sync_init(inst); // merkle_sync_init(0x31) + verify/purge + rebuild + подписка
// ── подписка на BGP-события канала (BGP → member_sync → node_props_changed) ──
member_sync_subscribe_group(inst, group); // при создании канала
// ── запуск синхронизации (из cs_on_conn_up) ──
member_sync_start(inst, peer, ch_id, on_done, ch);
// ── коллбэк завершения ──
static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) {
if (result == MT_OK) ch->synced = CS_SYNC_DONE;
}
// ── добавление/обновление мембера (локальное, без верификации подписей) ──
member_sync_put(inst, ch_id, node_id, x25519, ed25519,
join_sig, join_ts, update_sig, update_ts, userinfo,
adm_tags, adm_tags_sig, storage);
// ── онлайн-статус: только локальная запись, возвращает 1 если изменилось ──
if (member_sync_set_online(inst, node_id, 1))
merkle_sync_push_update(inst, ch_id, node_id, 0x01, (uint8_t[]){1}, 1); // рассылает caller
// ── отмена (коллбэк не вызовется) ──
member_sync_cancel(inst, peer, ch_id);
// ── количество мемберов ──
int n = member_sync_count(inst, ch_id);
// ── разово (из chat_sync_destroy) ──
member_sync_destroy(inst);
```
## 3. Модель данных
- **Таблицы**: `peers_<ch_id>` (per-channel мемберы), `nodes`/`node_addresses` (глобальная
идентичность и адреса, обновляются отдельно через `topo_node_sqlite_*`).
- **Хеш мембера** (участвует в Merkle-дереве):
`SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig)`.
Поля `userinfo` (имя) и `source` в хеш **не** входят.
- **Колонка `source`**: `source != 0` — плейсхолдер, созданный topo_group (не верифицирован);
такие записи **исключаются** из хеша и сериализации. `source == 0` — обычный верифицированный
меркл-рекорд.
### Wire-формат мембера (get_items / apply_items / _serialize_member)
```
[count:2][node_id:8][x25519:32][ed25519:32][flags:1]
(flags&HAS_JOIN → [join_sig:64][join_ts:8])
[update_sig:64][update_ts:8]
[userinfo_len:1][userinfo]
[adm_tags_len:1][adm_tags][adm_tags_sig:64]
```
`flags = PEERS_FLAG_HAS_JOIN` — `join_sig`/`join_ts` присутствуют (join-запись).
## 4. apply_record — по-блочное сравнение и применение
`member_sync_apply_record(inst, ch_id, from_peer, &rec)` — единая точка записи мембера
(и локальная `put`, и приёмная). Возвращает битовую маску `MS_APPLY_CHANGED | MS_APPLY_STALE`.
1. **Идентичность** (только для приёмных записей): `node_id == sc_derive_node_id_from_pubkey(x25519)`
и `ed25519` совпадает с доверенным из `nodes`. Несовпадение → forgery, блок A отклоняется.
2. **Блок A** — проверка `update_sig` (или `join_sig` для join-only). Локальная запись
(`from_peer == inst->node_id`) доверенная, без верификации. Сравнение `ver`:
- нет локальной записи → changed;
- `join_ts > local_join_ts` → changed (**re-join / смена ключа**);
- `join_ts < local_join_ts` → stale (старая эпоха);
- иначе по `update_ts`.
3. **Блок B** — проверка `adm_tags_sig` канальным ключом, `adm_storage = (storage=="yes")`.
Сравнение `adm_tags.ver` с локальным.
4. **Запись**: changed-блоки пишутся (`member_block_put` / `member_owner_put`), узел обновляется
через `node_update_verified`, пересчитывается классификация `nodeinfo_updated` (из
`node_addresses`, полученных по BGP), затем `merkle_sync_recompute_path`.
5. **Реальная сверка**: если `recompute_path` вернул 0 (данные идентичны) — `changed` сбрасывается
в 0 (no-op), чтобы не рассылать.
На приёме (`_member_apply_items`) каждый `MS_APPLY_CHANGED` генерирует `node_props_changed` и
включается в relay (`merkle_sync_broadcast`); `MS_APPLY_STALE` вызывает `member_sync_send_to`
(отправить нашу свежую запись автору).
## 5. Подписки и коллбэки
- **`member_sync_subscribe_group(inst, group)`** — подписывает на BGP node-события chat-группы
(`topo_group_add_node_cbk`). При событии BGP (`_ms_on_bgp_node`) рассылается `node_props_changed`
с текущим `adm_tags`. REMOVE **не удаляет** мембера.
- **`member_sync_add_props_cbk/remove_props_cbk`** — многоподписочный `node_props_changed_fn`
(изменились `adm_tags` любого узла, включая себя).
- **`member_sync_set_node_updated_cb`** — одиночный `member_sync_node_updated_fn` (online-статус
узла изменился по инициативе удалённого пира, вызывается из uasync-потока).
## 6. Верификация и удаление
Единственный механизм удаления мембера — битая запись:
- `member_sync_verify_local_record` — проверяет привязку `node_id = derive(x25519)`, подписи
`update_sig`/`join_sig`/`adm_tags_sig`. Отсутствие подписи = «не верифицировано» → **не удаляем**.
- `member_sync_verify_and_purge` / `_all` — прогоняют записи канала (всех `peers_%`), битые удаляют
через `topo_node_sqlite_member_del` + `recompute_path`.
- Вызываются при `member_sync_init` и при старте синхронизации канала. Отдельного
`member_sync_del` нет — conn_down ≠ удаление мембера.
## 7. API
| Функция | Назначение |
|---|---|
| `member_sync_init(inst)` | `merkle_sync_init(0x31, &g_member_ops, inst)` + verify/purge + rebuild всех деревьев + подписка на существующие chat-группы |
| `member_sync_destroy(inst)` | Отписка от групп, `merkle_sync_destroy` |
| `member_sync_subscribe_group(inst, group)` | Подписка на BGP-события CHAT-группы |
| `member_sync_start(inst, peer, ch_id, done_cb, arg)` | `verify_and_purge` + `merkle_sync_start` |
| `member_sync_cancel(inst, peer, ch_id)` | `merkle_sync_cancel` |
| `member_sync_put(...)` | Локальная запись мембера (trusted) → `apply_record` |
| `member_sync_apply_record(inst, ch_id, from_peer, &rec)` | Приём/применение рекорда (по-блочно) |
| `member_sync_send_to(inst, ch_id, member_id, target_peer)` | Send-back нашей свежей записи конкретному пиру |
| `member_sync_broadcast_one(inst, ch_id, member_id)` | Сериализовать мембера и разослать всем (relay после локального изменения) |
| `member_sync_set_online(inst, node_id, online)` | Записать `nodes.online`. Возвращает 1 (изменилось) / 0 |
| `member_sync_count(inst, ch_id)` | `COUNT(*)` из `peers_<ch_id>` |
| `member_sync_build_join_msg(...)` / `build_update_msg(...)` | Канонические сообщения для подписи/верификации join/update (112 байт и 64+8+userinfo) |
| `member_sync_get_hash(inst, ch_id, level, prefix64)` | Хеш бакета (для тестов) |
| `member_sync_add/remove_props_cbk`, `set_node_updated_cb` | Подписки на изменения |
## 8. Нюансы
- **Онлайн-статус не участвует в Merkle-дереве**: не хешируется и не версионируется. `set_online`
только пишет в БД и возвращает 1/0; рассылку `MSG_ITEM_UPDATE` выполняет **caller** (chat_sync).
- **`member_sync_put` = локальный `apply_record`** с `from_peer = inst->node_id` — без верификации
подписей (свои данные доверенные).
- **`source`-плейсхолдеры** topo_group пропускаются (`src != 0`), в дереве не участвуют.
- **Re-join / смена ключа** распознаётся по `join_ts` (перекрывает сравнение `update_ts`).
- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`.

10
src/chat/merkle_sync.h

@ -20,12 +20,13 @@ struct UTUN_INSTANCE;
* бакеты и передают только их содержимое. * бакеты и передают только их содержимое.
* *
* merkle_sync не знает, что такое "элемент" — потребитель (member_sync) * merkle_sync не знает, что такое "элемент" — потребитель (member_sync)
* предоставляет три коллбэка, описывающих модель данных: * предоставляет четыре коллбэка, описывающих модель данных:
* *
* update_bucket_hash — перечислить элементы в диапазоне ключей, * update_bucket_hash — перечислить элементы в диапазоне ключей,
* вычислить хеш каждого и подать в SHA256-контекст * вычислить хеш каждого и подать в SHA256-контекст
* get_items — сериализовать элементы бакета в wire-формат * get_items — сериализовать элементы бакета в wire-формат
* apply_items — десериализовать и сохранить полученные элементы * apply_items — десериализовать и сохранить полученные элементы
* apply_update — применить лёгкое обновление (MSG_ITEM_UPDATE)
* *
* Потребитель (chat_sync/chat_core) работает только с member_sync * Потребитель (chat_sync/chat_core) работает только с member_sync
* и не видит merkle_sync напрямую: * и не видит merkle_sync напрямую:
@ -120,7 +121,7 @@ struct merkle_sync_data_ops {
* *len — [in] размер буфера, [out] записанный размер * *len — [in] размер буфера, [out] записанный размер
* *
* Wire-формат для мемберов: * Wire-формат для мемберов:
* [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][userinfo_len:1][userinfo:var][addr_cnt:1][addrs:var]... * [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][userinfo_len:1][userinfo:var][adm_tags_len:1][adm_tags:var][adm_tags_sig:64]...
* *
* Возвращает 0 при успехе, <0 при ошибке, -2 если буфер мал. * Возвращает 0 при успехе, <0 при ошибке, -2 если буфер мал.
*/ */
@ -161,7 +162,7 @@ struct merkle_sync_data_ops {
* *
* peer — node_id пира * peer — node_id пира
* ns — namespace (channel_id) * ns — namespace (channel_id)
* result — MT_OK (0) = успех, MT_ERR_TIMEOUT (-1) = исчерпаны ретраи * result — MT_OK (0) = успех, MT_ERR_TIMEOUT (-1) = зарезервировано (в протоколе не возникает)
* arg — пользовательский контекст * arg — пользовательский контекст
* *
* При отмене через merkle_sync_cancel() коллбэк НЕ вызывается. * При отмене через merkle_sync_cancel() коллбэк НЕ вызывается.
@ -199,7 +200,8 @@ void merkle_sync_destroy(struct UTUN_INSTANCE* inst);
/* /*
* Запустить синхронизацию namespace ns с пиром peer. * Запустить синхронизацию namespace ns с пиром peer.
* Отправляет MSG_HASHES(level=1) и запускает таймаут 10s. * Отправляет MSG_HASHES(level=0). Таймаута нет — сессия завершается при
* совпадении корневого хеша либо удаляется вручную (cancel/destroy).
* *
* Если сессия для (peer, ns) уже существует — перезапускает её * Если сессия для (peer, ns) уже существует — перезапускает её
* с новым коллбэком (старый коллбэк теряется без вызова). * с новым коллбэком (старый коллбэк теряется без вызова).

158
src/chat/merkle_sync_doc.md

@ -0,0 +1,158 @@
# merkle_sync — синхронизация данных на Merkle-деревьях
## 1. Назначение
Универсальный движок синхронизации между двумя узлами. Группирует элементы в префиксное
Merkle-дерево (5 уровней × 32 бакета, SHA256). Два пира обмениваются хешами уровней, находят
различающиеся бакеты и передают **только их содержимое** — вместо полной пересылки всех данных.
merkle_sync **не знает**, что такое «элемент». Потребитель (member_sync) предоставляет набор
коллбэков `merkle_sync_data_ops`, описывающих модель данных (как перечислить элементы бакета
и посчитать хеш, как сериализовать элементы в wire-формат, как применить полученные).
Работает поверх надёжного транспорта (ETCP), поэтому **таймаутов и ретраев в протоколе нет**:
сессия завершается при совпадении корневого хеша, либо удаляется вручную (`cancel`/`destroy`).
## 2. Как пользоваться
Напрямую merkle_sync обычно не используют — его потребляет member_sync. Типовой сценарий
потребителя выглядит так:
```c
// ── разово: инициализация (из member_sync_init) ──
merkle_sync_init(inst, 0x31, &ops, data_ctx); // svc_id — ID ETCP-сервиса
// ── запустил синхронизацию — забыл ──
merkle_sync_start(inst, peer, ns, on_done, my_arg);
// ── коллбэк завершения ──
static void on_done(uint64_t peer, const char* ns, int result, void* arg) {
if (result == MT_OK) /* хеши сошлись, данные идентичны */;
if (result == MT_ERR_TIMEOUT) /* (зарезервировано, в протоколе не возникает) */;
}
// ── изменил данные — дерево само пересчиталось ──
int changed = merkle_sync_recompute_path(inst, ns, key); // 1 = изменилось, 0 = no-op
// ── рассылка дельты остальным пирам (relay) ──
merkle_sync_broadcast(inst, ns, from_peer, data, len);
merkle_sync_push_update(inst, ns, key, type, data, len); // лёгкое обновление (online и т.п.)
// ── отменил — коллбэк не вызовется ──
merkle_sync_cancel(inst, peer, ns);
// ── разово: завершение ──
merkle_sync_destroy(inst);
```
## 3. Модель данных (коллбэки `merkle_sync_data_ops`)
| Коллбэк | Что делает |
|---|---|
| `update_bucket_hash(ctx, ns, level, prefix64, sha_ctx)` | Перечислить элементы в префиксном диапазоне, вычислить хеш каждого и подать в `sha_ctx` через `EVP_DigestUpdate(sha_ctx, item_hash, 32)`. Возвращает число элементов (0 = бакет пуст, -1 = ошибка) |
| `get_items(ctx, ns, level, prefix, pbytes, buf, len)` | Сериализовать элементы бакета в wire-формат. Возвращает 0 / -2 (буфер мал) / <0 (ошибка) |
| `apply_items(ctx, ns, from_peer, data, len)` | Десериализовать и сохранить полученные элементы. Должна пересчитать дерево через `merkle_sync_recompute_path` для каждого изменённого ключа |
| `apply_update(ctx, ns, key, type, data, len)` | Применить лёгкое обновление (не влияет на Merkle-хеш) — MSG_ITEM_UPDATE |
`ctx` — прозрачный контекст, передаваемый в `merkle_sync_init` и возвращаемый первым аргументом
в каждый коллбэк. `ns` (namespace) — строка-имя домена данных, обычно `channel_id`.
## 4. Формат дерева
5-уровневое префиксное дерево над 64-битными ключами. Каждый уровень берёт 5 старших бит ключа:
уровень 1 — биты 59-63, уровень 5 — биты 39-63. Каждый узел разбивается на 32 бакета (5 бит = 32).
```
level 1 [0..31] корень: 32 бакета по 5 бит
level 2 [0..31]...[0..31] каждый — ещё 32 бакета
...
level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс
```
- Хеш бакета = `SHA256(хеш_элемента_1 || ... || хеш_элемента_N)`.
- Бакет считается **терминальным** (leaf) на уровне 5 или если в нём `< 8` элементов.
- Хеши бакетов хранятся в таблице `merkle_tree_hash(namespace, level, prefix64, hash, member_count)`.
Вспомогательные функции префикса: `merkle_sync_level_prefix(key, level)` (выделить `level*5`
старших бит) и `merkle_sync_prefix_bytes(level)` (байт на хранение префикса: уровень 1 → 1 байт,
уровень 5 → 4 байта).
## 5. Wire-протокол
Каждое сообщение: `[svc_id:1][ns_len:1][ns:var][type:1][payload:var]`.
| Тип | #define | Назначение |
|---|---|---|
| 0x01 | MSG_HASHES | Сравнение бакетов: bitmap + хеши, либо данные (is_data) |
| 0x02 | MSG_REQUEST | Запрос содержимого различающихся бакетов |
| 0x03 | MSG_BATCH | Передача данных/подхешей запрошенных бакетов |
| 0x04 | MSG_ITEM_UPDATE | Лёгкое обновление (online-статус), вне дерева |
Форматы payload:
- **MSG_HASHES**: `[level:1][prefix_bytes:1][prefix:pb][is_data:1]` + (`[bitmap:4][hash:32]*` | `[dlen:2][data]`)
- **MSG_REQUEST**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1} × count ]`
- **MSG_BATCH**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1, [data | bitmap+hashes]} × count ]`
- **MSG_ITEM_UPDATE**: `[key:8][type:1][data]`
### Алгоритм обмена
```
A → MSG_HASHES(level=0, bitmap+хеши всех 32 бакетов уровня 1) → B
B сравнивает со своим деревом, находит различающиеся бакеты (differs)
B → MSG_REQUEST(level=2, prefix=X, is_terminal) → A
A → MSG_BATCH(данные бакета или подхеши) → B
B сохраняет, пересчитывает хеши
Рекурсивно для подбакетов, пока хеши не совпадут.
differs==0 и level==0 → корневой хеш совпал → сессия SYNCED.
```
## 6. Сессии и out-очередь (backpressure)
`struct ms_session` — сессия на пару (peer, ns): `sess_state` (`SESS_SYNCING`/`SESS_SYNCED`),
`pending[128]` (ожидающие бакеты: `MS_PEND_WAITING`/`MS_PEND_RESOLVED`, с родительским индексом
и счётчиком подбакетов), `done_cb`/`cb_arg`, `out_q` + `waiter`.
**По-пировая очередь рассылки** `out_q`: рассылка (`broadcast`/`push_update`/`send_to`) не шлёт
немедленно, а кладёт каждый элемент в `out_q` подходящей сессии. Дренаж через `ms_outq_drain_cb`:
`queue_data_get(out_q)` → `etcp_send(conn, entry)`. Перед отправкой проверяется занятость
`conn->send_input_q`; при превышении порога — `queue_waiter_wait(send_input_q, &s->waiter, ...)`
приостанавливает дренаж до освобождения (backpressure, без дропов).
Отправка:
- `_send_msg` — синхронно одному пиру (служебные HASHES/REQUEST/BATCH — по прямому соединению).
- `_broadcast_to_session` — кладёт в `out_q` конкретной сессии (обёртка в MSG_HASHES is_data=1).
## 7. WAL-таймер
Отдельный редкий таймер `MS_WAL_INTERVAL_MS = 60000` (60 с) — `sqlite3_wal_checkpoint_v2(...,
PASSIVE)`. Локальная операция без сети. На Android (`UTUN_HAVE_STANDBY`) при спячке откладывается
через `standby_wait`. Периодической сверки деревьев (bg-таймера) нет — пересчёт событийный.
## 8. API
| Функция | Назначение |
|---|---|
| `merkle_sync_init(inst, svc_id, ops, data_ctx)` | Создать контекст, `_ensure_table`, `etcp_bind(svc_id, _recv_cb)`, WAL-таймер. `inst->msync = ms` |
| `merkle_sync_destroy(inst)` | Отвязать сервис, отменить WAL-таймер, удалить все сессии (коллбэки не вызываются) |
| `merkle_sync_start(inst, peer, ns, done_cb, arg)` | Создать/перезапустить сессию, отправить MSG_HASHES(level=0). Перезапуск теряет старый коллбэк без вызова |
| `merkle_sync_cancel(inst, peer, ns)` | Удалить сессию, `done_cb` не вызывается |
| `merkle_sync_recompute_path(inst, ns, key)` | Пересчитать все 5 уровней для ключа. Возвращает 1 (изменилось) / 0 (no-op) / -1 |
| `merkle_sync_push_update(inst, ns, key, type, data, len)` | MSG_ITEM_UPDATE всем **SYNCED**-сессиям ns |
| `merkle_sync_broadcast(inst, ns, from_peer, data, len)` | Relay: всем сессиям ns, кроме `from_peer` |
| `merkle_sync_send_to(inst, ns, peer, data, len)` | Данные одному пиру (send-back при stale) |
| `merkle_sync_bg_check(inst, ns)` | Проверить один level-5 бакет (сравнение с БД), при расхождении — пересчёт пути вверх. 1/0/-1 |
| `merkle_sync_get_hash(inst, ns, level, prefix64)` | Прочитать хеш бакета (статический буфер, нули если нет) |
| `merkle_sync_level_prefix(key, level)` / `merkle_sync_prefix_bytes(level)` | Арифметика префикса |
## 9. Нюансы
- **svc_id** задаётся при init (member_sync использует `0x31`); единственный приёмник `_recv_cb`.
- **Нет таймаута сессии**: `MT_ERR_TIMEOUT` зарезервирован, но не возникает — сессия живёт до
совпадения корневого хеша или явной отмены.
- **Перезапуск**: `merkle_sync_start` на существующую пару (peer, ns) перезаписывает `done_cb`
(старый коллбэк теряется без вызова).
- **Сессия создаётся и на приёме**: при неожиданном MSG_HASHES от неизвестного пира сессия
создаётся автоматически (symmetrical start).
- **Результат recompute** важен для подавления no-op рассылки: `changed==0` означает, что данные
фактически не изменились.
- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`.

6
src/chat/merkle_tz.md

@ -1,5 +1,11 @@
# ТЗ: событийная синхронизация чата (db_sync + merkle_sync) вместо периодического опроса # ТЗ: событийная синхронизация чата (db_sync + merkle_sync) вместо периодического опроса
> **Статус: реализовано.** Это историческое ТЗ. Актуальное описание модулей — в
> `merkle_sync_doc.md` и `member_sync_doc.md` (рядом с исходниками). Ключевые пункты
> реализованы: событийный peer_check (`db_sync_has_pending_work`/`resume`), per-session
> `out_q` с backpressure и отдельный WAL-таймер в merkle_sync, централизованный путь
> «запись → пересчёт → рассылка» в member_sync (коммиты `d62c370`, `d0784a3`, `a4e73ac`, `4e06db7`).
## 1. Цель ## 1. Цель
После завершения первичной репликации синхронизация должна перейти в **событийный режим**: После завершения первичной репликации синхронизация должна перейти в **событийный режим**:
любое изменение базы немедленно рассылается пирам (push), без периодического таймера-опроса. любое изменение базы немедленно рассылается пирам (push), без периодического таймера-опроса.

4
src/media_delivery/media_delivery.c

@ -868,7 +868,7 @@ static void md_handle_block_req(struct media_delivery_ctx* md, uint64_t from_nod
if (grp && grp->conn_mgr) { if (grp && grp->conn_mgr) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: no direct conn to 0x%016llx — connecting via conn_mgr", DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: no direct conn to 0x%016llx — connecting via conn_mgr",
MD_ID, (unsigned long long)from_node); MD_ID, (unsigned long long)from_node);
conn_mgr_open_invite(grp->instance, grp->group_id, NULL, from_node, NULL, NULL, NULL); conn_mgr_open(grp->instance, grp->group_id, from_node, NULL, NULL, NULL);
} }
} }
@ -1049,7 +1049,7 @@ static void md_super_connect(struct media_delivery_ctx* md, uint64_t peer_node_i
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: connecting to supernode 0x%016llx", DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: connecting to supernode 0x%016llx",
MD_ID, (unsigned long long)peer_node_id); MD_ID, (unsigned long long)peer_node_id);
conn_mgr_open_invite(grp->instance, grp->group_id, NULL, peer_node_id, md_super_conn_cb, md, NULL); conn_mgr_open(grp->instance, grp->group_id, peer_node_id, md_super_conn_cb, md, NULL);
} }
/* ── supernode start/stop ── */ /* ── supernode start/stop ── */

4
src/media_delivery/media_download.c

@ -352,8 +352,8 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: peer[%d] route: grp=%p conn_mgr=%p", DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: peer[%d] route: grp=%p conn_mgr=%p",
MDL_ID, pi, (void*)grp, grp ? (void*)grp->conn_mgr : NULL); MDL_ID, pi, (void*)grp, grp ? (void*)grp->conn_mgr : NULL);
if (grp && grp->conn_mgr) { if (grp && grp->conn_mgr) {
int cm_rc = conn_mgr_open_invite(inst, grp->group_id, NULL, dl->peers[pi].node_id, md_dl_conn_cb, dl, NULL); int cm_rc = conn_mgr_open(inst, grp->group_id, dl->peers[pi].node_id, md_dl_conn_cb, dl, NULL);
if (cm_rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: conn_mgr_open_invite failed rc=%d for 0x%016llx", MDL_ID, cm_rc, (unsigned long long)dl->peers[pi].node_id); if (cm_rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: conn_mgr_open failed rc=%d for 0x%016llx", MDL_ID, cm_rc, (unsigned long long)dl->peers[pi].node_id);
} else } else
for (int bi = 0; bi < dl->peers[pi].num_blocks; bi++) { for (int bi = 0; bi < dl->peers[pi].num_blocks; bi++) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: direct BLOCK_REQ: peer-local bi=%d peer_num_blocks=%d", DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: direct BLOCK_REQ: peer-local bi=%d peer_num_blocks=%d",

2
src/media_delivery/sync.md

@ -34,7 +34,7 @@ last_recv_id INTEGER — максимальный id, подтве
Механизмы обнаружения: Механизмы обнаружения:
- **При старте** (`md_super_start`, стр. 730): суперузел ищет в БД все узлы с `node_type=4` в своей chat-группе и инициирует прямое подключение к каждому через `conn_mgr_open_invite`. - **При старте** (`md_super_start`, стр. 730): суперузел ищет в БД все узлы с `node_type=4` в своей chat-группе и инициирует прямое подключение к каждому через `conn_mgr_open`.
- **При появлении нового узла в BGP** (`md_on_bgp_node`, стр. 790): проверяет `node_type=4` в `peers_<channel>` — если да, вызывает `md_super_connect`. - **При появлении нового узла в BGP** (`md_on_bgp_node`, стр. 790): проверяет `node_type=4` в `peers_<channel>` — если да, вызывает `md_super_connect`.
- **При изменении свойств** (`md_on_props_changed`, стр. 825): если `adm_tags` изменились на `supernode=yes` — подключается; если на `no` — удаляет пира. - **При изменении свойств** (`md_on_props_changed`, стр. 825): если `adm_tags` изменились на `supernode=yes` — подключается; если на `no` — удаляет пира.

32
src/routing_layer/conn_mgr.h

@ -28,14 +28,12 @@ struct TOPO_GROUP;
* существует группа. Внутри: кеш entries (по одной на целевой узел), список * существует группа. Внутри: кеш entries (по одной на целевой узел), список
* handle'ов (по одному на каждый вызов open), фоновые ping/idle/мониторинг. * handle'ов (по одному на каждый вызов open), фоновые ping/idle/мониторинг.
* *
* === Единая точка входа: conn_mgr_open_invite === * === Единая точка входа: conn_mgr_open ===
* *
* ni != NULL — invite-режим: DIRECT + INVITE_INFO_REQ/RESP, группа создаётся. * 3-фазное подключение (DIRECT→REVERSE→INDIRECT).
* Коллбэк: CONN_EVENT_JOIN (успех) или CONN_EVENT_TIMEOUT (ошибка).
* ni == NULL — обычное 3-фазное подключение (DIRECT→REVERSE→INDIRECT).
* Коллбэк: CONN_EVENT_UP (успех) или CONN_EVENT_TIMEOUT (провал). * Коллбэк: CONN_EVENT_UP (успех) или CONN_EVENT_TIMEOUT (провал).
* *
* === 3 фазы (когда ni == NULL) === * === 3 фазы ===
* *
* 1. DIRECT: node_conn_direct на все адреса, NAT-фильтрация, LAN broadcast ping. * 1. DIRECT: node_conn_direct на все адреса, NAT-фильтрация, LAN broadcast ping.
* 2. REVERSE: у нас прямой IP — шлём DIRECT_REQ цели через BGP. * 2. REVERSE: у нас прямой IP — шлём DIRECT_REQ цели через BGP.
@ -51,22 +49,20 @@ struct TOPO_GROUP;
* *
* Несколько handle на один ETCP_CONN. conn_mgr_close удаляет ОДИН handle. * Несколько handle на один ETCP_CONN. conn_mgr_close удаляет ОДИН handle.
* Последний handle: DIRECT/REVERSE — DISCONNECT + NCD CLOSE. * Последний handle: DIRECT/REVERSE — DISCONNECT + NCD CLOSE.
* Invite — только NCD CLOSE.
*/ */
enum conn_mgr_event { enum conn_mgr_event {
CONN_EVENT_UP = 0, /* связь есть. handle жив, можно отправлять данные */ CONN_EVENT_UP = 0, /* связь есть. handle жив, можно отправлять данные */
CONN_EVENT_DOWN = 1, /* временный обрыв. handle жив, само восстановится. НЕ закрывать */ CONN_EVENT_DOWN = 1, /* временный обрыв. handle жив, само восстановится. НЕ закрывать */
CONN_EVENT_TIMEOUT = 2, /* подключение не удалось. handle жив, закройте сами */ CONN_EVENT_TIMEOUT = 2, /* подключение не удалось. handle жив, закройте сами */
CONN_EVENT_JOIN = 3, /* invite принят, группа создана. handle жив */
}; };
/** /**
* Единый коллбэк для всех операций. * Единый коллбэк для всех операций.
* @param h handle (NULL при ошибках до создания handle) * @param h handle (NULL при ошибках до создания handle)
* @param node_id целевой узел (inviter при invite, target при обычном open) * @param node_id целевой узел
* @param group_id группа * @param group_id группа
* @param event UP / DOWN / TIMEOUT / JOIN * @param event UP / DOWN / TIMEOUT
*/ */
typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h, typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h,
uint64_t node_id, uint64_t group_id, uint64_t node_id, uint64_t group_id,
@ -76,28 +72,20 @@ typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h,
struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group); struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group);
void conn_mgr_destroy(struct CONN_MGR* mgr); void conn_mgr_destroy(struct CONN_MGR* mgr);
void conn_mgr_direct_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
/* ═══════ единая точка входа ═══════ */ /* ═══════ единая точка входа ═══════ */
/** /**
* Единственная публичная функция подключения. * Единственная публичная функция подключения — 3-фазное (DIRECT→REVERSE→INDIRECT).
* Используется медиа-доставкой. Пиры/группы подключаются через node_conn_direct,
* invite — через topo_group_invite.
* *
* ni != NULL → invite: DIRECT + INVITE_INFO_REQ/RESP, группа создаётся, * Ошибка/таймаут: CONN_EVENT_TIMEOUT.
* коллбэк CONN_EVENT_JOIN при успехе.
* ni == NULL → обычное 3-фазное подключение, коллбэк CONN_EVENT_UP при успехе.
*
* Ошибка/таймаут в обоих случаях: CONN_EVENT_TIMEOUT.
* node_id и group_id ВСЕГДА валидны в коллбэке (даже при TIMEOUT). * node_id и group_id ВСЕГДА валидны в коллбэке (даже при TIMEOUT).
*
* @param group_id группа (channel_id из invite-ссылки)
* @param ni inviter (caller-owned, копируется). NULL — поиск по node_id.
* @param node_id inviter'а (нужен когда ni == NULL)
*/ */
int conn_mgr_open_invite(struct UTUN_INSTANCE* inst, int conn_mgr_open(struct UTUN_INSTANCE* inst,
uint64_t group_id, uint64_t group_id,
struct TOPO_NODE* ni,
uint64_t node_id, uint64_t node_id,
conn_mgr_cb_t cb, void* cb_arg, conn_mgr_cb_t cb, void* cb_arg,
struct CONN_MGR_HANDLE** out_handle); struct CONN_MGR_HANDLE** out_handle);

470
src/routing_layer/conn_mgr_core.c

@ -21,9 +21,7 @@
#include "topo_node.h" #include "topo_node.h"
#include "topo_node_sqlite.h" #include "topo_node_sqlite.h"
#include "topo_group.h" #include "topo_group.h"
#include "../transport_layer/etcp_connect.h"
#include "route_ping.h" #include "route_ping.h"
#include "../chat/chat_core.h"
#ifdef UTUN_HAVE_STANDBY #ifdef UTUN_HAVE_STANDBY
#include "standby.h" #include "standby.h"
#endif #endif
@ -31,9 +29,6 @@
/* ═══════ forward-декларации ═══════ */ /* ═══════ forward-декларации ═══════ */
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
static void cm_handle_invite_info_req(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len);
static void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len);
static void cm_invite_cancel(struct cm_invite_pending* inv);
/* ═══════ утилиты ═══════ */ /* ═══════ утилиты ═══════ */
@ -46,13 +41,6 @@ static struct CONN_MGR_HANDLE* cm_handle_new(struct CONN_MGR_ENTRY* entry, uint6
return h; return h;
} }
static struct CONN_MGR_HANDLE* cm_invite_handle_new(conn_mgr_cb_t cb, void* cb_arg, uint64_t node_id, uint64_t group_id) {
struct CONN_MGR_HANDLE* h = u_calloc(1, sizeof(*h));
if (!h) return NULL;
h->node_id = node_id; h->group_id = group_id; h->cb = cb; h->cb_arg = cb_arg;
return h;
}
static void cm_deliver_up_cb(void* arg) { static void cm_deliver_up_cb(void* arg) {
struct CONN_MGR_HANDLE* h = (struct CONN_MGR_HANDLE*)arg; struct CONN_MGR_HANDLE* h = (struct CONN_MGR_HANDLE*)arg;
h->deliver_up_token = NULL; h->deliver_up_token = NULL;
@ -301,8 +289,6 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void
} }
} }
/* NCD → invite: UP (отправляем INVITE_INFO_REQ для проверки членства),
* TIMEOUT/DOWN (фейлим invite с CONN_EVENT_TIMEOUT). */
/* ═══════ init / destroy ═══════ */ /* ═══════ init / destroy ═══════ */
struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) { struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) {
@ -324,8 +310,8 @@ struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) {
void conn_mgr_destroy(struct CONN_MGR* mgr) { void conn_mgr_destroy(struct CONN_MGR* mgr) {
if (!mgr) return; if (!mgr) return;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 0 enter mgr=%p entries=%p invite=%p exch=%p rev=%p", DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 0 enter mgr=%p entries=%p exch=%p rev=%p",
mgr, mgr->entries, mgr->invite_list, mgr->exchange_pending, mgr->reverse_pending); mgr, mgr->entries, mgr->exchange_pending, mgr->reverse_pending);
if (mgr->bg_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer); mgr->bg_ping_timer = NULL; } if (mgr->bg_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer); mgr->bg_ping_timer = NULL; }
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 1 bg_ping done"); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 1 bg_ping done");
if (mgr->candidate_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer); mgr->candidate_ping_timer = NULL; } if (mgr->candidate_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer); mgr->candidate_ping_timer = NULL; }
@ -340,9 +326,6 @@ void conn_mgr_destroy(struct CONN_MGR* mgr) {
int clean = 0; int clean = 0;
while (e) { struct ll_entry* next = e->next; struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)e; cm_entry_cleanup(entry); clean++; e = next; } while (e) { struct ll_entry* next = e->next; struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)e; cm_entry_cleanup(entry); clean++; e = next; }
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries cleaned (%d)", clean); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries cleaned (%d)", clean);
int inv_clean = 0;
while (mgr->invite_list) { cm_invite_cancel(mgr->invite_list); inv_clean++; }
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 4 invite_list cleaned (%d)", inv_clean);
while (mgr->exchange_pending) { while (mgr->exchange_pending) {
struct cm_exchange_pending* ep = mgr->exchange_pending; mgr->exchange_pending = ep->next; struct cm_exchange_pending* ep = mgr->exchange_pending; mgr->exchange_pending = ep->next;
if (ep->timer) uasync_cancel_timeout(mgr->instance->ua, ep->timer); if (ep->timer) uasync_cancel_timeout(mgr->instance->ua, ep->timer);
@ -358,7 +341,7 @@ void conn_mgr_destroy(struct CONN_MGR* mgr) {
/* ═══════ публичное API ═══════ */ /* ═══════ публичное API ═══════ */
int conn_mgr_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms, int cm_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms,
conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle) { conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle) {
if (out_handle) *out_handle = NULL; if (out_handle) *out_handle = NULL;
if (!mgr) { if (cb) cb(NULL, node_id, 0, CONN_EVENT_TIMEOUT, cb_arg); return -1; } if (!mgr) { if (cb) cb(NULL, node_id, 0, CONN_EVENT_TIMEOUT, cb_arg); return -1; }
@ -749,394 +732,14 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
} }
} }
void conn_mgr_direct_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) { int conn_mgr_open(struct UTUN_INSTANCE* inst,
if (!entry || !entry->dgram || entry->len < 3) return;
uint8_t* d = entry->dgram + 1; size_t len = entry->len - 1; uint8_t sub = d[1];
struct CONN_MGR* mgr = conn->instance->conn_mgr;
switch (sub) {
case CM_SUBCMD_INVITE_INFO_REQ: if (mgr && len >= sizeof(struct CM_INVITE_REQ)) cm_handle_invite_info_req(mgr, conn, d, len); break;
case CM_SUBCMD_INVITE_INFO_RESP: if (mgr) cm_handle_invite_info_resp(mgr, conn, d, len); break;
}
}
/* ═══════ invite обработчики ═══════ */
/* Формирует полный INVITE_INFO_RESP: если сервер участник канала — возвращает
* is_member=1 + ВСЕ поля канала (name, owner, ключи, ch_sig, join/update_sig).
* Если не участник — is_member=0. Попутно создаёт инфраструктуру группы через
* chat_core_ensure_channel_ready, чтобы группа была готова к маршрутизации сразу. */
void cm_handle_invite_info_req(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len) {
if (len < sizeof(struct CM_INVITE_REQ)) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_REQ too short (%zu)", len); return; }
struct CM_INVITE_REQ* req = (struct CM_INVITE_REQ*)data;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ from=%016llx group=%016llx",
(unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id);
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)req->group_id);
/* данные узла-ответчика для ed_pubkey и node_name */
struct TOPO_GROUP* grp = topo_groups_find(mgr->instance->topo_groups, req->group_id);
struct TOPO_GROUP* sg = grp ? grp : mgr->group;
struct TOPO_NODE* ln = sg->local_node ? topo_node_registry_find(sg->instance->topo_groups, sg->local_node->node_id) : NULL;
const char* nm = ln ? ln->node_name : NULL; size_t nl = nm ? strlen(nm) : 0; if (nl > 255) nl = 255;
/* ── ищем канал в SQLite ── */
sqlite3* db = mgr->instance->topo_sqlite_db;
char ch_name[128] = ""; uint64_t owner = 0;
uint8_t ch_x25519[32] = {0}, ch_ed_pub[32] = {0}, ch_sig[64] = {0};
int have_channel = db && topo_node_sqlite_channel_get(db, ch_id, ch_name, (int)sizeof(ch_name),
&owner, ch_x25519, ch_ed_pub, ch_sig) == 0;
if (!have_channel) {
size_t rs = CM_INVITE_RESP_HDR_SIZE + nl;
struct CM_INVITE_RESP* resp = u_calloc(1, rs); if (!resp) return;
resp->cmd = ETCP_RT_ID_CONN_MGR; resp->subcmd = CM_SUBCMD_INVITE_INFO_RESP;
resp->group_id = req->group_id; resp->is_member = 0;
memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE);
resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl);
struct ll_entry* qe = queue_entry_new(0);
if (qe) { size_t new_len = rs + 1; uint8_t* buf = u_malloc(new_len);
if (buf) { buf[0]=ETCP_RT_ID_CONN_MGR; buf[1]=ETCP_RT_ID_CONN_MGR; buf[2]=CM_SUBCMD_INVITE_INFO_RESP;
memcpy(buf + 3, (uint8_t*)resp + 2, rs - 2); qe->dgram = buf; qe->len = (uint16_t)new_len; etcp_send(conn, qe); }
else { queue_entry_free(qe); }
}
u_free(resp);
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: group=%016llx NOT in DB — responding is_member=NO to 0x%016llx",
(unsigned long long)req->group_id, (unsigned long long)conn->peer_node_id);
return;
}
/* ── участник: генерируем join_sig / update_sig ── */
uint64_t myid = mgr->instance->node_id;
uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0;
{
if (db && topo_node_sqlite_member_get_join(db, ch_id, myid, my_join_sig, &my_join_ts) != 0) {
uint8_t join_msg[256]; size_t mlen = 0;
memcpy(join_msg + mlen, ch_x25519, 32); mlen += 32;
memcpy(join_msg + mlen, ch_ed_pub, 32); mlen += 32;
memcpy(join_msg + mlen, &myid, 8); mlen += 8;
memcpy(join_msg + mlen, mgr->instance->my_keys.public_key, 32); mlen += 32;
my_join_ts = (uint64_t)ntp_time_get_seconds(mgr->instance);
memcpy(join_msg + mlen, &my_join_ts, 8); mlen += 8;
if (sc_ed25519_sign(mgr->instance->my_ed25519_privkey, join_msg, mlen, my_join_sig) != SC_OK)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: join_sig sign FAILED myid=%016llx ch=%s", (unsigned long long)myid, ch_id);
else
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig NEW myid=%016llx ts=%llu ch=%s",
(unsigned long long)myid, (unsigned long long)my_join_ts, ch_id);
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig REUSED myid=%016llx ts=%llu ch=%s",
(unsigned long long)myid, (unsigned long long)my_join_ts, ch_id);
}
}
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts = 0;
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}",
mgr->instance->name[0] ? mgr->instance->name : "");
uint8_t umsg[256]; size_t ulen = 0;
memcpy(umsg + ulen, my_join_sig, 64); ulen += 64;
my_update_ts = (uint64_t)ntp_time_get_seconds(mgr->instance);
memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8;
size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1;
if (sc_ed25519_sign(mgr->instance->my_ed25519_privkey, umsg, ulen, my_update_sig) != SC_OK)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: update_sig sign FAILED myid=%016llx", (unsigned long long)myid);
}
/* ── создаём инфраструктуру группы СРАЗУ (topo_group, db_sync, msg-таблицу) ── */
chat_core_ensure_channel_ready(ch_id);
/* ── сериализуем ответ ── */
size_t ch_nl = strlen(ch_name); if (ch_nl > 255) ch_nl = 255;
uint8_t inv_flags = (my_join_sig[0] || my_join_ts) ? PEERS_FLAG_HAS_JOIN : 0;
char userinfo[256]; size_t userinfo_len = (size_t)snprintf(userinfo, sizeof(userinfo),
"{\"name\":\"%s\"}", mgr->instance->name[0] ? mgr->instance->name : "");
size_t ch_info_len = 1 + ch_nl + 8 + 32 + 32 + 64 + 1
+ (inv_flags & PEERS_FLAG_HAS_JOIN ? 72 : 0)
+ 64 + 8 + 1 + userinfo_len;
size_t rs = CM_INVITE_RESP_HDR_SIZE + nl + ch_info_len;
uint8_t* rb = u_malloc(rs); if (!rb) return;
memset(rb, 0, rs);
{ struct CM_INVITE_RESP* resp = (struct CM_INVITE_RESP*)rb;
resp->cmd = ETCP_RT_ID_CONN_MGR; resp->subcmd = CM_SUBCMD_INVITE_INFO_RESP;
resp->group_id = req->group_id; resp->is_member = 1;
memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE);
resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); }
uint8_t* ch = rb + CM_INVITE_RESP_HDR_SIZE + nl;
*ch++ = (uint8_t)ch_nl; memcpy(ch, ch_name, ch_nl); ch += ch_nl;
memcpy(ch, &owner, 8); ch += 8;
memcpy(ch, ch_x25519, 32); ch += 32;
memcpy(ch, ch_ed_pub, 32); ch += 32;
memcpy(ch, ch_sig, 64); ch += 64;
*ch++ = inv_flags;
if (inv_flags & PEERS_FLAG_HAS_JOIN) { memcpy(ch, my_join_sig, 64); ch += 64; memcpy(ch, &my_join_ts, 8); ch += 8; }
memcpy(ch, my_update_sig, 64); ch += 64;
memcpy(ch, &my_update_ts, 8); ch += 8;
*ch++ = (uint8_t)userinfo_len; memcpy(ch, userinfo, userinfo_len);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"invite: membership CHECK OK from=0x%016llx group=%016llx ch_name=%.*s owner=%016llx "
"ch_x25519=%016llx... ch_ed=%016llx... inv_flags=%u join_ts=%llu upd_ts=%llu my_name=%s → is_member=YES full_ch_info (%zu+%zu=%zuB)",
(unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id,
(int)ch_nl, ch_name, (unsigned long long)owner,
*(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed_pub,
(unsigned)inv_flags, (unsigned long long)my_join_ts, (unsigned long long)my_update_ts,
mgr->instance->name[0] ? mgr->instance->name : "?",
CM_INVITE_RESP_HDR_SIZE + nl, ch_info_len, rs);
{ struct ll_entry* qe = queue_entry_new(0);
if (qe) { size_t dgram_len = rs + 1; uint8_t* dgram = u_malloc(dgram_len);
if (dgram) { dgram[0]=ETCP_RT_ID_CONN_MGR; dgram[1]=ETCP_RT_ID_CONN_MGR; dgram[2]=CM_SUBCMD_INVITE_INFO_RESP;
memcpy(dgram + 3, rb + 2, rs - 2); qe->dgram = dgram; qe->len = (uint16_t)dgram_len; etcp_send(conn, qe); }
else { queue_entry_free(qe); }
}
}
u_free(rb);
}
void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len) {
if (len < CM_INVITE_RESP_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP too short (%zu < %zu)", len, CM_INVITE_RESP_HDR_SIZE); return; }
struct CM_INVITE_RESP* resp = (struct CM_INVITE_RESP*)data;
uint8_t nnl = resp->node_name_len;
if (len < CM_INVITE_RESP_HDR_SIZE + (size_t)nnl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP truncated (need %zu, got %zu)", CM_INVITE_RESP_HDR_SIZE + (size_t)nnl, len); return; }
/* ── ищем ожидающий invite ── */
struct cm_invite_pending* inv = NULL;
{ struct TOPO_GROUP* grp = topo_groups_find(mgr->instance->topo_groups, resp->group_id);
if (grp && grp->conn_mgr)
for (struct cm_invite_pending* p = grp->conn_mgr->invite_list; p; p = p->next)
if (p->state == CM_INVITE_WAIT_INFO && (!conn->peer_node_id || p->node_id == conn->peer_node_id)) { inv = p; break; }
}
if (!inv) { for (struct cm_invite_pending* p = mgr->invite_list; p; p = p->next) if (p->state == CM_INVITE_WAIT_INFO) { inv = p; break; } }
if (!inv) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP — no pending invite in WAIT_INFO state"); return; }
if (!resp->is_member) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: membership DENIED by 0x%016llx — not in channel", (unsigned long long)inv->node_id);
cm_invite_fail(inv); return;
}
/* ── сохраняем Ed25519 pubkey ответчика для NODEINFO-верификации ── */
if (inv->temp_nq) {
struct TOPO_NODE* ni2 = topo_node_registry_find(inv->mgr->instance->topo_groups, inv->temp_nq->node_id);
if (ni2) memcpy(ni2->ed25519_public_key, resp->ed25519_pubkey, SC_PUBKEY_SIZE);
}
/* ── парсим channel_info (новое) ── */
const uint8_t* ch = data + CM_INVITE_RESP_HDR_SIZE + nnl;
size_t ch_remain = len - (CM_INVITE_RESP_HDR_SIZE + nnl);
if (ch_remain < CM_INVITE_RESP_CH_MIN_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP channel info too short (%zu < %zu)", ch_remain, CM_INVITE_RESP_CH_MIN_SIZE); cm_invite_fail(inv); return; }
uint8_t c_nl = *ch++; ch_remain--;
if (ch_remain < (size_t)c_nl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_name truncated"); cm_invite_fail(inv); return; }
char ch_name[128]; size_t cn = c_nl > 127 ? 127 : c_nl; memcpy(ch_name, ch, cn); ch_name[cn] = '\0'; ch += c_nl; ch_remain -= c_nl;
uint64_t ch_owner; memcpy(&ch_owner, ch, 8); ch += 8; ch_remain -= 8;
const uint8_t* ch_x25519 = ch; ch += 32; ch_remain -= 32;
const uint8_t* ch_ed_pub = ch; ch += 32; ch_remain -= 32;
const uint8_t* ch_sig = ch; ch += 64; ch_remain -= 64;
if (ch_remain < 1) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at inv_flags"); cm_invite_fail(inv); return; }
uint8_t inv_flags = *ch++; ch_remain--;
const uint8_t* join_sig = NULL; uint64_t join_ts = 0;
if (inv_flags & PEERS_FLAG_HAS_JOIN) {
if (ch_remain < 72) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at join_sig"); cm_invite_fail(inv); return; }
join_sig = ch; ch += 64; memcpy(&join_ts, ch, 8); ch += 8; ch_remain -= 72;
}
if (ch_remain < 73) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at update"); cm_invite_fail(inv); return; }
const uint8_t* upd_sig = ch; ch += 64; uint64_t upd_ts; memcpy(&upd_ts, ch, 8); ch += 8; ch_remain -= 72;
uint8_t ui_len = *ch++; ch_remain--;
char ch_userinfo[256] = "";
if (ui_len > 0 && ch_remain >= ui_len) { size_t ul = ui_len > 255 ? 255 : ui_len; memcpy(ch_userinfo, ch, ul); ch_userinfo[ul] = '\0'; }
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)resp->group_id);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"invite: INFO_RESP from=0x%016llx group=%016llx node_name=%.*s "
"ch_name=%s ch_owner=%016llx ch_x25519=%016llx... ch_ed=%016llx... "
"inv_flags=%u join_ts=%llu upd_ts=%llu userinfo=%s",
(unsigned long long)conn->peer_node_id, (unsigned long long)resp->group_id,
(int)nnl, nnl ? (const char*)resp->node_name : "?",
ch_name, (unsigned long long)ch_owner,
*(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed_pub,
(unsigned)inv_flags, (unsigned long long)join_ts, (unsigned long long)upd_ts,
ch_userinfo[0] ? ch_userinfo : "?");
/* ── верифицируем ch_sig ── */
{
uint8_t vmsg[1024]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
vlen += (size_t)snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_name) + 1;
memcpy(vmsg + vlen, &ch_owner, 8); vlen += 8;
memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed_pub, 32); vlen += 32;
if (sc_ed25519_verify(ch_ed_pub, vmsg, vlen, ch_sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY FAIL — rejecting group=%016llx from=0x%016llx",
(unsigned long long)resp->group_id, (unsigned long long)conn->peer_node_id);
cm_invite_fail(inv); return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY OK");
}
/* ── сохраняем канал в БД и создаём ПОЛНОЦЕННУЮ группу (topo_group + db_sync + msg-таблицу) ── */
{
sqlite3* ldb = inv->mgr->instance->topo_sqlite_db;
if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner,
ch_x25519, NULL, ch_ed_pub, NULL, ch_sig);
}
chat_core_ensure_channel_ready(ch_id);
/* ── верифицируем update_sig приглашающего ── */
{
const uint8_t* inv_ed = conn->peer_ed25519_pubkey;
uint8_t ivmsg[256]; size_t ilen = 0;
if (join_sig && (inv_flags & PEERS_FLAG_HAS_JOIN)) { memcpy(ivmsg + ilen, join_sig, 64); ilen += 64; }
else { memset(ivmsg + ilen, 0, 64); ilen += 64; }
memcpy(ivmsg + ilen, &upd_ts, 8); ilen += 8;
{ const char* nm2 = ch_userinfo[0] ? ch_userinfo : "";
size_t nl2 = strlen(nm2); memcpy(ivmsg + ilen, nm2, nl2 + 1); ilen += nl2 + 1; }
if (sc_ed25519_verify(inv_ed, ivmsg, ilen, upd_sig) != SC_OK) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY FAIL node=0x%016llx", (unsigned long long)conn->peer_node_id);
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY OK");
}
}
if (inv->overall_timer) { uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" ch=%s — full channel ready",
(unsigned long long)inv->node_id, (int)nnl, nnl ? (const char*)resp->node_name : "?", ch_id);
if (inv->cb) inv->cb(inv->handle, inv->node_id, inv->mgr->group->group_id, CONN_EVENT_JOIN, inv->cb_arg);
if (inv->invite_conn) {
/* переиспользуем существующий conn через ncd: он добавляет недостающие линки
* (ncd_create_links с дедупом по etcp_link_find_by_addr) */
struct TOPO_NODE* ni = topo_node_registry_find(inv->mgr->instance->topo_groups, inv->node_id);
struct CONN_MGR_ENTRY* entry = cm_ensure_entry(inv->mgr, inv->node_id);
if (ni && entry) {
entry->state = CONN_MGR_STATE_CONNECTED;
entry->conn_type = CONN_TYPE_DIRECT;
if (inv->handle && !inv->handle->entry) {
inv->handle->entry = entry;
inv->handle->next = entry->handles;
entry->handles = inv->handle;
}
node_conn_direct_open_node(inv->mgr->instance, inv->node_id,
cm_ncd_callback, entry, &entry->ncd_handle, ni, NULL);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] cannot promote conn: ni=%p entry=%p node=0x%016llx",
(void*)ni, (void*)entry, (unsigned long long)inv->node_id);
}
inv->invite_conn = NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] conn transferred to ncd, calling cancel");
inv->temp_nq = NULL;
cm_invite_cancel(inv);
}
void cm_invite_overall_timeout(void* arg) {
struct cm_invite_pending* inv = (struct cm_invite_pending*)arg;
if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: overall timer IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; }
inv->overall_timer = NULL;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: overall TIMEOUT to 0x%016llx — no response within %ds",
(unsigned long long)inv->node_id, CM_INVITE_DEFAULT_TIMEOUT_MS/1000);
cm_invite_fail(inv);
}
/* Cleanup invite resources WITHOUT calling external callbacks.
* Safe to call during teardown/destroy. */
static void cm_invite_free_deferred(void* arg) {
struct cm_invite_pending* inv = (struct cm_invite_pending*)arg;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_FREE] freeing inv=%p node=0x%016llx", inv, (unsigned long long)inv->node_id);
u_free(inv);
}
static void cm_invite_cancel(struct cm_invite_pending* inv) {
if (!inv) return;
inv->cancelled = 1;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv=%p node=0x%016llx mgr=%p conn=%p",
inv, (unsigned long long)inv->node_id, inv->mgr, (void*)inv->invite_conn);
if (!inv->mgr) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr is NULL!"); goto inv_cancel_free; }
if (!inv->mgr->instance) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr->instance is NULL!"); goto inv_cancel_free; }
if (inv->overall_timer) {
uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] overall_timer cancelled");
}
/* гасим висящий etcp_connect (если ещё идёт) — иначе его settle-таймер дёрнет
* cm_invite_etcp_cb с освобождённым inv (use-after-free) */
etcp_connect_cancel_by_node(inv->mgr->instance, inv->node_id);
if (inv->temp_nq) {
queue_remove_data(inv->mgr->group->nodes, &inv->temp_nq->ll);
topo_nodeq_free_group_fields(inv->mgr->instance->topo_groups, inv->temp_nq);
queue_entry_free(&inv->temp_nq->ll); inv->temp_nq = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] temp_nq removed");
}
{ struct cm_invite_pending** pp = &inv->mgr->invite_list;
while (*pp) { if (*pp == inv) { *pp = inv->next; break; } pp = &(*pp)->next; }
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] unlinked from invite_list");
inv_cancel_free:
if (inv->mgr && inv->mgr->instance && inv->mgr->instance->ua)
{ DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] posting deferred free inv=%p", inv);
uasync_post(inv->mgr->instance->ua, cm_invite_free_deferred, inv); }
else
{ DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] no ua — immediate free inv=%p", inv);
u_free(inv); }
}
/* Cancel invite AND notify caller. For normal operation (timeout, error, invite failure).
* Not safe during teardown — use cm_invite_cancel for destroy paths. */
void cm_invite_fail(struct cm_invite_pending* inv) {
if (!inv) return;
if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "cm_invite_fail: IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "cm_invite_fail: node=0x%016llx state=%d conn=%p",
(unsigned long long)inv->node_id, (int)inv->state,
(void*)inv->invite_conn);
conn_mgr_cb_t cb = inv->cb;
struct CONN_MGR_HANDLE* handle = inv->handle;
uint64_t node_id = inv->node_id;
uint64_t group_id = inv->mgr->group->group_id;
void* cb_arg = inv->cb_arg;
cm_invite_cancel(inv);
if (cb) cb(handle, node_id, group_id, CONN_EVENT_TIMEOUT, cb_arg);
if (handle) u_free(handle);
}
void cm_invite_cleanup(struct cm_invite_pending* inv) { if (inv) u_free(inv); }
/* ── etcp_connect callback for invite ── */
static void cm_invite_etcp_cb(void* arg, struct ETCP_CONN* conn, int event) {
struct cm_invite_pending* inv = (struct cm_invite_pending*)arg;
if (!inv || inv->cancelled) return;
if (event != ETCP_CONNECT_LATE && event != ETCP_CONNECT_EARLY) return;
if (inv->state != CM_INVITE_CONNECTING) return;
if (!conn) { cm_invite_fail(inv); return; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: etcp_connect %s for 0x%016llx — sending membership check",
event == ETCP_CONNECT_LATE ? "LATE" : "EARLY", (unsigned long long)inv->node_id);
inv->invite_conn = conn;
inv->state = CM_INVITE_WAIT_INFO;
{ uint8_t buf[11];
buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ;
memcpy(buf + 3, &inv->mgr->group->group_id, 8);
struct ll_entry* qe = queue_entry_new(0);
if (!qe) { cm_invite_fail(inv); return; }
qe->dgram = u_malloc(sizeof(buf)); if (!qe->dgram) { queue_entry_free(qe); cm_invite_fail(inv); return; }
memcpy(qe->dgram, buf, sizeof(buf)); qe->len = sizeof(buf); etcp_send(conn, qe);
}
}
int conn_mgr_open_invite(struct UTUN_INSTANCE* inst,
uint64_t group_id, uint64_t group_id,
struct TOPO_NODE* ni,
uint64_t node_id, uint64_t node_id,
conn_mgr_cb_t cb, void* cb_arg, conn_mgr_cb_t cb, void* cb_arg,
struct CONN_MGR_HANDLE** out_handle) { struct CONN_MGR_HANDLE** out_handle) {
if (out_handle) *out_handle = NULL; if (out_handle) *out_handle = NULL;
if (!inst || !inst->topo_groups || !cb) return -1; if (!inst || !inst->topo_groups || !cb) return -1;
/* 1. Найти или создать группу → mgr */
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
if (!group) { if (!group) {
char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id);
@ -1146,68 +749,5 @@ int conn_mgr_open_invite(struct UTUN_INSTANCE* inst,
struct CONN_MGR* mgr = group->conn_mgr; struct CONN_MGR* mgr = group->conn_mgr;
if (!mgr) return -1; if (!mgr) return -1;
/* 2. ni == NULL → обычное 3-фазное подключение */ return cm_open(mgr, node_id, 0, cb, cb_arg, out_handle);
if (!ni) return conn_mgr_open(mgr, node_id, 0, cb, cb_arg, out_handle);
/* 3. ni != NULL → invite-режим */
uint64_t nid = ni->node_id;
if (!nid || nid == inst->node_id) return -1;
if (!ni->v4_addrs && !ni->v6_addrs) return -1;
if (!ni->public_key[0]) return -1;
/* 4. Копировать ni в group_ni */
struct TOPO_GROUPS* groups = inst->topo_groups;
struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1;
gni->node_id=nid; gni->ver=1; gni->group_id=group_id; memcpy(gni->public_key,ni->public_key,SC_PUBKEY_SIZE);
for (const struct TOPO_ADDR4* src = ni->v4_addrs; src; src = src->next) {
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); if (!a4) continue;
memset(a4,0,sizeof(*a4)); memcpy(a4->addr,src->addr,4); a4->port=src->port;
a4->type=TOPO_ADDR_NAT; a4->protocol=src->protocol; a4->next=gni->v4_addrs; gni->v4_addrs=a4;
}
for (const struct TOPO_ADDR6* src6 = ni->v6_addrs; src6; src6 = src6->next) {
struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); if (!a6) continue;
memset(a6,0,sizeof(*a6)); memcpy(a6->addr,src6->addr,16); a6->port=src6->port;
a6->type=TOPO_ADDR_NAT; a6->protocol=src6->protocol; a6->next=gni->v6_addrs; gni->v6_addrs=a6;
}
if (!gni->v4_addrs && !gni->v6_addrs) { u_free(gni); return -1; }
gni = topo_node_registry_store(groups, gni); if (!gni) return -1;
/* 5. TOPO_GROUP_NODE + invite_pending */
struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE));
if (!qe) { topo_node_registry_unref(groups,gni->node_id); return -1; }
struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)qe;
memset((uint8_t*)nq+sizeof(struct ll_entry),0,sizeof(*nq)-sizeof(struct ll_entry)); nq->node_id=nid;
struct cm_invite_pending* inv = u_calloc(1, sizeof(*inv));
if (!inv) { queue_entry_free(qe); topo_node_registry_unref(groups,gni->node_id); return -1; }
inv->mgr=mgr; inv->node_id=nid; inv->state=CM_INVITE_CONNECTING;
inv->temp_nq=nq; inv->next=mgr->invite_list; mgr->invite_list=inv;
inv->cb=cb; inv->cb_arg=cb_arg;
struct CONN_MGR_HANDLE* h = cm_invite_handle_new(NULL, NULL, nid, group_id);
if (!h) { cm_invite_fail(inv); return -1; } inv->handle=h; if (out_handle) *out_handle=h;
queue_data_put_with_index(group->nodes, &nq->ll);
/* 6. лог адресов ДО старта — виден даже при неудаче etcp_connect */
{
char addrs[512] = ""; int aoff = 0;
for (const struct TOPO_ADDR4* a = gni->v4_addrs; a; a = a->next)
aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %d.%d.%d.%d:%d(%s)",
a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port,
a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : "?");
for (const struct TOPO_ADDR6* a6 = gni->v6_addrs; a6; a6 = a6->next)
aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %s:%d(%s)",
ip_to_str(a6->addr, AF_INET6).str, (int)a6->port,
a6->protocol == TOPO_PROTO_UDP ? "UDP" : a6->protocol == TOPO_PROTO_TCP ? "TCP" : "?");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%ds",
(unsigned long long)nid, addrs, CM_INVITE_DEFAULT_TIMEOUT_MS/1000);
}
/* 7. etcp_connect — запускает UDP links + TCP STCP link через единый API */
if (etcp_connect(inst, nq, cm_invite_etcp_cb, inv, ETCP_CONNECT_EARLY | ETCP_CONNECT_LATE) != 0) {
cm_invite_fail(inv); if (out_handle) *out_handle=NULL; return -1;
}
inv->overall_timer = uasync_set_timeout(inst->ua, CM_INVITE_DEFAULT_TIMEOUT_MS*10, inv, cm_invite_overall_timeout, "conn_mgr_invite");
return 0;
} }

57
src/routing_layer/conn_mgr_priv.h

@ -31,7 +31,6 @@ struct stcp_link;
#define CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS 2000 #define CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS 2000
#define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 15000 #define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 15000
#define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000 #define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000
#define CM_INVITE_DEFAULT_TIMEOUT_MS 2000
#define CACHE_MS 20000 /* время жизни RTT-замера (через cm_is_rtt_fresh) */ #define CACHE_MS 20000 /* время жизни RTT-замера (через cm_is_rtt_fresh) */
#define CONN_MGR_MAX_INTERMEDIARIES 3 #define CONN_MGR_MAX_INTERMEDIARIES 3
@ -63,8 +62,6 @@ enum {
CM_SUBCMD_BRIDGE_RESPONSE = 0x11, CM_SUBCMD_BRIDGE_RESPONSE = 0x11,
CM_SUBCMD_BRIDGE_CLOSE = 0x12, CM_SUBCMD_BRIDGE_CLOSE = 0x12,
CM_SUBCMD_BRIDGE_CLOSED = 0x13, CM_SUBCMD_BRIDGE_CLOSED = 0x13,
CM_SUBCMD_INVITE_INFO_REQ = 0x0A,
CM_SUBCMD_INVITE_INFO_RESP = 0x0B,
}; };
struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed)); struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed));
@ -78,49 +75,12 @@ struct CM_INTERM_SEL { uint8_t cmd, subcmd; uint32_t request_id; uint8_t coun
struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; }; struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; };
#pragma pack(pop) #pragma pack(pop)
#pragma pack(push, 1)
struct CM_INVITE_REQ { uint8_t cmd, subcmd; uint64_t group_id; };
#pragma pack(pop)
/*
* INVITE_INFO_RESP wire format (devel — без обратной совместимости):
*
* [0..1] cmd, subcmd
* [2..9] group_id
* [10] is_member (1=участник, 0=не в канале)
* [11..13] reserved
* [14..45] ed25519_pubkey (Ed25519 pubkey узла-ответчика, для NODEINFO-верификации)
* [46] node_name_len
* [47..N] node_name (node_name_len байт)
*
* Если is_member == 1 — далее полная информация о канале:
* [N+1] ch_name_len
* [N+2..] ch_name (ch_name_len байт)
* +8 owner (node_id создателя)
* +32 ch_x25519 (X25519 pubkey канала)
* +32 ch_ed_pub (Ed25519 pubkey канала)
* +64 ch_sig (Ed25519-подпись канала)
* +1 inv_flags (PEERS_FLAG_HAS_JOIN)
* [if PEERS_FLAG_HAS_JOIN: +64 join_sig + 8 join_ts]
* +64 update_sig
* +8 update_ts
* +1 userinfo_len
* +var userinfo (userinfo_len байт, JSON с именем узла)
*/
#pragma pack(push, 1)
struct CM_INVITE_RESP { uint8_t cmd, subcmd; uint64_t group_id; uint8_t is_member; uint8_t reserved[3]; uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; uint8_t node_name_len; uint8_t node_name[0]; };
#pragma pack(pop)
#define CM_INVITE_RESP_HDR_SIZE (sizeof(struct CM_INVITE_RESP)) /* 47 байт, без node_name */
#define CM_INVITE_RESP_CH_MIN_SIZE (1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1) /* 211 байт, без учёта join и имён */
#define CM_DIRECT_REQ_H_SIZE (sizeof(struct CM_DIRECT_REQ)) #define CM_DIRECT_REQ_H_SIZE (sizeof(struct CM_DIRECT_REQ))
#define CM_DIRECT_RESP_HDR_SIZE (sizeof(struct CM_DIRECT_RESP)) #define CM_DIRECT_RESP_HDR_SIZE (sizeof(struct CM_DIRECT_RESP))
#define CM_EXCHANGE_REQ_SIZE (sizeof(struct CM_EXCHANGE_REQ)) #define CM_EXCHANGE_REQ_SIZE (sizeof(struct CM_EXCHANGE_REQ))
#define CM_EXCHANGE_RESP_SIZE (sizeof(struct CM_EXCHANGE_RESP)) #define CM_EXCHANGE_RESP_SIZE (sizeof(struct CM_EXCHANGE_RESP))
#define CM_INTERM_SEL_SIZE (sizeof(struct CM_INTERM_SEL)) #define CM_INTERM_SEL_SIZE (sizeof(struct CM_INTERM_SEL))
#define CM_DISCONNECT_SIZE (sizeof(struct CM_DISCONNECT)) #define CM_DISCONNECT_SIZE (sizeof(struct CM_DISCONNECT))
#define CM_INVITE_REQ_SIZE (sizeof(struct CM_INVITE_REQ))
/* ═══════ внутренние структуры ═══════ */ /* ═══════ внутренние структуры ═══════ */
@ -132,17 +92,6 @@ struct cm_exchange_pending {
struct CM_EXCHANGE_RESP cached; uint8_t received, probed; struct CM_EXCHANGE_RESP cached; uint8_t received, probed;
}; };
enum CM_INVITE_STATE { CM_INVITE_CONNECTING = 0, CM_INVITE_WAIT_INFO = 1, };
struct cm_invite_pending {
struct cm_invite_pending* next; struct CONN_MGR* mgr;
uint64_t node_id; uint8_t state;
struct ETCP_CONN* invite_conn;
uint8_t cancelled:1; void* overall_timer;
struct CONN_MGR_HANDLE* handle; struct TOPO_GROUP_NODE* temp_nq;
conn_mgr_cb_t cb; void* cb_arg;
};
struct CONN_MGR_HANDLE { struct CONN_MGR_HANDLE {
uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg; uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg;
struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next; struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next;
@ -171,14 +120,13 @@ struct CONN_MGR {
uint32_t next_request_id, direct_timeout_ms; uint8_t initialized; uint32_t next_request_id, direct_timeout_ms; uint8_t initialized;
struct cm_reverse_pending* reverse_pending; struct cm_reverse_pending* reverse_pending;
struct cm_exchange_pending* exchange_pending; struct cm_exchange_pending* exchange_pending;
struct cm_invite_pending* invite_list;
uint8_t _unused[0]; uint8_t _unused[0];
}; };
/* ═══════ декларации внутренних функций ═══════ */ /* ═══════ декларации внутренних функций ═══════ */
/* core — публичные (в priv.h, не в conn_mgr.h) */ /* core — публичные (в priv.h, не в conn_mgr.h) */
int conn_mgr_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms, int cm_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms,
conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle); conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle);
/* core */ /* core */
@ -198,8 +146,6 @@ void cm_handle_direct_req(struct ETCP_CONN* conn, const uint8_t* data, size_t le
void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id); void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id);
void cm_reverse_init_cb(struct ETCP_CONN* conn, int event, void* arg); void cm_reverse_init_cb(struct ETCP_CONN* conn, int event, void* arg);
void cm_reverse_timeout_cb(void* arg); void cm_reverse_timeout_cb(void* arg);
void cm_invite_fail(struct cm_invite_pending* inv);
void cm_invite_cleanup(struct cm_invite_pending* inv);
int cm_nat_compatible(struct ETCP_SOCKET* our, uint8_t tcfg, uint8_t tnat); int cm_nat_compatible(struct ETCP_SOCKET* our, uint8_t tcfg, uint8_t tnat);
void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s); void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s);
void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s); void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s);
@ -228,6 +174,5 @@ void cm_bg_ping_timer_cb(void* arg);
void cm_candidate_ping_timer_cb(void* arg); void cm_candidate_ping_timer_cb(void* arg);
uint64_t cm_get_node_max_probe_time(struct TOPO_GROUP_NODE* nq); uint64_t cm_get_node_max_probe_time(struct TOPO_GROUP_NODE* nq);
int cm_is_rtt_fresh(struct TOPO_GROUP_NODE* nq, uint64_t now_tb); int cm_is_rtt_fresh(struct TOPO_GROUP_NODE* nq, uint64_t now_tb);
void cm_invite_overall_timeout(void* arg);
#endif #endif

2
src/routing_layer/route6_lib_doc.md

@ -47,4 +47,4 @@ if (rd) forward_to_etcp(rd->v_node_info, pkt);
### Зависимости ### Зависимости
- `lib/radix.h` — radix tree (longest-match lookup) - `lib/radix.h` — radix tree (longest-match lookup)
- `lib/ll_queue.h` — lock-free очередь с хеш-индексом для поиска по `node_id` - `lib/ll_queue.h` — lock-free очередь с хеш-индексом для поиска по `node_id`
- `src/topo_node.h` — `TOPO_NODEQ`, `TOPO_SUBNET6` - `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_SUBNET6`

2
src/routing_layer/route_connectivity_doc.md

@ -59,6 +59,6 @@ route_connectivity_cancel_all(instance);
### Зависимости ### Зависимости
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг) - `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг)
- `src/topo_node.h` — `TOPO_NODEQ`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6` - `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6`
- `src/topo_group.h` — `topo_groups_get_default()` (для cancel_all) - `src/topo_group.h` — `topo_groups_get_default()` (для cancel_all)
- `lib/u_async.h` — get_time_tb() для отметок времени - `lib/u_async.h` — get_time_tb() для отметок времени

4
src/routing_layer/route_lib_doc.md

@ -40,7 +40,7 @@ route_table_destroy(rt);
| Структура | Поля | Описание | | Структура | Поля | Описание |
|-----------|------|----------| |-----------|------|----------|
| `ROUTE_ENTRY` | `network` (uint32_t, BE), `prefix_length` (uint8_t), `v_node_info` (TOPO_NODEQ*) | Запись маршрута. `v_node_info==NULL` → локальный | | `ROUTE_ENTRY` | `network` (uint32_t, BE), `prefix_length` (uint8_t), `v_node_info` (TOPO_GROUP_NODE*) | Запись маршрута. `v_node_info==NULL` → локальный |
| `ROUTE_TABLE` | `entries`, `count`, `capacity`, `dynamic_subnets`, `local_subnets`, `stats` | Таблица маршрутизации. Динамический массив + статистика | | `ROUTE_TABLE` | `entries`, `count`, `capacity`, `dynamic_subnets`, `local_subnets`, `stats` | Таблица маршрутизации. Динамический массив + статистика |
### Функции ### Функции
@ -49,7 +49,7 @@ route_table_destroy(rt);
|---------|----------| |---------|----------|
| `route_table_create()` | Выделяет `ROUTE_TABLE`, `entries` на `INITIAL_CAPACITY=100`, счётчики в 0 | | `route_table_create()` | Выделяет `ROUTE_TABLE`, `entries` на `INITIAL_CAPACITY=100`, счётчики в 0 |
| `route_table_destroy(table)` | Освобождает `entries`, `dynamic_subnets`, `local_subnets`, саму таблицу | | `route_table_destroy(table)` | Освобождает `entries`, `dynamic_subnets`, `local_subnets`, саму таблицу |
| `route_insert(table, node)` | Вставляет все v4-подсети узла (`TOPO_NODEQ`) с сортировкой. Проверяет overlap. Расширяет capacity при необходимости | | `route_insert(table, node)` | Вставляет все v4-подсети узла (`TOPO_GROUP_NODE`) с сортировкой. Проверяет overlap. Расширяет capacity при необходимости |
| `route_delete(table, node)` | Удаляет все записи, где `v_node_info == node`, со сдвигом массива | | `route_delete(table, node)` | Удаляет все записи, где `v_node_info == node`, со сдвигом массива |
| `route_lookup(table, dest_ip)` | LPM-поиск. Возвращает `ROUTE_ENTRY*` или NULL. Инкрементит `stats` (hits/misses) | | `route_lookup(table, dest_ip)` | LPM-поиск. Возвращает `ROUTE_ENTRY*` или NULL. Инкрементит `stats` (hits/misses) |
| `route_table_print(table)` | Печатает содержимое таблицы в лог | | `route_table_print(table)` | Печатает содержимое таблицы в лог |

55
src/routing_layer/topo_group_connect.c

@ -1,7 +1,7 @@
/* /*
* topo_group_connect.c — авто-подключение к узлам группы (бесконечный цикл до успеха) * topo_group_connect.c — авто-подключение к узлам группы (бесконечный цикл до успеха)
* *
* Phase 1: одновременный запуск conn_mgr_open_invite для пиров с connected=1. * Phase 1: одновременный запуск node_conn_direct_open для пиров с connected=1.
* Таймаут = TGC_DIRECT_TIMEOUT_MS. Если хоть один подключился → done. * Таймаут = TGC_DIRECT_TIMEOUT_MS. Если хоть один подключился → done.
* *
* Phase 2: последовательный перебор (supernode, затем public/EIM адреса). * Phase 2: последовательный перебор (supernode, затем public/EIM адреса).
@ -26,7 +26,7 @@
#include "topo_group.h" #include "topo_group.h"
#include "topo_node.h" #include "topo_node.h"
#include "topo_node_sqlite.h" #include "topo_node_sqlite.h"
#include "conn_mgr.h" #include "../transport_layer/node_conn_direct.h"
#include "../lib/debug_config.h" #include "../lib/debug_config.h"
#include "../lib/mem.h" #include "../lib/mem.h"
#include "../lib/u_async.h" #include "../lib/u_async.h"
@ -61,9 +61,9 @@ struct TOPO_GROUP_CONNECT {
int tried_super; int tried_super;
uint64_t* candidate_ids; uint64_t* candidate_ids;
int candidate_count; int candidate_count;
struct CONN_MGR_HANDLE* handles[TGC_MAX_HANDLES]; struct NODE_CONN_DIRECT* handles[TGC_MAX_HANDLES];
int handle_count; int handle_count;
struct CONN_MGR_HANDLE* cur_handle; /* handle текущей попытки Phase 2/3 (для закрытия на TIMEOUT) */ struct NODE_CONN_DIRECT* cur_handle; /* handle текущей попытки Phase 2/3 (для закрытия на TIMEOUT) */
}; };
static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc); static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc);
@ -72,8 +72,7 @@ static void tgc_phase1_timeout(void* arg);
static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc); static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc);
static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), const char* label); static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), const char* label);
static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg);
enum conn_mgr_event event, void* arg);
/* ─── нотификация GUI о списке узлов в процессе подключения ─── */ /* ─── нотификация GUI о списке узлов в процессе подключения ─── */
@ -131,8 +130,8 @@ int topo_group_connect_init(struct TOPO_GROUP* group) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase 1 launching %d connects ch=%s grp=%016llx", TGC_ID, count, group->channel_id, (unsigned long long)group->group_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase 1 launching %d connects ch=%s grp=%016llx", TGC_ID, count, group->channel_id, (unsigned long long)group->group_id);
for (int i = 0; i < count && gc->handle_count < TGC_MAX_HANDLES; i++) { for (int i = 0; i < count && gc->handle_count < TGC_MAX_HANDLES; i++) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: Phase1 connect to 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)ids[i], i + 1, count); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: Phase1 connect to 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)ids[i], i + 1, count);
conn_mgr_open_invite(group->instance, group->group_id, NULL, ids[i], tgc_callback, gc, node_conn_direct_open(group->instance, ids[i], tgc_callback, gc,
&gc->handles[gc->handle_count++]); &gc->handles[gc->handle_count++], NULL);
} }
tgc_notify_connecting(gc, ids, gc->handle_count); tgc_notify_connecting(gc, ids, gc->handle_count);
u_free(ids); u_free(ids);
@ -156,8 +155,8 @@ void topo_group_connect_destroy(struct TOPO_GROUP* group) {
#ifdef UTUN_HAVE_STANDBY #ifdef UTUN_HAVE_STANDBY
if (gc->pause_wait) { standby_wait_cancel(gc->pause_wait); gc->pause_wait = NULL; } if (gc->pause_wait) { standby_wait_cancel(gc->pause_wait); gc->pause_wait = NULL; }
#endif #endif
for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); for (int i = 0; i < gc->handle_count; i++) node_conn_direct_close(gc->handles[i]);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; }
u_free(gc->candidate_ids); u_free(gc->candidate_ids);
u_free(gc); u_free(gc);
} }
@ -275,17 +274,17 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn
* В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить * В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить
* залипшее состояние) и ставит паузу перед следующей попыткой. * залипшее состояние) и ставит паузу перед следующей попыткой.
*/ */
static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
enum conn_mgr_event event, void* arg) {
struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg;
(void)group_id; struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
uint64_t node_id = conn ? conn->peer_node_id : 0;
int ok = (event == CONN_EVENT_UP); int ok = (event == NCD_EVENT_UP);
if (ok) { if (ok) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id);
if (nq) nq->handle = h; if (nq) nq->handle = h;
} else if (event == CONN_EVENT_TIMEOUT) { } else if (event == NCD_EVENT_TIMEOUT) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id);
if (nq) nq->handle = NULL; if (nq) nq->handle = NULL;
} }
@ -304,7 +303,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g
tgc_notify_connecting(gc, NULL, 0); tgc_notify_connecting(gc, NULL, 0);
} else { } else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; }
tgc_start_pause(gc, (void(*)(void*))tgc_phase2_try_next, "tgc_p2_try"); tgc_start_pause(gc, (void(*)(void*))tgc_phase2_try_next, "tgc_p2_try");
} }
break; break;
@ -315,7 +314,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g
tgc_notify_connecting(gc, NULL, 0); tgc_notify_connecting(gc, NULL, 0);
} else { } else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; }
tgc_start_pause(gc, (void(*)(void*))tgc_phase3_try_next, "tgc_p3_try"); tgc_start_pause(gc, (void(*)(void*))tgc_phase3_try_next, "tgc_p3_try");
} }
break; break;
@ -369,8 +368,8 @@ static void tgc_phase1_timeout(void* arg) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int connected = 0; int connected = 0;
for (int j = 0; j < gc->handle_count; j++) { for (int j = 0; j < gc->handle_count; j++) {
struct CONN_MGR_HANDLE* h = gc->handles[j]; struct NODE_CONN_DIRECT* h = gc->handles[j];
if (h && conn_mgr_get_conn(h) != NULL) { connected = 1; break; } if (h && node_conn_direct_get_conn(h) != NULL) { connected = 1; break; }
} }
if (!connected) topo_node_sqlite_set_connected(db, gc->group->channel_id, ids[i], 0); if (!connected) topo_node_sqlite_set_connected(db, gc->group->channel_id, ids[i], 0);
} }
@ -378,11 +377,11 @@ static void tgc_phase1_timeout(void* arg) {
int remaining = 0; int remaining = 0;
for (int i = 0; i < gc->handle_count; i++) { for (int i = 0; i < gc->handle_count; i++) {
struct CONN_MGR_HANDLE* h = gc->handles[i]; struct NODE_CONN_DIRECT* h = gc->handles[i];
if (h && conn_mgr_get_conn(h) != NULL) { if (h && node_conn_direct_get_conn(h) != NULL) {
gc->handles[remaining++] = h; gc->handles[remaining++] = h;
} else { } else {
if (h) conn_mgr_close(h); if (h) node_conn_direct_close(h);
} }
} }
gc->handle_count = remaining; gc->handle_count = remaining;
@ -435,7 +434,7 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) {
uint64_t nid = gc->candidate_ids[gc->cursor++]; uint64_t nid = gc->candidate_ids[gc->cursor++];
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 trying 0x%016llx (%d/%d) super_round=%d", TGC_ID, DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 trying 0x%016llx (%d/%d) super_round=%d", TGC_ID,
(unsigned long long)nid, gc->cursor, gc->candidate_count, gc->tried_super); (unsigned long long)nid, gc->cursor, gc->candidate_count, gc->tried_super);
if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue; if (node_conn_direct_open(gc->group->instance, nid, tgc_callback, gc, &gc->cur_handle, NULL) < 0) continue;
tgc_notify_connecting(gc, &nid, 1); tgc_notify_connecting(gc, &nid, 1);
return; return;
} }
@ -469,7 +468,7 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) {
uint64_t nid = gc->candidate_ids[gc->cursor++]; uint64_t nid = gc->candidate_ids[gc->cursor++];
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 trying 0x%016llx (%d/%d)", TGC_ID, DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 trying 0x%016llx (%d/%d)", TGC_ID,
(unsigned long long)nid, gc->cursor, gc->candidate_count); (unsigned long long)nid, gc->cursor, gc->candidate_count);
if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue; if (node_conn_direct_open(gc->group->instance, nid, tgc_callback, gc, &gc->cur_handle, NULL) < 0) continue;
tgc_notify_connecting(gc, &nid, 1); tgc_notify_connecting(gc, &nid, 1);
return; return;
} }
@ -490,8 +489,8 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) {
static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) { static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) {
gc->pause_timer = NULL; gc->pause_wait = NULL; gc->pause_timer = NULL; gc->pause_wait = NULL;
if (gc->phase_timer) { uasync_cancel_timeout(gc->group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; } if (gc->phase_timer) { uasync_cancel_timeout(gc->group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; }
for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); for (int i = 0; i < gc->handle_count; i++) node_conn_direct_close(gc->handles[i]);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; }
gc->handle_count = 0; gc->pending = 0; gc->connected_count = 0; gc->handle_count = 0; gc->pending = 0; gc->connected_count = 0;
gc->cursor = 0; gc->tried_super = 0; gc->cursor = 0; gc->tried_super = 0;
u_free(gc->candidate_ids); gc->candidate_ids = NULL; gc->candidate_count = 0; u_free(gc->candidate_ids); gc->candidate_ids = NULL; gc->candidate_count = 0;
@ -513,8 +512,8 @@ static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) {
TGC_ID, count, gc->group->channel_id); TGC_ID, count, gc->group->channel_id);
for (int i = 0; i < count && gc->handle_count < TGC_MAX_HANDLES; i++) { for (int i = 0; i < count && gc->handle_count < TGC_MAX_HANDLES; i++) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: Phase1 connect to 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)ids[i], i + 1, count); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: Phase1 connect to 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)ids[i], i + 1, count);
conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, ids[i], tgc_callback, gc, node_conn_direct_open(gc->group->instance, ids[i], tgc_callback, gc,
&gc->handles[gc->handle_count++]); &gc->handles[gc->handle_count++], NULL);
} }
tgc_notify_connecting(gc, ids, gc->handle_count); tgc_notify_connecting(gc, ids, gc->handle_count);
u_free(ids); u_free(ids);

396
src/routing_layer/topo_group_doc.md

@ -2,106 +2,112 @@
## 1. Назначение ## 1. Назначение
BGP-подобный обмен топологией узлов между пирами uTun. Каждый узел хранит полную таблицу известных узлов и при подключении нового пира синхронизирует её с ним. При изменении информации об узле или обнаружении его недоступности — изменения распространяются всем пирам (broadcast NODEINFO/WITHDRAW). BGP-подобный обмен топологией узлов между пирами uTun через ETCP. Каждый узел хранит таблицу
известных узлов и при подключении нового пира синхронизирует её с ним. Изменения информации об
узле и обнаружение недоступности распространяются всем пирам (NODEINFO/WITHDRAW).
Модуль поддерживает **изолированные группы** — разные «пространства имён» узлов с разной семантикой: Модуль поддерживает **изолированные группы** — разные «пространства имён» узлов с разной
- **TOPO_GROUP_TYPE_UTUN (1)** — VPN-сеть: обмен маршрутами (подсети) между узлами, NAT-детекция, проверка связности семантикой:
- **TOPO_GROUP_TYPE_CHAT (2)** — чат-группа: только узлы, без подсетей, персистентность в SQLite (membership) - **TOPO_GROUP_TYPE_UTUN (1)** — VPN-сеть: обмен маршрутами (подсети) между узлами, NAT-детекция,
проверка связности.
- **TOPO_GROUP_TYPE_CHAT (2)** — чат-группа: только узлы, без подсетей, персистентность в SQLite.
Группа по умолчанию (UTUN) создаётся автоматически при `topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для chatgui). Узлы разных групп изолированы: peer из utun-группы не видит узлы чат-группы и наоборот. Группа по умолчанию (UTUN, `group_id = TOPO_GROUP_UTUN`) создаётся автоматически при
`topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для
chatgui). Узлы разных групп изолированы, маршрутизация пакетов внутри группы ведётся по
`group_id` из wire-заголовков.
Помимо обмена топологией, модуль выполняет: Помимо обмена топологией, модуль выполняет:
- **Сборку local_node** (`topo_group_update_my_nodeinfo`) — при старте и при изменениях конфигурации сокетов собирает информацию о себе (адреса, подсети, ключи) - **Сборку local_node** (`topo_group_update_my_nodeinfo`) — при старте и при изменениях сокетов/active-mode.
- **NAT-детекцию** — выделена в отдельный самодостаточный модуль `nat_detection.h` (свой ETCP_ID 0x02, своя обработка пакетов); topo_group предоставляет делегат `topo_group_set_nat_check_local` для тестовых нужд - **NAT-детекцию** — выделена в отдельный модуль `nat_detection.h` (свой ETCP_ID 0x02).
- **Поиск оптимального маршрута** до узла (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей (ETCP соединений) - **Поиск оптимального маршрута** (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей.
- **Зондирование связности** — для новых узлов запускает `route_connectivity_probe_node()` - **Зондирование связности** — `route_connectivity_probe_node()` для новых узлов.
- **Персистентность в SQLite** — синхронизация узлов и membership в БД (таблицы nodes, node_addresses, peers_*) - **Авто-подключение к узлам CHAT-группы** — `topo_group_connect` (бесконечный цикл до успеха).
- **Восстановление каскадно отвалившихся узлов** — `topo_recovery` (после разрыва соединения).
- **Invite/join** — `topo_group_invite` (прямое подключение + проверка членства).
- **Персистентность в SQLite** — `topo_node_sqlite` (таблицы nodes, node_addresses, peers_*).
- **Per-group broadcast** — `broadcast` (дёшевая рассылка данных с TTL внутри группы).
## 2. Как пользоваться ## 2. Как пользоваться
### 2.1. Инициализация (при старте utun) ### 2.1. Инициализация (при старте utun)
```c ```c
struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт контейнер + utun-группу по умолчанию struct TOPO_GROUPS* g = topo_groups_init(instance); // контейнер + utun-группа по умолчанию
``` ```
Здесь же: Здесь же:
- `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — регистрирует приёмник пакетов топологии - Открывается SQLite (`{db_path}/chats.db`), если задан `db_path`; БД — `instance->topo_sqlite_db`.
- `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)` — подписывается на события on_up/on_down всех новых ETCP-соединений - `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — приёмник пакетов топологии.
- Открывается SQLite (если указан `db_path` в конфиге), БД сохраняется в `instance->topo_sqlite_db` - `etcp_add_conn_status_cbk(instance, topo_group_conn_status, g)` — события UP/DOWN/DELETE всех соединений.
- NAT-детекция инициализируется отдельно в `utun_instance.c` через `nat_detection_create(instance)` - `etcp_add_socket_cbk(...)` — изменения адресов/статуса сокетов.
- `utun_add_activity_cbk(...)` — смена active/standby режима.
### 2.2. Новое ETCP-соединение ### 2.2. Новое ETCP-соединение
При ETCP on_up автоматически вызывается `topo_group_new_conn(group, conn)`: При ETCP UP (`topo_group_conn_status` → `topo_group_new_conn` для каждой группы):
1. Добавляет conn в `senders_list` (через `topo_group_add_to_senders`) 1. Дедупликация (тот же conn стреляет UP дважды — UDP- и TCP-линк).
2. Отправляет `TOPO_SUBCMD_REQUEST_TABLE` — запрос полной таблицы 2. `topo_recovery_cancel_for_node` — узел появился в сети, отменяем его recovery.
3. Обновление peer-узла: `conn_presence/conn_up |= NCONN_DIRECT`.
4. `topo_group_add_to_senders` + `topo_group_send_table_request` (запрос полной таблицы).
5. `topo_group_connect_on_up` (авто-подключение CHAT).
При приёме `TOPO_SUBCMD_REQUEST_TABLE` peer отправляет: На `REQUEST_TABLE` пир отвечает: NODEINFO о себе (local_node) + full table sync (все узлы, кроме
- NODEINFO c локальным узлом (local_node) тех, чей путь уже включает этого пира — защита от петель) + `TABLE_COMPLETE`. При получении
- Full table sync — все известные узлы (с hop_count > 0), кроме тех, чей путь уже включает этого пира (защита от петель) `TABLE_COMPLETE` вызывается `etcp_set_routing_exchange_state(from_conn, 3)`.
- `TOPO_SUBCMD_TABLE_COMPLETE` — метка завершения начальной синхронизации; при получении вызывает `etcp_set_routing_exchange_state(from_conn, 3)`
### 2.3. NODEINFO (обновление/создание узла) ### 2.3. NODEINFO (обновление/создание узла)
`topo_group_process_nodeinfo(group, from, data, len)`: `topo_group_process_nodeinfo(group, from, data, len)`:
1. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS`; при несовпадении — отправка `TOPO_SUBCMD_ERR_GROUP_MISMATCH` 1. Проверка `group_id` пакета (`ni->group_id == group->group_id`).
2. Проверка `hop_count < MAX_HOPS(16)` 2. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS` (CHAT → 0, UTUN → 1);
3. Проверка версии узла — если версия не новее (stale), только обновляет пути (hop_list) и выходит при несовпадении — `TOPO_SUBCMD_ERR_GROUP_MISMATCH`.
4. Десериализация wire-формата → обновление/создание `TOPO_NODEQ` (через `topo_node_deserialize`) 3. Проверка `hop_count < MAX_HOPS`; пропуск собственного `node_id`.
5. При создании нового узла: инициализация `connectivity` (PROBE_STATUS_NONE, PROBE_RESULT_UNKNOWN) 4. Проверка версии: если `(int8_t)(last_ver - new_ver) >= 0` (stale) — только обновляется путь.
6. Добавление пути (`topo_group_add_path`) через текущее соединение, с очисткой старых путей содержащих peer_node_id (`topo_group_remove_path_by_hop`) 5. Десериализация wire-формата → `TOPO_NODE` (глобальная идентичность) + подсети + hop_list.
7. Вставка в `routing` (`route_insert`) — только для UTUN-групп 6. Верификация идентичности: `node_id == sc_derive_node_id_from_pubkey(public_key)` и
8. Сохранение в SQLite (если есть): `topo_node_sqlite_node_put` + для CHAT групп — `topo_node_sqlite_member_put` Ed25519 self-signature (`x25519_self_sig`) над каноническим сообщением.
9. Уведомление control_server (`control_server_notify_node_change`) 7. Сохранение в глобальный реестр `node_registry` (дедупликация по node_id, ref_count).
10. Callback `node_updated_cb` (для chatgui member_sync) — вызывается и при создании, и при обновлении 8. Обновление/создание `TOPO_GROUP_NODE` (per-group): paths, last_ver, subnets.
11. Broadcast всем пирам, кроме hop_list (защита от петель) 9. `topo_group_remove_path_by_hop` + `topo_group_add_path` (через текущий conn).
12. Запуск зондирования связности для новых узлов с адресами (`route_connectivity_probe_node`) 10. `conn_presence/conn_up |= NCONN_BGP` (+ `NCONN_DIRECT` если прямой).
11. `route_insert` — только для UTUN-групп.
12. SQLite: `topo_node_sqlite_member_exists` → `addrs_put`; для CHAT — `member_placeholder_put`
(source=1, вне merkle-хеша); `nodeinfo_updated`.
13. `control_server_notify_node_change`.
14. Callback `node_updated_cb` (legacy, в текущем коде потребителей нет).
15. Fire BGP node event callbacks (`TOPO_NODE_EVENT_NEW`/`UPDATE`) — используют member_sync, media_delivery.
16. `topo_fire_nodeinfo_cbk` — глобальные nodeinfo-подписчики.
17. Broadcast NODEINFO всем пирам, кроме самого узла и тех, чей node_id есть в hop_list (антипетля).
18. `route_connectivity_probe_node` для новых/изменённых узлов с адресами.
### 2.4. WITHDRAW (удаление узла) ### 2.4. WITHDRAW (удаление узла)
`topo_group_process_withdraw(group, sender, data, len)`: `topo_group_process_withdraw(group, sender, data, len)`:
1. Поиск узла по `node_id` 1. Поиск `TOPO_GROUP_NODE` по `node_id`.
2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление всех путей, содержащих wd_source в hop_list 2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление путей, содержащих wd_source в hop_list.
3. Если путей не осталось (ret == 1): удаление из routing, уведомление control_server, установка `dirty = 1`, отмена connectivity probe (`route_connectivity_cancel_node`), освобождение paths, `topo_node_free_lists`, закрытие виртуальных каналов (`etcp_router_conn_close_all_for_node`), broadcast withdraw дальше 3. Если путей не осталось: сброс `conn_presence/conn_up`, `route_delete` (UTUN), уведомление
4. Если группа CHAT — удаление member из SQLite через `topo_node_sqlite_member_del` control_server, отмена connectivity probe, освобождение paths и group-полей, удаление из
`nodes`, `etcp_router_conn_close_all_for_node`, broadcast withdraw (кроме отправителя),
fire `TOPO_NODE_EVENT_REMOVE`.
4. **BGP не удаляет member-запись из SQLite** — удаление из `peers_*` только через битую подпись
при верификации (member_sync).
### 2.5. Отключение соединения ### 2.5. Отключение соединения
При ETCP on_down вызывается `topo_group_remove_conn(group, conn)`: При ETCP DOWN/DELETE (`topo_group_remove_conn`):
1. Проверка наличия conn в senders_list (если уже удалён — выход) 1. Проверка наличия conn в senders_list (если нет — выход).
2. Для всех узлов с путями через этот conn — `topo_group_remove_path`. Если узел стал полностью недоступен (нет путей) — удаляется из routing и nodes, устанавливается `dirty = 1`, отменяется connectivity probe, закрываются виртуальные каналы, broadcast withdraw 2. Для каждого узла с путями через conn: вычисляется next_hop из hop_list, `remove_path`. Если
3. Удаление conn из `senders_list` узел стал недоступен — `topo_recovery_add_node` (для каскадных, кроме самого пира), полная
очистка (как в WITHDRAW) + broadcast withdraw.
3. Удаление conn из `senders_list`.
4. Если были каскадные узлы — `topo_recovery_start`.
5. `topo_group_connect_on_down`.
### 2.6. Поиск пути до узла ### 2.6. Поиск пути до узла
`topo_group_find_conn_for_node(group, node_id)` — перебирает `path`'ы узла, выбирает с минимальным `hop_count`. Предпочитает live-пути (links_up > 0). Если live-путей нет, fallback на любой путь с минимальным hop_count. `topo_group_find_conn_for_node(group, node_id)` — перебирает paths узла, выбирает min `hop_count`,
предпочитая live-пути (`conn->links_up > 0`); fallback на любой путь. Интенсивно используется
Интенсивно используется `etcp_router`: `etcp_router` и `conn_mgr` (next-hop для маршрутов и multihop-соединений).
- При маршрутизации пакетов к удалённому узлу
- При поиске next-hop для multihop соединений
### 2.7. NAT-детекция
NAT-детекция выделена в отдельный самодостаточный модуль `nat_detection.h`:
- Свой ETCP ID: `ETCP_ID_NAT_DETECTION` (0x02)
- Сам подписывается на conn_up/conn_down, сам принимает свои пакеты
- При поднятии линка (INIT handshake) вызывается `nat_detection_link_ready()`
- Ищет третий узел напрямую в `instance->connections`, обновляет `ETCP_SOCKET` и broadcast-ит NODEINFO через `topo_group_update_my_nodeinfo()`
Из topo_group в NAT-детекцию остался только делегат:
- `topo_group_set_nat_check_local(group, allow)` → вызывает `nat_detection_set_allow_local(instance->nat_det, allow)`
### 2.8. Широковещательная рассылка
- **NODEINFO**: при поступлении новой/обновлённой информации об узле — отправляется всем соседям, кроме тех, чей node_id уже есть в hop_list (защита от петель)
- **WITHDRAW**: при удалении узла — отправляется всем соседям, кроме отправителя
### 2.9. Изоляция групп
При приёме NODEINFO проверяется флаг `TOPO_FLAG_SEND_SUBNETS`:
- UTUN-группа ожидает флаг = 1 (с подсетями)
- CHAT-группа ожидает флаг = 0 (без подсетей)
- При несовпадении отправляется `TOPO_SUBCMD_ERR_GROUP_MISMATCH` и пакет игнорируется
## 3. API ## 3. API
@ -109,130 +115,119 @@ NAT-детекция выделена в отдельный самодостат
| Структура | Назначение | | Структура | Назначение |
|---|---| |---|---|
| `TOPO_GROUPS` | Контейнер всех групп топологии экземпляра. Хранит `group_list` (ll_queue), memory_pool'ы для сериализации (v4_sock_meta, v4_addr, v6_sock_meta, v6_addr, v4_subnet, v6_subnet), callback `node_updated_cb` | | `TOPO_GROUPS` | Контейнер всех групп. `group_list` (hash), `node_registry` (глобальный реестр TOPO_NODE*, hash), memory_pool'ы (v4/v6 sock_meta, addr, subnet), `node_updated_cb` (legacy) |
| `TOPO_GROUP` | Одна группа топологии. `group_id`, `group_type`, `nodes` (ll_queue узлов), `senders_list` (ll_queue conn'ов), `local_node` (TOPO_NODEQ), `ed25519_public_key` (производный из X25519), `channel_id[64]` (для CHAT-групп) | | `TOPO_GROUP` | Одна группа. `group_id`, `group_type`, `nodes` (queue TOPO_GROUP_NODE), `senders_list` (queue conn'ов), `local_node`, `ed25519_public_key`, `channel_id` (CHAT), `conn_mgr`, `recovery_list`, `connect`, `broadcast`, `node_cbks` |
| `TOPO_GROUP_CONN_ITEM` | Элемент списка senders: conn + ll_entry — связка с senders_list | | `TOPO_NODE` | Глобальная идентичность узла (в `node_registry`): pubkeys, `x25519_self_sig`, name, v4/v6 sock_meta+addrs, `client_type`, `client_activity`, `ver`, `group_ref_count` |
| `TOPO_NODE` | Идентичность узла: pubkeys, имя, версия (ver), списки адресов (v4/v6 sock_meta, addrs). Разделяется между группами через ref_count | | `TOPO_GROUP_NODE` | Per-group запись узла: `node_id`, `subnets`, `paths`, `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) |
| `TOPO_NODEQ` | Запись в очереди группы: указатель на TOPO_NODE, пути (paths), hop_list, tranzit_data, subnets, connectivity, conn_mgr (тип + intermediaries), best_socket, dirty, last_ver | | `TOPO_NODEPATH` | Путь до узла через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]` |
| `TOPO_NODEPATH` | Путь до узла через конкретное ETCP_CONN: conn + hop_count | | `TOPO_NODESUBNETS` | Подсети узла: списки `TOPO_SUBNET4`/`TOPO_SUBNET6` |
| `TOPO_NODESUBNETS` | Подсети узла: списки TOPO_SUBNET4 / TOPO_SUBNET6 | | `TOPO_CONNECTIVITY` | Связность: probe_status, rtt-метрики, статусы (interface/nat/real), `probe_deferred_wait` (standby) |
| `TOPO_CONNECTIVITY` | Состояние связности с узлом: probe_status, pending_count, rtt-метрики, статусы (interface/nat/real), probe_list | | `TOPOMSG_NODEINFO_PKT` | Wire NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть |
| `TOPOMSG_NODEINFO_PKT` | Wire-формат пакета NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть | | `TOPOMSG_WITHDRAW_PKT` | Wire WITHDRAW: `group_id`, `node_id` (удаляемый), `wd_source` (инициатор) |
| `TOPOMSG_WITHDRAW_PKT` | Wire-формат WITHDRAW: node_id (удаляемый) + wd_source (инициатор) | | `TOPOMSG_TABLE_REQ` | Wire запроса/завершения таблицы: `group_id` |
| `TOPOMSG_TABLE_REQ` | Wire-формат запроса/завершения таблицы: cmd + subcmd | | `TOPOMSG_ERR_GROUP_MISMATCH` | Wire ошибки типа группы: `group_id`, `expected_type`, `received_flags` |
| `TOPOMSG_ERR_GROUP_MISMATCH` | Wire-формат ошибки несоответствия типа группы: expected_type + received_flags | | `NAT_DETECTION` | Контекст NAT-детекции (отдельный модуль) |
| `NAT_DETECTION` | Контекст NAT-детекции (отдельный модуль): ping_pending, next_ping_req_id, allow_nat_check_local |
### 3.2. Константы ### 3.2. Константы
| Константа | Значение | Назначение | | Константа | Значение | Назначение |
|---|---|---| |---|---|---|
| `ETCP_ID_TOPO_ENTRY` | 0x01 | ETCP ID для пакетов топологии | | `ETCP_ID_TOPO_ENTRY` | 0x01 | ETCP ID пакетов топологии |
| `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле + подсети | | `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле |
| `TOPO_SUBCMD_REQUEST_TABLE` | 0x05 | Запрос полной таблицы | | `TOPO_SUBCMD_REQUEST_TABLE` | 0x05 | Запрос полной таблицы |
| `TOPO_SUBCMD_WITHDRAW` | 0x06 | Узел стал недоступен | | `TOPO_SUBCMD_WITHDRAW` | 0x06 | Узел стал недоступен |
| `TOPO_SUBCMD_TABLE_COMPLETE` | 0x0B | Завершение начальной синхронизации | | `TOPO_SUBCMD_TABLE_COMPLETE` | 0x0B | Завершение начальной синхронизации |
| `TOPO_SUBCMD_ERR_GROUP_MISMATCH` | 0x0C | Ошибка несоответствия типа группы | | `TOPO_SUBCMD_ERR_GROUP_MISMATCH` | 0x0C | Ошибка несоответствия типа группы |
| `MAX_HOPS` | 16 | Максимальная длина hop-листа | | `MAX_HOPS` | 16 | Максимальная длина hop-листа |
| `BGP_NODES_HASH_SIZE` | 256 | Размер хеш-таблицы nodes | | `BGP_NODES_HASH_SIZE` / `TOPO_NODE_REGISTRY_HASH_SIZE` | 256 | Хеш-таблицы nodes / node_registry |
| `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id по умолчанию для utun-группы | | `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id utun-группы по умолчанию |
| `TOPO_GROUP_TYPE_UTUN` | 1 | Тип группы: VPN-сеть (подсети, маршруты) | | `TOPO_GROUP_TYPE_UTUN` / `TOPO_GROUP_TYPE_CHAT` | 1 / 2 | Тип группы |
| `TOPO_GROUP_TYPE_CHAT` | 2 | Тип группы: чат (без подсетей) |
| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг NODEINFO: узел передаёт подсети | | `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг NODEINFO: узел передаёт подсети |
| `PROBE_STATUS_NONE` | 0 | Связность: не проверялась | | `NCONN_DIRECT` / `NCONN_INDIRECT` / `NCONN_BGP` | 1/2/4 | Биты наличия соединений (presence/up) |
| `PROBE_STATUS_IN_PROGRESS` | 1 | Связность: проверка идёт | | `CONN_TYPE_DIRECT` / `REVERSE` / `INDIRECT` / `NONE` | 1/2/3/0 | Стратегия conn_mgr |
| `PROBE_STATUS_DONE` | 2 | Связность: проверка завершена | | `CLIENT_TYPE_SERVER` / `DESKTOP` / `MOBILE` | 0/1/2 | Тип клиента |
| `PROBE_RESULT_UNKNOWN` | 0 | Результат проверки: неизвестно | | `CLIENT_ACTIVITY_STANDBY` / `ACTIVE` | 0/1 | Режим (standby/active) |
| `PROBE_RESULT_REACHABLE` | 1 | Результат проверки: доступен | | `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы |
| `PROBE_RESULT_UNREACHABLE` | 2 | Результат проверки: недоступен | | `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы |
| `TOPO_NODE_EVENT_NEW/UPDATE/REMOVE` | 0/1/2 | События BGP node callbacks |
| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников для INDIRECT |
### 3.3. Функции инициализации/завершения ### 3.3. Инициализация/завершение
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_groups_init(instance)` | Создаёт TOPO_GROUPS, memory_pool'ы, открывает SQLite (если db_path в конфиге), создаёт utun-группу по умолчанию, регистрирует ETCP-коллбэки | | `topo_groups_init(instance)` | Контейнер + node_registry + пулы + SQLite + utun-группа + ETCP/activity коллбэки |
| `topo_groups_destroy(instance)` | Освобождает все группы, memory_pool'ы, закрывает SQLite, отменяет connectivity probes, снимает ETCP-регистрацию | | `topo_groups_destroy(instance)` | Отвязка коллбэков, destroy групп, node_registry, пулов, закрытие SQLite |
| `topo_group_create(instance, group_id, group_type)` | Создаёт TOPO_GROUP: nodes (hash queue), senders_list, local_node (через update_my_nodeinfo), ed25519_pubkey; вызывается из topo_groups_init / topo_groups_create_group | | `topo_group_create(instance, group_id, group_type)` | Создать TOPO_GROUP: nodes/senders/local_node/conn_mgr/broadcast |
| `topo_group_destroy(group)` | Освобождает senders_list, local_node (free_lists), nodes очереди | | `topo_group_destroy(group)` | recovery_cancel_all, connect_destroy, broadcast_destroy, conn_mgr_destroy, очистка nodes/local_node |
| `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создаёт новую группу и добавляет в group_list; для CHAT устанавливает channel_id | | `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создать группу + добавить в group_list + new_conn для существующих соединений + connect_init (CHAT) |
| `topo_groups_find(g, group_id)` | Поиск группы по group_id через хеш-индекс group_list | | `topo_groups_find(g, group_id)` / `topo_groups_get_default(g)` | Поиск группы / utun-группа |
| `topo_groups_get_default(g)` | Возвращает utun-группу по умолчанию (group_id=TOPO_GROUP_UTUN) | | `topo_group_on_activity_change(instance, active, arg)` | Обновить local_node и broadcast его по всем группам/пирам |
### 3.4. Функции жизненного цикла соединения ### 3.4. Жизненный цикл соединения
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_group_new_conn(group, conn)` | Добавляет conn в senders_list, отправляет REQUEST_TABLE | | `topo_group_new_conn(group, conn)` | Дедуп, отмена recovery, обновить peer, add_to_senders, request_table, connect_on_up |
| `topo_group_remove_conn(group, conn)` | Удаляет все пути через conn, удаляет недоступные узлы (route_delete, cancel_connectivity, etcp_router_conn_close_all_for_node, broadcast withdraw), убирает conn из senders_list | | `topo_group_remove_conn(group, conn)` | Удалить пути, удалить недоступные узлы (recovery/broadcast), убрать conn, connect_on_down |
| `topo_group_etcp_conn_cbk(conn, arg)` | Callback при создании нового ETCP-соединения: подписывается на on_up/on_down через etcp_conn_add_up_cbk/down_cbk | | `topo_group_conn_status(conn, status, arg)` | Callback status: UP→new_conn, DOWN/DELETE→remove_conn для каждой группы |
| `topo_group_on_conn_up(conn, arg)` | При on_up: вызывает topo_group_new_conn |
| `topo_group_on_conn_down(conn, arg)` | При on_down: вызывает topo_group_remove_conn |
### 3.5. Функции обработки пакетов ### 3.5. Обработка пакетов
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_group_receive_cbk(from_conn, entry)` | Точка входа приёма пакетов топологии: работает только с default-группой, диспетчеризация по subcmd (NODEINFO/WITHDRAW/REQUEST_TABLE/TABLE_COMPLETE/ERR_GROUP_MISMATCH) | | `topo_group_receive_cbk(from_conn, entry)` | Точка входа: извлекает `group_id`, находит группу, диспетчеризация по subcmd |
| `topo_group_process_nodeinfo(group, from, data, len)` | Обработка NODEINFO: проверка группы/версии/hops, десериализация, обновление/создание TOPO_NODEQ, пути, routing, SQLite, broadcast, зондирование связности, node_updated_cb | | `topo_group_process_nodeinfo(group, from, data, len)` | Полный цикл NODEINFO (см. 2.3) |
| `topo_group_process_withdraw(group, sender, data, len)` | Обработка WITHDRAW: удаление путей с wd_source, если узел недоступен — полная очистка (routing, connectivity, paths, etcp_router), broadcast | | `topo_group_process_withdraw(group, sender, data, len)` | Удаление путей/узла по WITHDRAW (см. 2.4) |
| `topo_group_handle_request_table(group, conn)` | Обработка REQUEST_TABLE: отправляет NODEINFO (local_node) + full table sync + add_to_senders + TABLE_COMPLETE | | `topo_group_handle_request_table(group, conn)` | local_node + full table + add_to_senders + TABLE_COMPLETE |
### 3.6. Функции поиска ### 3.6. Поиск и пути
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_group_find_conn_for_node(group, node_id)` | Поиск оптимального ETCP_CONN до узла: min hop_count, предпочтение live-путям | | `topo_group_find_conn_for_node(group, node_id)` | Оптимальный ETCP_CONN (min hop_count, предпочтение live) |
| `topo_node_find_by_id(group, node_id)` | Поиск TOPO_NODEQ в nodes по node_id через хеш-индекс | | `topo_node_find_by_id(group, node_id)` | Поиск TOPO_GROUP_NODE по node_id (hash) |
| `topo_groups_find(g, group_id)` | Поиск группы по group_id | | `topo_group_add_path(nq, conn, hop_list, hop_count, cumulative_rtt)` | Создать TOPO_NODEPATH + inline hop_list |
| `topo_group_remove_path(nq, conn)` | Удалить путь; возвращает 1 если путей не осталось |
| `topo_group_remove_path_by_hop(nq, wd_source)` | Удалить пути, содержащие wd_source в hop_list |
| `topo_group_should_send_to(nq, target_id)` | Проверка антипетли (target_id не во всех путях) |
### 3.7. Функции broadcast ### 3.7. Broadcast
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_group_send_nodeinfo(group, node, conn)` | Сериализует TOPO_NODEQ и отправляет NODEINFO одному conn | | `topo_group_send_nodeinfo(group, node, conn, cumulative_rtt)` | Сериализовать и отправить NODEINFO одному conn |
| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW всем пирам (без exclude) | | `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW (wd_source = self) |
| `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Рассылает WITHDRAW всем conn в senders_list, кроме exclude | | `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Broadcast WITHDRAW всем, кроме exclude |
| `topo_group_send_full_table(group, conn)` | Отправляет все известные узлы (с hop_count > 0), кроме тех, чей путь включает target, указанному conn | | `topo_group_send_full_table(group, conn)` | Все известные узлы (антипетля) указанному conn |
| `topo_group_send_table_request(group, conn)` | Отправляет запрос полной таблицы (TOPO_SUBCMD_REQUEST_TABLE) | | `topo_group_send_table_request/complete(group, conn)` | REQUEST_TABLE / TABLE_COMPLETE |
| `topo_group_send_table_complete(group, conn)` | Отправляет метку завершения синхронизации (TOPO_SUBCMD_TABLE_COMPLETE) | | `topo_group_send_err_group_mismatch(group, conn, ...)` | ERR_GROUP_MISMATCH |
| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку несоответствия типа группы |
### 3.8. Функции NAT (делегаты в nat_detection) ### 3.8. Коллбэки (подписки)
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_group_set_nat_check_local(group, allow)` | Делегат в `nat_detection_set_allow_local(instance->nat_det, allow)` — разрешить/запретить NAT check для локальных подсетей (тесты) | | `topo_group_add_node_cbk/remove_node_cbk(group, fn, arg)` | BGP node events (NEW/UPDATE/REMOVE) — consumer: member_sync, media_delivery |
| `nat_detection_create(inst)` | Создаёт модуль NAT-детекции: etcp_bind на 0x02, подписка на conn_up/conn_down | | `utun_add_nodeinfo_cbk/remove_nodeinfo_cbk(instance, fn, arg)` | Глобальная подписка на изменения nodeinfo (per-instance) |
| `nat_detection_destroy(nd)` | Уничтожает модуль NAT-детекции | | `topo_fire_nodeinfo_cbk(instance, group, node)` | Вызвать глобальные nodeinfo-подписчики |
| `nat_detection_link_ready(nd, link)` | Запускает NAT-чек для линка после INIT handshake | | `topo_groups_set_node_updated_cb(groups, fn)` | Legacy callback (в текущем коде потребителей нет) |
| `nat_detection_cancel_for_conn(nd, conn)` | Отменяет все pending NAT-пинги для соединения |
| `nat_detection_request_check_all(nd)` | Принудительная перепроверка NAT для всех линков |
### 3.9. Функции путей ### 3.9. NAT (делегаты в nat_detection)
| Функция | Назначение | | Функция | Назначение |
|---|---| |---|---|
| `topo_group_add_path(nq, conn, hop_list, hop_count)` | Создаёт TOPO_NODEPATH для узла (conn + hop_list) и добавляет в paths | | `topo_group_set_nat_check_local(group, allow)` | Делегат в `nat_detection_set_allow_local` |
| `topo_group_remove_path(nq, conn)` | Удаляет путь через указанный conn; возвращает 1 если путей не осталось (unreachable) |
| `topo_group_remove_path_by_hop(nq, wd_source)` | Удаляет все пути, содержащие wd_source в hop_list (используется в WITHDRAW) |
| `topo_group_add_to_senders(group, conn)` | Добавляет conn в senders_list (с проверкой дубликатов) |
| `topo_group_should_send_to(nq, target_id)` | Проверяет, нужно ли отправлять NODEINFO узла указанному пиру (target_id не во всех путях) |
### 3.10. Callbacks/вспомогательные ### 3.10. Связанные модули
| Функция | Назначение | | Модуль | Назначение |
|---|---|
| `topo_groups_set_node_updated_cb(groups, fn)` | Устанавливает callback при создании/обновлении узла (используется chatgui для member_sync) |
| `topo_group_update_my_nodeinfo(instance, group)` | Собирает информацию о локальном узле из конфига и ключей: адреса сокетов, подсети, pubkeys, имя; обновляет group->local_node; вызывается при старте и при изменениях сокетов |
| `nodeinfo_dump_log(data, len)` | Логирует краткую сводку NODEINFO (node_id, ver, счётчики, hops) |
| `nodeinfo_format(data, len)` | Форматирует полный NODEINFO с именем, подсетями в читаемую строку (аллокация через u_malloc) |
| `group_subcmd_name(subcmd)` | Возвращает строковое имя subcmd для логов |
### 3.11. Проверка соответствия типа группы
| Функция | Назначение |
|---|---| |---|---|
| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку GROUP_MISMATCH пиру, если его NODEINFO содержит неверный flags (разные типы групп) | | `topo_group_connect` | Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл, standby-aware) |
| `topo_group_invite` | Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP, ETCP_ID 0x13) |
| `topo_recovery` | Восстановление каскадно отвалившихся узлов после разрыва соединения |
| `broadcast` | Per-group рассылка данных с TTL (ETCP_ID_BROADCAST), `broadcast_init/сброс` |
| `topo_node` | Модель данных: TOPO_NODE/TOPO_GROUP_NODE, ser/deserialize, node_registry, sig-msg |
| `topo_node_sqlite` | Персистентность: nodes, node_addresses, peers_*, member block/owner/placeholder |
## 4. Архитектурные связи ## 4. Архитектурные связи
@ -240,74 +235,81 @@ NAT-детекция выделена в отдельный самодостат
| Модуль | Как используется | | Модуль | Как используется |
|---|---| |---|---|
| `topo_node.c/h` | Модель данных узла: TOPO_NODE, TOPO_NODEQ, ser/deserialize, find_by_id, update_my_nodeinfo, free_lists, dump_all, format_all | | `topo_node.c/h` | Модель узла, ser/deserialize, node_registry, update_my_nodeinfo, sign_self |
| `topo_node_sqlite.h` | Персистентность в SQLite: topo_node_sqlite_node_put, member_put, member_del, init | | `topo_node_sqlite.h` | Персистентность (node_put, member_placeholder_put, addrs_put, nodeinfo_updated) |
| `nat_detection.h` | NAT-детекция: отдельный модуль с собственным ETCP_ID 0x02; делегат set_nat_check_local | | `nat_detection.h` | NAT-детекция (отдельный модуль, ETCP_ID 0x02) |
| `route_lib.h` | Таблица маршрутов: route_insert, route_delete | | `route_lib.h` | route_insert / route_delete |
| `route_connectivity.h` | Зондирование связности узла: route_connectivity_probe_node, cancel_node, cancel_all | | `route_connectivity.h` | probe_node / cancel_node / cancel_all |
| `etcp_api.h` | Регистрация приёмника: etcp_bind, etcp_add_new_conn_cbk, etcp_unbind | | `etcp_api.h` | etcp_bind, etcp_add_conn_status_cbk, etcp_add_socket_cbk, etcp_unbind |
| `etcp_connections.h` | Управление соединениями: etcp_conn_add_up_cbk, etcp_conn_add_down_cbk, ETCP_CONN, ETCP_LINK | | `etcp_connections.h` | ETCP_CONN, conn->peer_node_id, links_up |
| `etcp.h` | Отправка пакетов: etcp_send, etcp_set_routing_exchange_state | | `etcp.h` | etcp_send, etcp_set_routing_exchange_state |
| `etcp_debug.h` | Логирование ETCP: log_name | | `etcp_router.h` | etcp_router_conn_close_all_for_node |
| `etcp_router.h` | Закрытие виртуальных каналов: etcp_router_conn_close_all_for_node | | `control_server.h` | notify_node_change / notify_node_removed |
| `control_server.h` | Уведомления мониторинга: control_server_notify_node_change, control_server_notify_node_removed | | `conn_mgr.h` | conn_mgr_init/destroy (per-group), поиск путей |
| `conn_mgr.h` | Менеджер соединений: использует topo_group_find_conn_for_node для поиска путей | | `secure_channel.h` | sc_derive_ed25519_pubkey, sc_derive_node_id_from_pubkey, sc_ed25519_verify |
| `utun_instance.h` | UTUN_INSTANCE: конфиг, node_id, ключи, rt, topo_groups, nat_det, topo_sqlite_db, control_srv | | `broadcast.h` | broadcast_init/destroy (per-group) |
| `secure_channel.h` | Ключи: sc_derive_ed25519_pubkey | | `memory_pool.h` | Пулы для сериализации |
| `memory_pool.h` | Память для сериализации: memory_pool_init / destroy | | `debug_config.h` | Логирование, категория BGP |
| `debug_config.h` | Логирование: DEBUG_INFO/WARN/ERROR/DEBUG с категорией BGP | | `mem.h` | u_malloc/u_free/u_calloc |
| `mem.h` | Аллокация: u_malloc, u_free, u_calloc |
| `platform_compat.h` | Кроссплатформенная совместимость |
### 4.2. Кто вызывает topo_group ### 4.2. Кто вызывает topo_group
| Вызывающий | Что вызывает | Для чего | | Вызывающий | Что вызывает | Для чего |
|---|---|---| |---|---|---|
| `utun_instance.c` | `topo_groups_init`, `topo_groups_destroy` | Инициализация/завершение при старте/остановке utun | | `utun_instance.c` | `topo_groups_init/destroy` | Инициализация/завершение |
| `etcp_connections.c` | `topo_group_update_my_nodeinfo` | При изменении конфигурации сокетов — обновление local_node | | `etcp_connections.c` | `topo_group_send_nodeinfo` | Переотправка local_node при смене сокетов |
| `etcp_router.c` | `topo_group_find_conn_for_node` | Поиск next-hop для маршрутов и multihop-соединений (~15 вызовов) | | `etcp_router.c` | `topo_group_find_conn_for_node` | next-hop для маршрутов |
| `conn_mgr.c` | `topo_group_find_conn_for_node` | Поиск пути для установки прямого/обратного/indirect соединения | | `conn_mgr_*.c` | `topo_group_find_conn_for_node`, `topo_fire_nodeinfo_cbk` | Пути для соединений |
| `nat_detection.c` | `topo_group_update_my_nodeinfo` | При обновлении NAT-типа сокета — обновление local_node и broadcast | | `nat_detection.c` | `topo_group_send_nodeinfo`, `topo_group_update_my_nodeinfo` | Обновление local_node после NAT-чека |
| Тесты | `topo_groups_get_default`, `topo_group_find_conn_for_node`, `topo_node_find_by_id`, `topo_group_set_nat_check_local` | 9 тестовых файлов | | `member_sync.c` | `topo_group_add_node_cbk` | BGP → member_sync → node_props_changed |
| `media_delivery.c` | `topo_group_add_node_cbk` | События узлов для медиа |
### 4.3. ETCP-коллбэки (верхний уровень) | `route_connectivity.c` | `topo_fire_nodeinfo_cbk` | Уведомление об изменении узла |
Точка входа для пакетов топологии — `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` в `topo_groups_init`. Единственный обработчик на весь ETCP ID 0x01. Диспетчеризация внутри по subcmd (byte data[1]). Обработка всегда идёт через default-группу (TOPO_GROUP_UTUN).
Коллбэки on_up/on_down новых соединений регистрируются через `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)`, которая при создании каждого нового ETCP_CONN подписывается на его события.
## 5. Форматы пакетов (wire) ## 5. Форматы пакетов (wire)
### 5.1. NODEINFO ### 5.1. NODEINFO
``` ```
cmd(1) | subcmd(1) | TOPOMSG_NODE (82+ байта) | [node_name] | [v4_sock_meta[]] | [v4_addrs[]] | [v6_sock_meta[]] | [v6_addrs[]] | [v4_subnets[]] | [v6_subnets[]] | [tranzit[]] | [hop_list[]] cmd(1) | subcmd(1) | TOPOMSG_NODE | [node_name] | [v4_sock_meta[]] | [v4_addrs[]] | [v6_sock_meta[]] | [v6_addrs[]] | [v4_subnets[]] | [v6_subnets[]] | [hop_list[]]
``` ```
`TOPOMSG_NODE` содержит `group_id`, `node_id`, `ver`, pubkeys, `x25519_self_sig(64)`, счётчики,
`hop_count`, `client_type`, `client_activity`, `cumulative_rtt`.
### 5.2. WITHDRAW ### 5.2. WITHDRAW
``` ```
cmd(1) | subcmd(1) | node_id(8) | wd_source(8) = 18 байт cmd(1) | subcmd(1) | group_id(8) | node_id(8) | wd_source(8) = 26 байт
``` ```
### 5.3. REQUEST_TABLE / TABLE_COMPLETE ### 5.3. REQUEST_TABLE / TABLE_COMPLETE
``` ```
cmd(1) | subcmd(1) = 2 байта cmd(1) | subcmd(1) | group_id(8) = 10 байт
``` ```
### 5.4. ERR_GROUP_MISMATCH ### 5.4. ERR_GROUP_MISMATCH
``` ```
cmd(1) | subcmd(1) | expected_type(1) | received_flags(1) = 4 байта cmd(1) | subcmd(1) | group_id(8) | expected_type(1) | received_flags(1) = 12 байт
``` ```
## 6. Примечания ## 6. Примечания
- **Хранилище SQLite**: Открывается в `topo_groups_init` если в конфиге задан `db_path`. БД хранится как `instance->topo_sqlite_db` (sqlite3*), используется во всех группах. Файл: `{db_path}/chats.db`. Режим: WAL, foreign_keys=ON, synchronous=NORMAL. - **Глобальный реестр идентичности**: `TOPO_NODE` (pubkeys, адреса, имя) хранится один раз в
- **Версионирование NODEINFO**: Версия (ver, uint8_t 1-255) сравнивается через циклическую арифметику `(int8_t)(cur_ver - new_ver) >= 0`. Версия 0 не используется (нет данных). Если версия stale — обновляются только пути без замены данных узла. `node_registry` и разделяется между группами через `group_ref_count`. Per-group данные — в
- **Защита от петель**: NODEINFO рассылается только тем пирам, чей node_id отсутствует во всех путях (hop_list). WITHDRAW broadcast всем кроме отправителя. `TOPO_GROUP_NODE` (paths, subnets, connectivity, conn_presence/up).
- **Связность узлов**: Для новых/обновлённых узлов с адресами автоматически запускается зондирование (`route_connectivity_probe_node`). При удалении узла (withdraw/remove_conn) пробы отменяются (`route_connectivity_cancel_node`). - **Версионирование NODEINFO**: `ver` (uint8_t) сравнивается циклически `(int8_t)(cur - new) >= 0`;
- **Виртуальные каналы**: При удалении узла закрываются все виртуальные ETCP-каналы через `etcp_router_conn_close_all_for_node`. stale-версия только обновляет путь, не заменяя данные узла.
- **Dirty-флаг**: При withdraw/remove_conn узлы помечаются `dirty = 1` перед удалением. - **Верификация идентичности**: `node_id == derive(x25519)` + Ed25519 self-signature над каноническим
- **NAT check для локальных сетей**: По умолчанию запрещён (`allow_nat_check_local=0`), можно включить через `topo_group_set_nat_check_local(group, 1)` для тестов (делегирует в `nat_detection_set_allow_local`). сообщением (`topo_node_build_sig_msg`). Несовпадение → пакет отвергается как forgery.
- **Conn_mgr поля**: `conn_mgr_type`, `conn_mgr_intermediaries[]`, `conn_mgr_intermediariy_count` — используются менеджером соединений для стратегии установки связи с узлом (direct/reverse/indirect). - **Защита от петель**: NODEINFO рассылается пирам, чей node_id отсутствует в hop_list всех путей;
- **Обновление local_node при NAT-детекции**: Модуль `nat_detection` после определения типа NAT вызывает `topo_group_update_my_nodeinfo()` для обновления local_node с новыми данными сокетов — это автоматически увеличивает версию, и NODEINFO broadcast-ится всем пирам через механизм изменения конфигурации сокетов. WITHDRAW broadcast всем, кроме отправителя.
- **Hop-лист хранится в пути**: `TOPO_NODEPATH` содержит inline `hop_list[hop_count]` сразу после
(End of file - total 302 lines) структуры; `next_hop` для recovery вычисляется из него.
- **Связность**: для новых узлов с адресами запускается `route_connectivity_probe_node`; при
удалении — `route_connectivity_cancel_node`. В standby пробы откладываются (`PROBE_STATUS_DEFERRED`).
- **Виртуальные каналы**: при удалении узла закрываются `etcp_router_conn_close_all_for_node`.
- **CHAT-группа пишет плейсхолдер** `source=1` (pubkeys из BGP, без подписей) — вне merkle-хеша;
полная запись приходит позже через merkle-путь (member_sync).
- **BGP не удаляет member из SQLite**: работает только со своей RAM-таблицей; удаление из `peers_*`
— только битая подпись (member_sync).
- **NAT check для локальных сетей**: по умолчанию запрещён (`allow_nat_check_local=0`), включается
для тестов через `topo_group_set_nat_check_local`.
- **Обновление local_node при NAT/active-изменениях**: `nat_detection` и `topo_group_on_activity_change`
вызывают `topo_group_update_my_nodeinfo` → новая версия → NODEINFO broadcast всем пирам.

535
src/routing_layer/topo_group_invite.c

@ -0,0 +1,535 @@
/**
* @file topo_group_invite.c
* @brief Invite/join к каналу через прямое (ncd) подключение + проверка членства.
*
* Перенесено из conn_mgr (INVITE_INFO_REQ/RESP) — conn_mgr теперь только медиа.
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "../lib/platform_compat.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/memory_pool.h"
#include "../lib/u_async.h"
#include "utun_instance.h"
#include "etcp_api.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "topo_node.h"
#include "topo_node_sqlite.h"
#include "topo_group.h"
#include "topo_group_invite.h"
#include "../transport_layer/node_conn_direct.h"
#include "ntp_time.h"
#include "../chat/chat_core.h"
#define TGI_ID "topo_group_invite"
#define TGI_TIMEOUT_MS 2000
/* wire-формат INVITE_INFO_RESP (devel — без обратной совместимости):
*
* [0..1] cmd, subcmd
* [2..9] group_id
* [10] is_member
* [11..13] reserved
* [14..45] ed25519_pubkey
* [46] node_name_len
* [47..N] node_name
*
* Если is_member == 1 — далее полная информация о канале:
* [N+1] ch_name_len
* [N+2..] ch_name
* +8 owner
* +32 ch_x25519
* +32 ch_ed_pub
* +64 ch_sig
* +1 inv_flags
* [if PEERS_FLAG_HAS_JOIN: +64 join_sig + 8 join_ts]
* +64 update_sig
* +8 update_ts
* +1 userinfo_len
* +var userinfo
*/
#pragma pack(push, 1)
struct TGI_INVITE_REQ { uint8_t cmd, subcmd; uint64_t group_id; };
struct TGI_INVITE_RESP { uint8_t cmd, subcmd; uint64_t group_id; uint8_t is_member; uint8_t reserved[3]; uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; uint8_t node_name_len; uint8_t node_name[0]; };
#pragma pack(pop)
#define TGI_INVITE_RESP_HDR_SIZE (sizeof(struct TGI_INVITE_RESP))
#define TGI_INVITE_RESP_CH_MIN_SIZE (1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1)
enum { TGI_CONNECTING = 0, TGI_WAIT_INFO = 1 };
struct tgi_pending {
struct tgi_pending* next;
struct UTUN_INSTANCE* inst;
uint64_t node_id, group_id;
uint8_t state;
uint8_t cancelled;
void* overall_timer;
struct NODE_CONN_DIRECT* ncd_handle;
struct TOPO_GROUP_NODE* temp_nq;
tgi_cb_t cb; void* cb_arg;
};
/* глобальный список pending-invite (invite — редкая одиночная операция) */
static struct tgi_pending* g_tgi_list = NULL;
static void tgi_cancel(struct tgi_pending* p);
static void tgi_fail(struct tgi_pending* p);
static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len);
static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len);
static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg);
static void tgi_unlink(struct tgi_pending* p) {
struct tgi_pending** pp = &g_tgi_list;
while (*pp) { if (*pp == p) { *pp = p->next; return; } pp = &(*pp)->next; }
}
static struct tgi_pending* tgi_find_waiting(uint64_t node_id) {
for (struct tgi_pending* p = g_tgi_list; p; p = p->next)
if (p->state == TGI_WAIT_INFO && p->node_id == node_id) return p;
return NULL;
}
/* ═══════════ приём пакетов ═══════════ */
void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!conn || !entry || entry->len < 2) {
if (entry) { if (entry->dgram) queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
uint8_t* d = entry->dgram;
if (d[0] != ETCP_RT_ID_GROUP_INVITE) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint8_t sub = d[1];
if (sub == TGI_SUBCMD_INFO_REQ) tgi_handle_info_req(conn, d, entry->len);
else if (sub == TGI_SUBCMD_INFO_RESP) tgi_handle_info_resp(conn, d, entry->len);
queue_dgram_free(entry); queue_entry_free(entry);
}
/* Инвайтера сторона: INVITE_INFO_REQ → проверка членства → INVITE_INFO_RESP */
static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len) {
if (len < sizeof(struct TGI_INVITE_REQ)) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ too short (%zu)", len); return; }
struct TGI_INVITE_REQ* req = (struct TGI_INVITE_REQ*)data;
struct UTUN_INSTANCE* inst = conn->instance;
if (!inst || !inst->topo_groups) return;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ from=%016llx group=%016llx",
(unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id);
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)req->group_id);
struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, req->group_id);
struct TOPO_GROUP* sg = grp ? grp : topo_groups_get_default(inst->topo_groups);
struct TOPO_NODE* ln = sg && sg->local_node ? topo_node_registry_find(inst->topo_groups, sg->local_node->node_id) : NULL;
const char* nm = ln ? ln->node_name : NULL; size_t nl = nm ? strlen(nm) : 0; if (nl > 255) nl = 255;
sqlite3* db = inst->topo_sqlite_db;
char ch_name[128] = ""; uint64_t owner = 0;
uint8_t ch_x25519[32] = {0}, ch_ed_pub[32] = {0}, ch_sig[64] = {0};
int have_channel = db && topo_node_sqlite_channel_get(db, ch_id, ch_name, (int)sizeof(ch_name),
&owner, ch_x25519, ch_ed_pub, ch_sig) == 0;
if (!have_channel) {
size_t rs = TGI_INVITE_RESP_HDR_SIZE + nl;
uint8_t* rb = u_calloc(1, rs); if (!rb) return;
struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)rb;
resp->cmd = ETCP_RT_ID_GROUP_INVITE; resp->subcmd = TGI_SUBCMD_INFO_RESP;
resp->group_id = req->group_id; resp->is_member = 0;
if (sg) memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE);
resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl);
struct ll_entry* qe = queue_entry_new(0);
if (qe) { qe->dgram = rb; qe->len = (uint16_t)rs; etcp_send(conn, qe); } else u_free(rb);
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: group=%016llx NOT in DB — responding is_member=NO to 0x%016llx",
(unsigned long long)req->group_id, (unsigned long long)conn->peer_node_id);
return;
}
/* ── участник: генерируем join_sig / update_sig ── */
uint64_t myid = inst->node_id;
uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0;
{
if (db && topo_node_sqlite_member_get_join(db, ch_id, myid, my_join_sig, &my_join_ts) != 0) {
uint8_t join_msg[256]; size_t mlen = 0;
memcpy(join_msg + mlen, ch_x25519, 32); mlen += 32;
memcpy(join_msg + mlen, ch_ed_pub, 32); mlen += 32;
memcpy(join_msg + mlen, &myid, 8); mlen += 8;
memcpy(join_msg + mlen, inst->my_keys.public_key, 32); mlen += 32;
my_join_ts = (uint64_t)ntp_time_get_seconds(inst);
memcpy(join_msg + mlen, &my_join_ts, 8); mlen += 8;
if (sc_ed25519_sign(inst->my_ed25519_privkey, join_msg, mlen, my_join_sig) != SC_OK)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: join_sig sign FAILED myid=%016llx ch=%s", (unsigned long long)myid, ch_id);
else
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig NEW myid=%016llx ts=%llu ch=%s",
(unsigned long long)myid, (unsigned long long)my_join_ts, ch_id);
}
}
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts = 0;
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}",
inst->name[0] ? inst->name : "");
uint8_t umsg[256]; size_t ulen = 0;
memcpy(umsg + ulen, my_join_sig, 64); ulen += 64;
my_update_ts = (uint64_t)ntp_time_get_seconds(inst);
memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8;
size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1;
if (sc_ed25519_sign(inst->my_ed25519_privkey, umsg, ulen, my_update_sig) != SC_OK)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: update_sig sign FAILED myid=%016llx", (unsigned long long)myid);
}
/* ── создаём инфраструктуру группы СРАЗУ ── */
chat_core_ensure_channel_ready(ch_id);
/* ── сериализуем ответ ── */
size_t ch_nl = strlen(ch_name); if (ch_nl > 255) ch_nl = 255;
uint8_t inv_flags = (my_join_sig[0] || my_join_ts) ? PEERS_FLAG_HAS_JOIN : 0;
char userinfo[256]; size_t userinfo_len = (size_t)snprintf(userinfo, sizeof(userinfo),
"{\"name\":\"%s\"}", inst->name[0] ? inst->name : "");
size_t ch_info_len = 1 + ch_nl + 8 + 32 + 32 + 64 + 1
+ (inv_flags & PEERS_FLAG_HAS_JOIN ? 72 : 0)
+ 64 + 8 + 1 + userinfo_len;
size_t rs = TGI_INVITE_RESP_HDR_SIZE + nl + ch_info_len;
uint8_t* rb = u_malloc(rs); if (!rb) return;
memset(rb, 0, rs);
{ struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)rb;
resp->cmd = ETCP_RT_ID_GROUP_INVITE; resp->subcmd = TGI_SUBCMD_INFO_RESP;
resp->group_id = req->group_id; resp->is_member = 1;
if (sg) memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE);
resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); }
uint8_t* ch = rb + TGI_INVITE_RESP_HDR_SIZE + nl;
*ch++ = (uint8_t)ch_nl; memcpy(ch, ch_name, ch_nl); ch += ch_nl;
memcpy(ch, &owner, 8); ch += 8;
memcpy(ch, ch_x25519, 32); ch += 32;
memcpy(ch, ch_ed_pub, 32); ch += 32;
memcpy(ch, ch_sig, 64); ch += 64;
*ch++ = inv_flags;
if (inv_flags & PEERS_FLAG_HAS_JOIN) { memcpy(ch, my_join_sig, 64); ch += 64; memcpy(ch, &my_join_ts, 8); ch += 8; }
memcpy(ch, my_update_sig, 64); ch += 64;
memcpy(ch, &my_update_ts, 8); ch += 8;
*ch++ = (uint8_t)userinfo_len; memcpy(ch, userinfo, userinfo_len);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"invite: membership CHECK OK from=0x%016llx group=%016llx ch_name=%.*s owner=%016llx inv_flags=%u → is_member=YES (%zuB)",
(unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id,
(int)ch_nl, ch_name, (unsigned long long)owner, (unsigned)inv_flags, rs);
{ struct ll_entry* qe = queue_entry_new(0);
if (qe) { qe->dgram = rb; qe->len = (uint16_t)rs; etcp_send(conn, qe); } else u_free(rb);
}
}
/* Джойнер: INVITE_INFO_RESP → верификация ch_sig → создать канал → TGI_EVENT_JOIN */
static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len) {
if (len < TGI_INVITE_RESP_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP too short (%zu < %zu)", len, TGI_INVITE_RESP_HDR_SIZE); return; }
struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)data;
uint8_t nnl = resp->node_name_len;
if (len < TGI_INVITE_RESP_HDR_SIZE + (size_t)nnl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP truncated"); return; }
struct tgi_pending* inv = tgi_find_waiting(conn->peer_node_id);
if (!inv) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP — no pending invite in WAIT_INFO state"); return; }
if (!resp->is_member) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: membership DENIED by 0x%016llx — not in channel", (unsigned long long)inv->node_id);
tgi_fail(inv); return;
}
/* ── сохраняем Ed25519 pubkey ответчика для NODEINFO-верификации ── */
if (inv->temp_nq) {
struct TOPO_NODE* ni2 = topo_node_registry_find(inv->inst->topo_groups, inv->temp_nq->node_id);
if (ni2) memcpy(ni2->ed25519_public_key, resp->ed25519_pubkey, SC_PUBKEY_SIZE);
}
/* ── парсим channel_info ── */
const uint8_t* ch = data + TGI_INVITE_RESP_HDR_SIZE + nnl;
size_t ch_remain = len - (TGI_INVITE_RESP_HDR_SIZE + nnl);
if (ch_remain < TGI_INVITE_RESP_CH_MIN_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP channel info too short"); tgi_fail(inv); return; }
uint8_t c_nl = *ch++; ch_remain--;
if (ch_remain < (size_t)c_nl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_name truncated"); tgi_fail(inv); return; }
char ch_name[128]; size_t cn = c_nl > 127 ? 127 : c_nl; memcpy(ch_name, ch, cn); ch_name[cn] = '\0'; ch += c_nl; ch_remain -= c_nl;
uint64_t ch_owner; memcpy(&ch_owner, ch, 8); ch += 8; ch_remain -= 8;
const uint8_t* ch_x25519 = ch; ch += 32; ch_remain -= 32;
const uint8_t* ch_ed_pub = ch; ch += 32; ch_remain -= 32;
const uint8_t* ch_sig = ch; ch += 64; ch_remain -= 64;
if (ch_remain < 1) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at inv_flags"); tgi_fail(inv); return; }
uint8_t inv_flags = *ch++; ch_remain--;
const uint8_t* join_sig = NULL; uint64_t join_ts = 0;
if (inv_flags & PEERS_FLAG_HAS_JOIN) {
if (ch_remain < 72) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at join_sig"); tgi_fail(inv); return; }
join_sig = ch; ch += 64; memcpy(&join_ts, ch, 8); ch += 8; ch_remain -= 72;
}
if (ch_remain < 73) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at update"); tgi_fail(inv); return; }
const uint8_t* upd_sig = ch; ch += 64; uint64_t upd_ts; memcpy(&upd_ts, ch, 8); ch += 8; ch_remain -= 72;
uint8_t ui_len = *ch++; ch_remain--;
char ch_userinfo[256] = "";
if (ui_len > 0 && ch_remain >= ui_len) { size_t ul = ui_len > 255 ? 255 : ui_len; memcpy(ch_userinfo, ch, ul); ch_userinfo[ul] = '\0'; }
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)resp->group_id);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"invite: INFO_RESP from=0x%016llx group=%016llx ch_name=%s ch_owner=%016llx inv_flags=%u join_ts=%llu upd_ts=%llu userinfo=%s",
(unsigned long long)conn->peer_node_id, (unsigned long long)resp->group_id,
ch_name, (unsigned long long)ch_owner, (unsigned)inv_flags,
(unsigned long long)join_ts, (unsigned long long)upd_ts,
ch_userinfo[0] ? ch_userinfo : "?");
/* ── верифицируем ch_sig ── */
{
uint8_t vmsg[1024]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
vlen += (size_t)snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_name) + 1;
memcpy(vmsg + vlen, &ch_owner, 8); vlen += 8;
memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed_pub, 32); vlen += 32;
if (sc_ed25519_verify(ch_ed_pub, vmsg, vlen, ch_sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY FAIL — rejecting group=%016llx from=0x%016llx",
(unsigned long long)resp->group_id, (unsigned long long)conn->peer_node_id);
tgi_fail(inv); return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY OK");
}
/* ── сохраняем канал и создаём группу ── */
{ sqlite3* ldb = inv->inst->topo_sqlite_db;
if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner,
ch_x25519, NULL, ch_ed_pub, NULL, ch_sig);
}
chat_core_ensure_channel_ready(ch_id);
/* ── верифицируем update_sig приглашающего ── */
{
const uint8_t* inv_ed = conn->peer_ed25519_pubkey;
uint8_t ivmsg[256]; size_t ilen = 0;
if (join_sig && (inv_flags & PEERS_FLAG_HAS_JOIN)) { memcpy(ivmsg + ilen, join_sig, 64); ilen += 64; }
else { memset(ivmsg + ilen, 0, 64); ilen += 64; }
memcpy(ivmsg + ilen, &upd_ts, 8); ilen += 8;
{ const char* nm2 = ch_userinfo[0] ? ch_userinfo : "";
size_t nl2 = strlen(nm2); memcpy(ivmsg + ilen, nm2, nl2 + 1); ilen += nl2 + 1; }
if (sc_ed25519_verify(inv_ed, ivmsg, ilen, upd_sig) != SC_OK) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY FAIL node=0x%016llx", (unsigned long long)conn->peer_node_id);
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY OK");
}
}
if (inv->overall_timer) { uasync_cancel_timeout(inv->inst->ua, inv->overall_timer); inv->overall_timer = NULL; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" ch=%s — full channel ready",
(unsigned long long)inv->node_id, (int)nnl, nnl ? (const char*)resp->node_name : "?", ch_id);
/* ── успех: переводим ncd-handle во владение узла, доставляем JOIN ── */
{
uint64_t node_id = inv->node_id, group_id = inv->group_id;
tgi_cb_t cb = inv->cb; void* cb_arg = inv->cb_arg;
if (inv->ncd_handle) node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL);
struct TOPO_GROUP* g = topo_groups_find(inv->inst->topo_groups, group_id);
struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, node_id) : NULL;
if (nq) nq->handle = inv->ncd_handle;
inv->ncd_handle = NULL; /* не закрываем — соединение переходит узлу */
inv->temp_nq = NULL; /* не удаляем узел из группы */
tgi_cancel(inv);
if (cb) cb(node_id, group_id, TGI_EVENT_JOIN, cb_arg);
}
}
/* ═══════════ ncd-коллбэк (джойнер) ═══════════ */
static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) {
struct tgi_pending* p = (struct tgi_pending*)arg;
if (!p || p->cancelled) return;
if (ev == NCD_EVENT_UP) {
if (p->state != TGI_CONNECTING) return;
struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
if (!conn) { tgi_fail(p); return; }
p->state = TGI_WAIT_INFO;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: connected to 0x%016llx — sending membership check", (unsigned long long)p->node_id);
struct TGI_INVITE_REQ req;
req.cmd = ETCP_RT_ID_GROUP_INVITE; req.subcmd = TGI_SUBCMD_INFO_REQ; req.group_id = p->group_id;
struct ll_entry* qe = queue_entry_new(0);
if (!qe) { tgi_fail(p); return; }
qe->dgram = u_malloc(sizeof(req));
if (!qe->dgram) { queue_entry_free(qe); tgi_fail(p); return; }
memcpy(qe->dgram, &req, sizeof(req)); qe->len = sizeof(req);
if (etcp_send(conn, qe) != 0) { u_free(qe->dgram); queue_entry_free(qe); tgi_fail(p); }
} else if (ev == NCD_EVENT_TIMEOUT) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ncd TIMEOUT connecting to 0x%016llx", (unsigned long long)p->node_id);
tgi_fail(p);
}
}
static void tgi_overall_timeout(void* arg) {
struct tgi_pending* p = (struct tgi_pending*)arg;
if (p->cancelled) return;
p->overall_timer = NULL;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: overall TIMEOUT to 0x%016llx — no response within %dms",
(unsigned long long)p->node_id, TGI_TIMEOUT_MS);
tgi_fail(p);
}
/* ═══════════ отмена / fail ═══════════ */
static void tgi_cancel(struct tgi_pending* p) {
if (!p) return;
p->cancelled = 1;
if (p->overall_timer) { uasync_cancel_timeout(p->inst->ua, p->overall_timer); p->overall_timer = NULL; }
if (p->ncd_handle) { node_conn_direct_force_close(p->ncd_handle); p->ncd_handle = NULL; }
if (p->temp_nq) {
struct TOPO_GROUP* g = topo_groups_find(p->inst->topo_groups, p->group_id);
if (g) { queue_remove_data(g->nodes, &p->temp_nq->ll); topo_nodeq_free_group_fields(p->inst->topo_groups, p->temp_nq); queue_entry_free(&p->temp_nq->ll); }
p->temp_nq = NULL;
}
tgi_unlink(p);
u_free(p);
}
static void tgi_fail(struct tgi_pending* p) {
if (!p || p->cancelled) return;
uint64_t node_id = p->node_id, group_id = p->group_id;
tgi_cb_t cb = p->cb; void* cb_arg = p->cb_arg;
tgi_cancel(p);
if (cb) cb(node_id, group_id, TGI_EVENT_TIMEOUT, cb_arg);
}
void topo_group_invite_cancel_all(struct UTUN_INSTANCE* inst) {
struct tgi_pending* p = g_tgi_list;
while (p) {
struct tgi_pending* next = p->next;
if (!inst || p->inst == inst) tgi_cancel(p);
p = next;
}
}
/* ═══════════ инициализация ═══════════ */
void topo_group_invite_init(struct UTUN_INSTANCE* inst) {
if (!inst) return;
etcp_bind(inst, ETCP_RT_ID_GROUP_INVITE, topo_group_invite_recv_handler);
}
/* ═══════════ входная точка джойнера ═══════════ */
int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id,
struct TOPO_NODE* ni, uint64_t node_id,
tgi_cb_t cb, void* cb_arg) {
if (!inst || !inst->topo_groups || !cb || !ni) return -1;
/* 1. Найти или создать группу */
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
if (!group) {
char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id);
group = topo_groups_create_group(inst->topo_groups, group_id, TOPO_GROUP_TYPE_CHAT, ch);
if (!group) return -1;
}
uint64_t nid = node_id;
if (!nid || nid == inst->node_id) return -1;
if (!ni->v4_addrs && !ni->v6_addrs) return -1;
if (!ni->public_key[0]) return -1;
/* 2. Копировать ni в group_ni */
struct TOPO_GROUPS* groups = inst->topo_groups;
struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1;
gni->node_id = nid; gni->ver = 1; gni->group_id = group_id; memcpy(gni->public_key, ni->public_key, SC_PUBKEY_SIZE);
for (const struct TOPO_ADDR4* src = ni->v4_addrs; src; src = src->next) {
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); if (!a4) continue;
memset(a4, 0, sizeof(*a4)); memcpy(a4->addr, src->addr, 4); a4->port = src->port;
a4->type = TOPO_ADDR_NAT; a4->protocol = src->protocol; a4->next = gni->v4_addrs; gni->v4_addrs = a4;
}
for (const struct TOPO_ADDR6* src6 = ni->v6_addrs; src6; src6 = src6->next) {
struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); if (!a6) continue;
memset(a6, 0, sizeof(*a6)); memcpy(a6->addr, src6->addr, 16); a6->port = src6->port;
a6->type = TOPO_ADDR_NAT; a6->protocol = src6->protocol; a6->next = gni->v6_addrs; gni->v6_addrs = a6;
}
if (!gni->v4_addrs && !gni->v6_addrs) { u_free(gni); return -1; }
gni = topo_node_registry_store(groups, gni); if (!gni) return -1;
/* 3. TOPO_GROUP_NODE + pending */
struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE));
if (!qe) { topo_node_registry_unref(groups, gni->node_id); return -1; }
struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)qe;
memset((uint8_t*)nq + sizeof(struct ll_entry), 0, sizeof(*nq) - sizeof(struct ll_entry)); nq->node_id = nid;
struct tgi_pending* p = u_calloc(1, sizeof(*p));
if (!p) { queue_entry_free(qe); topo_node_registry_unref(groups, gni->node_id); return -1; }
p->inst = inst; p->node_id = nid; p->group_id = group_id; p->state = TGI_CONNECTING;
p->temp_nq = nq; p->cb = cb; p->cb_arg = cb_arg;
p->next = g_tgi_list; g_tgi_list = p;
queue_data_put_with_index(group->nodes, &nq->ll);
/* 4. прямое подключение (ncd) */
if (node_conn_direct_open_node(inst, nid, tgi_ncd_cb, p, &p->ncd_handle, gni, NULL) == NCD_ERR) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ncd open failed for 0x%016llx", (unsigned long long)nid);
tgi_fail(p); return -1;
}
/* 5. лог адресов */
{
char addrs[512] = ""; int aoff = 0;
for (const struct TOPO_ADDR4* a = gni->v4_addrs; a; a = a->next)
aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %d.%d.%d.%d:%d(%s)",
a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port,
a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : "?");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%dms",
(unsigned long long)nid, addrs, TGI_TIMEOUT_MS);
}
p->overall_timer = uasync_set_timeout(inst->ua, TGI_TIMEOUT_MS * 10, p, tgi_overall_timeout, "tgi_overall");
return 0;
}
/* ═══════════ инвайтера сторона (direct, без INVITE_INFO) ═══════════ */
struct tgi_conn_ctx { struct UTUN_INSTANCE* inst; uint64_t node_id, group_id; };
static void tgi_to_channel_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) {
struct tgi_conn_ctx* c = (struct tgi_conn_ctx*)arg;
if (ev == NCD_EVENT_UP) {
struct TOPO_GROUP* g = topo_groups_find(c->inst->topo_groups, c->group_id);
struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, c->node_id) : NULL;
if (nq) nq->handle = h;
node_conn_direct_set_callback(h, NULL, NULL);
u_free(c);
} else if (ev == NCD_EVENT_TIMEOUT) {
u_free(c);
}
}
int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id,
struct TOPO_NODE* ni, uint64_t node_id) {
if (!inst || !inst->topo_groups) return -1;
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
if (!group) {
char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id);
group = topo_groups_create_group(inst->topo_groups, group_id, TOPO_GROUP_TYPE_CHAT, ch);
if (!group) return -1;
}
if (!node_id || node_id == inst->node_id) return -1;
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id);
if (!nq) {
struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE));
if (!qe) return -1;
nq = (struct TOPO_GROUP_NODE*)qe;
memset((uint8_t*)nq + sizeof(struct ll_entry), 0, sizeof(*nq) - sizeof(struct ll_entry));
nq->node_id = node_id;
queue_data_put_with_index(group->nodes, &nq->ll);
}
struct tgi_conn_ctx* c = u_calloc(1, sizeof(*c));
if (!c) return -1;
c->inst = inst; c->node_id = node_id; c->group_id = group_id;
struct NODE_CONN_DIRECT* h = NULL;
int r = ni ? node_conn_direct_open_node(inst, node_id, tgi_to_channel_cb, c, &h, ni, NULL)
: node_conn_direct_open(inst, node_id, tgi_to_channel_cb, c, &h, NULL);
if (r == NCD_ERR || !h) { u_free(c); return -1; }
return 0;
}

73
src/routing_layer/topo_group_invite.h

@ -0,0 +1,73 @@
/**
* @file topo_group_invite.h
* @brief Invite/join к каналу через прямое (ncd) подключение.
*
* Модуль содержит всю логику работы с invite-ссылками: прямое подключение к
* приглашающему узлу через node_conn_direct (ncd) и проверку членства
* (INVITE_INFO_REQ/RESP) с бутстрапом криптографической идентичности канала.
*
* Поток джойнера (topo_group_invite_join):
* 1. Создаёт/находит группу, сохраняет TOPO_NODE приглашающего.
* 2. Открывает прямое соединение (ncd) и шлёт INVITE_INFO_REQ.
* 3. Получает INVITE_INFO_RESP — проверяет ch_sig, сохраняет канал в БД,
* поднимает инфраструктуру группы (chat_core_ensure_channel_ready).
* 4. Доставляет TGI_EVENT_JOIN.
*
* Инвайтера сторона: tgi_handle_info_req отвечает на INVITE_INFO_REQ —
* проверяет членство в БД и возвращает полную информацию о канале.
*
* conn_mgr при этом используется ТОЛЬКО для медиа — invite больше от него
* не зависит.
*/
#ifndef TOPO_GROUP_INVITE_H
#define TOPO_GROUP_INVITE_H
#include <stdint.h>
#include "topo_node.h"
#include "secure_channel.h"
struct UTUN_INSTANCE;
struct ETCP_CONN;
struct ll_entry;
struct NODE_CONN_DIRECT;
/* service id протокола invite (etcp_bind) */
#define ETCP_RT_ID_GROUP_INVITE 0x13
#define TGI_SUBCMD_INFO_REQ 0x01
#define TGI_SUBCMD_INFO_RESP 0x02
/* события коллбэка джойнера */
#define TGI_EVENT_JOIN 0
#define TGI_EVENT_TIMEOUT 1
typedef void (*tgi_cb_t)(uint64_t node_id, uint64_t group_id, int event, void* arg);
/** Идемпотентная инициализация: биндит обработчик ETCP_RT_ID_GROUP_INVITE. */
void topo_group_invite_init(struct UTUN_INSTANCE* inst);
/**
* Джойнер: прямое подключение (ncd) к приглашающему + проверка членства.
* ni — TOPO_NODE приглашающего (pubkey + адреса), не владеем.
* При успехе вызывается cb(TGI_EVENT_JOIN), при провале cb(TGI_EVENT_TIMEOUT).
*/
int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id,
struct TOPO_NODE* ni, uint64_t node_id,
tgi_cb_t cb, void* cb_arg);
/**
* Инвайтера сторона: прямое подключение (ncd) к целевому узлу для последующей
* отправки CHANNEL_INVITE (через chat_sync, по ETCP_CONN_STATUS_UP).
* Без INVITE_INFO-проверки членства. ni может быть NULL — тогда адреса грузятся
* из БД (node_conn_direct_open). Handle сохраняется в nq->handle.
*/
int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id,
struct TOPO_NODE* ni, uint64_t node_id);
/** Отменяет все pending-invite (teardown). */
void topo_group_invite_cancel_all(struct UTUN_INSTANCE* inst);
/** Обработчик приёма пакетов invite (etcp_bind). */
void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
#endif

4
src/routing_layer/topo_node.h

@ -33,7 +33,7 @@ extern "C" {
struct TOPO_GROUP; struct TOPO_GROUP;
struct TOPO_GROUPS; struct TOPO_GROUPS;
struct ETCP_SOCKET; struct ETCP_SOCKET;
struct CONN_MGR_HANDLE; struct NODE_CONN_DIRECT;
struct ETCP_CONN; struct ETCP_CONN;
struct UTUN_INSTANCE; struct UTUN_INSTANCE;
@ -176,7 +176,7 @@ struct TOPO_GROUP_NODE {
struct TOPO_CONNECTIVITY connectivity; // результаты ping-проб (interface/nat/real) struct TOPO_CONNECTIVITY connectivity; // результаты ping-проб (interface/nat/real)
uint8_t conn_presence; // NODE_CONN_* — какие подключения есть в принципе uint8_t conn_presence; // NODE_CONN_* — какие подключения есть в принципе
uint8_t conn_up; // NODE_CONN_* — какие из них подняты uint8_t conn_up; // NODE_CONN_* — какие из них подняты
struct CONN_MGR_HANDLE* handle; // активный хендл соединения к узлу struct NODE_CONN_DIRECT* handle; // активный ncd-handle прямого соединения к узлу
}; };
// API — глобальный реестр TOPO_NODE // API — глобальный реестр TOPO_NODE

208
src/routing_layer/topo_node_doc.md

@ -2,52 +2,64 @@
## 1. Назначение ## 1. Назначение
Модель данных узла топологии — структуры данных для представления узла в памяти, сериализации/десериализации в бинарный wire-формат для BGP-обмена, учёт ссылок (refcounting) и диагностический дамп. Модель данных узла топологии — структуры для представления узла в памяти, сериализации/
десериализации в бинарный wire-формат для BGP-обмена, глобальный реестр идентичностей
(refcounting) и диагностический дамп.
Модуль определяет два слоя представлений: Модуль определяет два слоя:
- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) для удобной модификации. - **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) и две
- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети без выравнивания. структуры узла — `TOPO_NODE` (глобальная идентичность) и `TOPO_GROUP_NODE` (per-group данные).
- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети.
Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только данные и их упаковка/распаковка. Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только
данные и их упаковка/распаковка. Персистентность — в `topo_node_sqlite`.
Узлы персистентно хранятся в SQLite через модуль `topo_node_sqlite`. Память для элементов списков (адреса, сокет-мета, подсети) выделяется из memory pool'ов из `topo_groups`. Два уровня представления узла:
- **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. Как пользоваться ## 2. Как пользоваться
### Типовые сценарии ### Типовые сценарии
**Создание/обновление информации о себе:** **Сборка информации о себе:**
```c ```c
topo_group_update_my_nodeinfo(instance, group); topo_group_update_my_nodeinfo(instance, group);
``` ```
Строит `TOPO_NODEQ` (в `group->local_node`) из конфига: собирает список сокетов, адресов (interface/NAT/real), подсетей; при изменениях инкрементирует `ver` и помечает `dirty=1`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети не включаются. Строит `group->local_node` (`TOPO_GROUP_NODE`) из конфига: сокеты, адреса (interface/NAT/real),
подсети; при изменениях инкрементирует `ver`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети
не включаются.
**Сериализация для отправки по BGP:** **Сериализация для отправки по BGP:**
```c ```c
uint8_t buf[2048]; struct TOPO_NODE* ni = topo_node_registry_find(groups, nq->node_id);
int len = topo_node_serialize(group, nq, buf, sizeof(buf)); uint8_t buf[4096];
int len = topo_node_serialize(ni, nq, buf, sizeof(buf), cumulative_rtt);
``` ```
Запаковывает `TOPO_NODEQ` → бинарный буфер: заголовок `TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если `TOPO_FLAG_SEND_SUBNETS`), транзитные узлы и hop-лист. Запаковывает `TOPO_NODE` + per-group данные (`TOPO_GROUP_NODE`) в буфер: заголовок
`TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если
`TOPO_FLAG_SEND_SUBNETS`) и hop-лист (через `topo_node_best_hop_list`).
**Десериализация при получении NODEINFO:** **Десериализация при получении NODEINFO:**
```c ```c
struct TOPO_NODE *ni = NULL; struct TOPO_NODE* ni = NULL;
struct TOPO_NODESUBNETS *subnets = NULL; struct TOPO_NODESUBNETS* subnets = NULL;
struct TOPOMSG_TRANZIT *tranzit = NULL; uint64_t* hop_list = NULL; uint8_t hop_count = 0;
uint8_t tranzit_count = 0; uint16_t cumulative_rtt = 0;
uint64_t *hop_list = NULL; topo_node_deserialize(group, data, data_len, &ni, &subnets, &hop_list, &hop_count, &cumulative_rtt);
uint8_t hop_count = 0;
topo_node_deserialize(group, data, data_len, &ni, &subnets, &tranzit, &tranzit_count, &hop_list, &hop_count);
``` ```
Восстанавливает `TOPO_NODE`, подсети, транзит и hop-лист из бинарного буфера. Элементы списков аллоцируются из memory pool'ов. Выделенная память должна быть освобождена: Восстанавливает `TOPO_NODE` (через `u_calloc`), подсети и hop-лист. Освобождение:
- `topo_node_unref(ni)` — освобождает `TOPO_NODE` и `node_name` - `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя).
- `topo_node_free_lists(group, nq)` — освобождает списки обратно в пулы, а также `subnets`, `tranzit_data`, `hop_list` - `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (`subnets`, `paths`-элементы).
**Поиск узла по ID:** **Поиск узла по ID:**
```c ```c
struct TOPO_NODEQ *nq = topo_node_find_by_id(group, node_id); 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); // глобальная идентичность
``` ```
Использует хеш-индекс очереди `group->nodes` (ключ — `node_id`, 8 байт).
**Дамп всех узлов:** **Дамп всех узлов:**
```c ```c
@ -57,21 +69,21 @@ topo_node_format_all(group, buf, size); // в строку
### Ключевые концепции ### Ключевые концепции
- **TOPO_NODE** — базовые данные узла: pubkey (X25519), ed25519_pubkey, имя, версия, групповой ID. Разделяется между группами через счётчик ссылок. - **Глобальный реестр**: `TOPO_NODE` хранится один раз в `node_registry`; `topo_node_registry_store`
дедуплицирует по `node_id` (возвращает существующий при совпадении). `registry_ref/unref`
- **TOPO_NODEQ** — запись в очереди узлов группы. Содержит `TOPO_NODE*` (общий), плюс динамические пути, подсети, транзитные узлы, hop-лист, связность, conn_mgr. Все поля аллоцированы отдельно и освобождаются через `topo_node_free_lists()`. управляют `group_ref_count`; при 0 — `topo_node_destroy`.
- **Путь**: `TOPO_NODEPATH = {conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]`
- **TOPO_NODEPATH** — путь до узла через конкретное ETCP-соединение. Хранится в `paths`-очереди типа `ll_queue`. После самой структуры идёт массив `uint64_t hop[hop_count]` (variable-length). сразу после структуры. Полный hop-лист для отправки — `topo_node_best_hop_list`.
- **Self-signature**: `x25519_self_sig` — Ed25519-подпись над каноническим сообщением
- **Счётчик ссылок:** `topo_node_ref()`/`topo_node_unref()`. При падении счётчика до 0 освобождается `node_name` и сам `TOPO_NODE`. Сами списки адресов и sock_meta освобождаются отдельно через `free_lists()`. (`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` (после детекции NAT), `TOPO_ADDR_REAL` (подтверждённый прямой адрес). Для каждого сокета может быть до 3 адресов в списке. - **Адреса трёх типов**: `TOPO_ADDR_INTERFACE` (LAN), `TOPO_ADDR_NAT` (после детекции),
`TOPO_ADDR_REAL` (подтверждённый прямой). Для каждого сокета — до 3 адресов.
- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): статус зондирования (NONE/IN_PROGRESS/DONE), результаты для interface/nat/real адресов, минимальные RTT, время зондирования. - **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): probe_status
(NONE/IN_PROGRESS/DONE/DEFERRED), RTT и статусы interface/nat/real, `probe_deferred_wait`.
- **TOPO_FLAG_SEND_SUBNETS** — флаг в `TOPO_NODE.flags`: если установлен, подсети сериализуются и включаются в wire-формат. Для chat-групп сбрасывается в 0. - **TOPO_FLAG_SEND_SUBNETS** — если установлен, подсети сериализуются; для chat-групп сброшен.
- **Wire-протокол**: заголовок `TOPOMSG_NODE` (packed) + динамика: `node_name`, `SOCKMETA4*N`,
- **Wire-протокол:** заголовок `TOPOMSG_NODE` (75 байт packed) + динамическая часть: `node_name` (0-255 байт), `SOCKMETA4*N`, `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, транзит, hop-лист. Размер динамической части вычисляется функцией `topo_node_dyn_size()`. `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, hop-лист. Размер динамики — `topo_node_dyn_size()`.
## 3. API ## 3. API
@ -79,92 +91,88 @@ topo_node_format_all(group, buf, size); // в строку
| Структура | Назначение | | Структура | Назначение |
|-----------|-----------| |-----------|-----------|
| `struct TOPO_NODE` | Идентичность узла: pubkeys (X25519+Ed25519), имя, версия, group_id, node_id, списки v4/v6 сокет-метаданных и адресов. Счётчик ссылок. | | `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_NODEQ` | Запись в очереди группы. Содержит `TOPO_NODE*`, подсети, транзитные данные, hop-лист, очередь `paths` (TOPO_NODEPATH), связность, conn_mgr. `best_socket` — указатель на лучший ETCP-сокет для связи. | | `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-соединение с hop_count. Variable-length: после структуры идёт `uint64_t hop[]`. | | `struct TOPO_NODEPATH` | Путь через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[]` |
| `struct TOPO_NODESUBNETS` | Списки v4 и v6 подсетей, анонсируемых узлом. Отдельный malloc, может быть NULL. | | `struct TOPO_NODESUBNETS` | v4/v6 подсети узла (отдельный malloc, может быть NULL) |
| `struct TOPO_CONNECTIVITY` | Локальное состояние проверки связности: статусы зондирования, RTT для interface/nat/real адресов. | | `struct TOPO_CONNECTIVITY` | Связность: probe_status, RTT, статусы interface/nat/real, probe-тайминги |
| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата (75 байт). Содержит счётчики элементов динамической части. | | `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_ADDR4/6` | Packed wire одного адреса (addr, port, type, socket_id, protocol) |
| `struct TOPOMSG_SOCKMETA4/6` | Packed wire-формат метаданных сокета (id, config_type, nat_type). | | `struct TOPOMSG_SOCKMETA4/6` | Packed wire сокет-метаданных (id, config_type, nat_type) |
| `struct TOPOMSG_SUBNET4/6` | Packed wire-формат подсети (addr, prefix_length). | | `struct TOPOMSG_SUBNET4/6` | Packed wire подсети (addr, prefix_length) |
| `struct TOPOMSG_TRANZIT` | Packed wire-формат транзитного узла (node_id, rtt, link_q). |
### Вспомогательные типы в памяти (связанные списки) ### Вспомогательные типы в памяти (связанные списки)
| Тип | Назначение | | Тип | Назначение |
|-----|-----------| |-----|-----------|
| `struct TOPO_SOCKMETA4` | Элемент списка v4 сокет-метаданных (next*, id, config_type, nat_type). | | `struct TOPO_SOCKMETA4/6` | Элемент списка сокет-метаданных (next*, id, config_type, nat_type) |
| `struct TOPO_ADDR4` | Элемент списка v4 адресов (next*, addr[4], port, type, socket_id, protocol). | | `struct TOPO_ADDR4/6` | Элемент списка адресов (next*, addr, port, type, socket_id, protocol) |
| `struct TOPO_SOCKMETA6` | Элемент списка v6 сокет-метаданных. | | `struct TOPO_SUBNET4/6` | Элемент списка подсетей (next*, addr, prefix_length) |
| `struct TOPO_ADDR6` | Элемент списка v6 адресов. |
| `struct TOPO_SUBNET4` | Элемент списка v4 подсетей (next*, addr[4], prefix_length). |
| `struct TOPO_SUBNET6` | Элемент списка v6 подсетей. |
Все элементы списков аллоцируются из memory pool'ов и освобождаются через `memory_pool_free()`. Элементы списков аллоцируются из memory pool'ов (`topo_groups`) через `memory_pool_alloc/free`.
### Константы ### Константы
| Константа | Значение | Назначение | | Константа | Значение | Назначение |
|-----------|---------|-----------| |-----------|---------|-----------|
| `TOPO_ADDR_INTERFACE` | 0 | Интерфейсный (LAN) адрес сокета | | `TOPO_ADDR_INTERFACE` / `TOPO_ADDR_NAT` / `TOPO_ADDR_REAL` | 0/1/2 | Тип адреса |
| `TOPO_ADDR_NAT` | 1 | NAT-адрес после детекции | | `TOPO_PROTO_UDP` / `TOPO_PROTO_TCP` | 0x01/0x02 | Транспорт |
| `TOPO_ADDR_REAL` | 2 | Подтверждённый прямой адрес | | `ADDR_TYPE_NETIF/DIRECT/NAT_EIM/NAT_STRICT` | 0/1/2/3 | Классификация адреса |
| `TOPO_PROTO_UDP` | 0x01 | UDP-транспорт | | `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы |
| `TOPO_PROTO_TCP` | 0x02 | TCP-транспорт | | `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы |
| `PROBE_STATUS_NONE` | 0 | Зондирование не запускалось | | `CONN_TYPE_NONE/DIRECT/REVERSE/INDIRECT` | 0/1/2/3 | Стратегия соединения |
| `PROBE_STATUS_IN_PROGRESS` | 1 | Зондирование идёт | | `CLIENT_TYPE_SERVER/DESKTOP/MOBILE` | 0/1/2 | Тип клиента |
| `PROBE_STATUS_DONE` | 2 | Зондирование завершено | | `CLIENT_ACTIVITY_STANDBY/ACTIVE` | 0/1 | Режим |
| `PROBE_RESULT_UNKNOWN` | 0 | Результат неизвестен | | `NCONN_DIRECT/INDIRECT/BGP` | 1/2/4 | Биты наличия соединений |
| `PROBE_RESULT_REACHABLE` | 1 | Адрес достижим | | `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Отправлять подсети |
| `PROBE_RESULT_UNREACHABLE` | 2 | Адрес недостижим | | `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID utun-группы по умолчанию |
| `CONN_TYPE_DIRECT` | 1 | Прямое соединение | | `TOPO_GROUP_TYPE_UTUN/CHAT` | 1/2 | Тип группы |
| `CONN_TYPE_REVERSE` | 2 | Обратное соединение | | `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников |
| `CONN_TYPE_INDIRECT` | 3 | Соединение через посредника |
| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг: отправлять подсети |
| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID группы uTun по умолчанию |
### Функции ### Функции
**Управление памятью и refcounting:** **Реестр и refcounting:**
- `topo_node_ref(ni)` — увеличить счётчик ссылок на `TOPO_NODE`. - `topo_node_registry_find(groups, node_id)` — найти `TOPO_NODE` в глобальном реестре.
- `topo_node_unref(ni)` — уменьшить счётчик; при 0 освободить `node_name` и `TOPO_NODE` (но не списки — их надо предварительно освободить через `topo_node_free_lists`). - `topo_node_registry_store(groups, ni)` — сохранить с дедупликацией (возвращает существующий при совпадении).
- `topo_node_free_lists(group, nq)` — освободить все списки адресов/sock_meta обратно в memory pool'ы, освободить `subnets`, `tranzit_data`, `hop_list`, сделать `unref` на `node`, обнулить указатели. - `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_dyn_size(msg)` — вычислить размер динамической части wire-формата по полям-счётчикам заголовка `TOPOMSG_NODE`. Не учитывает сам заголовок. - `topo_node_dyn_size(msg)` — размер динамической части по счётчикам заголовка.
- `topo_node_serialize(group, nq, out, out_max)` — сериализовать `TOPO_NODEQ` в бинарный буфер. Возвращает количество записанных байт или -1 при ошибке (переполнение или невалидные аргументы). Формат: заголовок `TOPOMSG_NODE`, затем name, v4 sock_meta, v4 addrs, v6 sock_meta, v6 addrs, подсети (опционально), tranzit, hop_list. - `topo_node_serialize(ni, nq, 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_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_tranzit, &out_tranzit_count, &out_hop_list, &out_hop_count)` — восстановить `TOPO_NODE`, подсети, транзит и hop-лист из бинарного wire-формата. Аллоцирует `TOPO_NODE` через `u_calloc` (ref_count=1), элементы списков — из memory pool'ов, `subnets`, `tranzit_data`, `hop_list` — отдельными `u_malloc`. Возвращает 0 при успехе, -1 при несоответствии размера или ошибке аллокации. Если подсети есть в wire-формате но флаг `TOPO_FLAG_SEND_SUBNETS` не установлен, они пропускаются. - `topo_node_best_hop_list(nq, &out_count, &out_rtt)` — лучший hop-лист (по min hop_count).
**Поиск:** **Подпись:**
- `topo_node_find_by_id(group, node_id)` — найти `TOPO_NODEQ` в очереди группы по `node_id` через хеш-индекс. Возвращает NULL если не найден. - `topo_node_build_sig_msg(ni, buf, buf_size)` — каноническое сообщение для Ed25519-подписи.
- `topo_node_sign_self(instance, ni)` — подписать свой NODEINFO (`x25519_self_sig`).
**Обновление информации о себе:** **Обновление о себе:**
- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `group->local_node` (`TOPO_NODEQ`) из текущих данных инстанса (сокеты, NAT, адреса, подсети, ключи). Сравнивает количество элементов с предыдущим значением; если есть изменения — освобождает старый `local_node`, создаёт новый, инкрементирует `ver`, помечает `dirty=1`. Обновляет таблицу маршрутов (`route_delete`/`route_insert`). Для chat-групп подсети не включаются. Возвращает количество v4 подсетей. - `topo_group_update_my_nodeinfo(instance, group)` — перестроить `local_node`.
- `topo_node_update_my_addresses(instance)` — обновить адреса.
- `topo_node_on_socket_changed(sock, event, arg)` — обработчик изменений сокетов.
**Диагностика:** **Диагностика:**
- `topo_node_dump_all(group)` — дамп всех узлов группы в лог (DEBUG_CATEGORY_BGP). Для каждого узла выводит: ID, имя, версию, pubkeys, v4/v6 сокеты и адреса, подсети, транзитные узлы, пути (с hop-ами), статус связности, conn_mgr. Пропускает узлы с `node==NULL`. - `topo_node_dump_all(group)` / `topo_node_format_all(group, buf, size)` — дамп узлов.
- `topo_node_format_all(group, buf, buf_size)` — аналогичный дамп в строковый буфер. Возвращает количество записанных байт. При переполнении добавляет `[TRUNCATED]`.
**Inline-аксессоры (из .h):** **Inline-аксессоры (.h):**
- `topo_v4_sock_meta(ni)` / `topo_v4_addrs(ni)` / `topo_v6_sock_meta(ni)` / `topo_v6_addrs(ni)` — получить списки из `TOPO_NODE`. - `topo_v4_sock_meta/addr`, `topo_v6_sock_meta/addr`, `topo_v4_subnets/topo_v6_subnets`, `topo_list_count`.
- `topo_v4_subnets(r)` / `topo_v6_subnets(r)` — получить списки подсетей из `TOPO_NODESUBNETS*` (NULL-safe).
- `topo_list_count(head)` — посчитать количество элементов в любом из списков (next — первое поле).
### Зависимости ### Зависимости
| Модуль | Использование | | Модуль | Использование |
|--------|--------------| |--------|--------------|
| `ll_queue.h` | `TOPO_NODEQ` — запись в очереди, хеш-индекс по `node_id`, `paths` — вложенная очередь | | `ll_queue.h` | `TOPO_GROUP_NODE` — запись в очереди (hash по node_id), `paths` — вложенная очередь |
| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) для массивов pubkey/ed25519 | | `secure_channel.h` | `SC_PUBKEY_SIZE` (32) |
| `mem.h` | `u_malloc`, `u_calloc`, `u_free`, `u_strdup` | | `mem.h` | `u_malloc`, `u_calloc`, `u_free` |
| `memory_pool.h` | `memory_pool_alloc`/`memory_pool_free` для элементов списков (пулы из `topo_groups`) | | `memory_pool.h` | Элементы списков из пулов `topo_groups` |
| `debug_config.h` | `DEBUG_INFO`, `DEBUG_WARN`, `DEBUG_ERROR`, `log_dump` | | `debug_config.h` | Логирование (BGP) |
| `topo_group.h` | `struct TOPO_GROUP` (аргумент большинства функций) | | `topo_group.h` | `struct TOPO_GROUP`, `TOPO_GROUPS` |
| `etcp.h` | `struct ETCP_CONN`, `struct ETCP_SOCKET` | | `route_lib.h` | `route_insert/route_delete` |
| `config_parser.h` | `struct CFG_ROUTE_ENTRY`, `struct CFG_SERVER` (при построении local_node) | | `utun_instance.h` | `struct UTUN_INSTANCE` |
| `route_lib.h` | `route_insert()`, `route_delete()` (при обновлении local_node) |
| `etcp_debug.h` | `ip_to_str()` (для дампа адресов и подсетей) |
| `utun_instance.h` | `struct UTUN_INSTANCE` (аргумент `topo_group_update_my_nodeinfo`) |

2
src/routing_layer/topo_node_sqlite_doc.md

@ -36,4 +36,4 @@
| `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online | | `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online |
| `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) | | `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) |
**Структуры:** внешние — `sqlite3*`, `struct TOPO_NODEQ`, `struct TOPO_NODE`; собственных структур нет. **Структуры:** внешние — `sqlite3*`, `struct TOPO_GROUP_NODE`, `struct TOPO_NODE`; собственных структур нет.

137
src/routing_layer/topo_recovery.c

@ -1,13 +1,13 @@
/** /**
* @file topo_recovery.c * @file topo_recovery.c
* @brief Реализация восстановления каскадных узлов. * @brief Реализация восстановления каскадных узлов (прямые подключения, ncd).
* *
* Цепочка: add_node (группировка по next_hop) → start (запуск всех ctx) → try_next (цикл) * Цепочка: add_node (группировка по next_hop) → start (запуск всех ctx) → try_next (цикл)
* try_next: сперва is_next_hop-узел группы, затем остальные по мин. RTT * try_next: сперва is_next_hop-узел группы, затем остальные по мин. RTT
* ├─ recovery_callback (ok/fail): удалить узел → try_next * ├─ recovery_callback (ok/fail): удалить узел → try_next
* └─ timeout_callback (2с): conn_mgr_cancel_callback → удалить → try_next * └─ timeout_callback (2с): закрыть handle → удалить → try_next
* Завершение: оба списка пусты → освобождение ctx * Завершение: список пуст → освобождение ctx
* Отмена: cancel_for_node (on_up) → cancel_callback + освобождение ctx * Отмена: cancel_for_node (on_up) → закрыть handle + освобождение ctx
*/ */
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -19,9 +19,8 @@
#include "topo_node.h" #include "topo_node.h"
#include "topo_group.h" #include "topo_group.h"
#include "topo_recovery.h" #include "topo_recovery.h"
#include "conn_mgr.h" #include "../transport_layer/node_conn_direct.h"
#include "config_parser.h" #include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#define RECOVERY_CAPACITY_INIT 16 #define RECOVERY_CAPACITY_INIT 16
@ -30,37 +29,18 @@ static struct TOPO_RECOVERY_CTX* topo_recovery_ctx_alloc(struct TOPO_GROUP* grou
struct TOPO_RECOVERY_CTX* ctx = u_calloc(1, sizeof(struct TOPO_RECOVERY_CTX)); struct TOPO_RECOVERY_CTX* ctx = u_calloc(1, sizeof(struct TOPO_RECOVERY_CTX));
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "recovery: alloc ctx failed"); return NULL; } if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "recovery: alloc ctx failed"); return NULL; }
ctx->instance = group->instance; ctx->group = group; ctx->next_hop_id = next_hop_id; ctx->instance = group->instance; ctx->group = group; ctx->next_hop_id = next_hop_id;
ctx->capacity[0] = RECOVERY_CAPACITY_INIT; ctx->capacity[1] = RECOVERY_CAPACITY_INIT; ctx->capacity = RECOVERY_CAPACITY_INIT;
ctx->nodes[0] = u_calloc(ctx->capacity[0], sizeof(struct TOPO_RECOVERY_NODE)); ctx->nodes = u_calloc(ctx->capacity, sizeof(struct TOPO_RECOVERY_NODE));
ctx->nodes[1] = u_calloc(ctx->capacity[1], sizeof(struct TOPO_RECOVERY_NODE)); if (!ctx->nodes) { u_free(ctx); return NULL; }
if (!ctx->nodes[0] || !ctx->nodes[1]) { u_free(ctx->nodes[0]); u_free(ctx->nodes[1]); u_free(ctx); return NULL; }
return ctx; return ctx;
} }
static void topo_recovery_ctx_free(struct TOPO_RECOVERY_CTX* ctx) { static void topo_recovery_ctx_free(struct TOPO_RECOVERY_CTX* ctx) {
if (!ctx) return; if (!ctx) return;
u_free(ctx->nodes[0]); u_free(ctx->nodes[1]); u_free(ctx->nodes);
u_free(ctx); u_free(ctx);
} }
/* Аналог cm_has_direct_ip: есть ли адреса с прямым доступом (PUBLIC/UNKNOWN/EIM/DIRECT) */
static int node_has_direct_ip(struct TOPO_GROUP* group, const struct TOPO_GROUP_NODE* nq) {
if (!nq) return 0;
struct TOPO_NODE* ni = topo_node_registry_find(group->instance->topo_groups, nq->node_id);
if (!ni) return 0;
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
if (a->type != TOPO_ADDR_NAT && a->type != TOPO_ADDR_INTERFACE) continue;
for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) {
if (m->id != a->socket_id) continue;
if (m->config_type == CFG_SERVER_TYPE_PUBLIC || m->config_type == CFG_SERVER_TYPE_UNKNOWN
|| m->nat_type == NAT_VERIFIED_EIM || m->nat_type == NAT_VERIFIED_DIRECT)
return 1;
}
}
return 0;
}
/* Ищет незапущенный (started==0) контекст по next_hop_id */ /* Ищет незапущенный (started==0) контекст по next_hop_id */
static struct TOPO_RECOVERY_CTX* topo_recovery_find_by_next_hop(struct TOPO_GROUP* group, uint64_t next_hop_id) { static struct TOPO_RECOVERY_CTX* topo_recovery_find_by_next_hop(struct TOPO_GROUP* group, uint64_t next_hop_id) {
struct TOPO_RECOVERY_CTX* ctx = group->recovery_list; struct TOPO_RECOVERY_CTX* ctx = group->recovery_list;
@ -76,16 +56,16 @@ static void topo_recovery_ctx_remove(struct TOPO_GROUP* group, struct TOPO_RECOV
static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx); static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx);
/* Коллбэк conn_mgr: подключение удалось или провалилось */ /* Коллбэк ncd: подключение удалось или провалилось */
static void topo_recovery_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
enum conn_mgr_event event, void* arg) { struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg;
struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; (void)h; (void)group_id; struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
uint64_t node_id = conn ? conn->peer_node_id : 0;
if (node_id != ctx->current_node_id) return; if (node_id != ctx->current_node_id) return;
if (ctx->connect_timer) { uasync_cancel_timeout(ctx->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } if (ctx->connect_timer) { uasync_cancel_timeout(ctx->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; }
int list = ctx->current_list; for (size_t i = 0; i < ctx->count; i++)
for (size_t i = 0; i < ctx->count[list]; i++) if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; }
if (ctx->nodes[list][i].node_id == node_id) { ctx->nodes[list][i] = ctx->nodes[list][ctx->count[list] - 1]; ctx->count[list]--; break; } if (event == NCD_EVENT_UP) {
if (event == CONN_EVENT_UP) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(ctx->group, node_id); struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(ctx->group, node_id);
if (nq) nq->handle = h; if (nq) nq->handle = h;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id);
@ -96,58 +76,53 @@ static void topo_recovery_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id,
topo_recovery_try_next(ctx); topo_recovery_try_next(ctx);
} }
/* Таймаут 2с: отменяет conn_mgr-коллбэк, удаляет узел, переходит к следующему */ /* Таймаут 2с: закрывает handle, удаляет узел, переходит к следующему */
static void topo_recovery_timeout_cb(void* arg) { static void topo_recovery_timeout_cb(void* arg) {
struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg;
ctx->connect_timer = NULL; ctx->connect_timer = NULL;
uint64_t node_id = ctx->current_node_id; uint64_t node_id = ctx->current_node_id;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx timeout connecting to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx timeout connecting to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id);
if (ctx->current_handle) { if (ctx->current_handle) {
conn_mgr_close(ctx->current_handle); node_conn_direct_close(ctx->current_handle);
ctx->current_handle = NULL; ctx->current_handle = NULL;
} }
int list = ctx->current_list; for (size_t i = 0; i < ctx->count; i++)
for (size_t i = 0; i < ctx->count[list]; i++) if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; }
if (ctx->nodes[list][i].node_id == node_id) { ctx->nodes[list][i] = ctx->nodes[list][ctx->count[list] - 1]; ctx->count[list]--; break; }
ctx->current_node_id = 0; ctx->current_node_id = 0;
topo_recovery_try_next(ctx); topo_recovery_try_next(ctx);
} }
/* Выбирает узел: сперва is_next_hop с мин. RTT, за ним — остальные по мин. RTT */ /* Выбирает узел: сперва is_next_hop с мин. RTT, за ним — остальные по мин. RTT */
static struct TOPO_RECOVERY_NODE* topo_recovery_find_best(struct TOPO_RECOVERY_CTX* ctx, int list) { static struct TOPO_RECOVERY_NODE* topo_recovery_find_best(struct TOPO_RECOVERY_CTX* ctx) {
struct TOPO_RECOVERY_NODE* best = NULL; struct TOPO_RECOVERY_NODE* best = NULL;
for (size_t i = 0; i < ctx->count[list]; i++) for (size_t i = 0; i < ctx->count; i++)
if (ctx->nodes[list][i].is_next_hop && (!best || ctx->nodes[list][i].min_rtt < best->min_rtt)) if (ctx->nodes[i].is_next_hop && (!best || ctx->nodes[i].min_rtt < best->min_rtt))
best = &ctx->nodes[list][i]; best = &ctx->nodes[i];
if (best) return best; if (best) return best;
for (size_t i = 0; i < ctx->count[list]; i++) for (size_t i = 0; i < ctx->count; i++)
if (!best || ctx->nodes[list][i].min_rtt < best->min_rtt) if (!best || ctx->nodes[i].min_rtt < best->min_rtt)
best = &ctx->nodes[list][i]; best = &ctx->nodes[i];
return best; return best;
} }
static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx) { static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx) {
while (1) { while (1) {
if (ctx->count[ctx->current_list] == 0) { if (ctx->count == 0) {
ctx->current_list++;
if (ctx->current_list > 1) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx all nodes exhausted, no reconnections", (unsigned long long)ctx->next_hop_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx all nodes exhausted, no reconnections", (unsigned long long)ctx->next_hop_id);
topo_recovery_ctx_remove(ctx->group, ctx); topo_recovery_ctx_remove(ctx->group, ctx);
topo_recovery_ctx_free(ctx); topo_recovery_ctx_free(ctx);
return; return;
} }
continue; struct TOPO_RECOVERY_NODE* best = topo_recovery_find_best(ctx);
}
struct TOPO_RECOVERY_NODE* best = topo_recovery_find_best(ctx, ctx->current_list);
uint64_t node_id = best->node_id; uint64_t node_id = best->node_id;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx trying %016llx rtt=%u list=%s %s", DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx trying %016llx rtt=%u %s",
(unsigned long long)ctx->next_hop_id, (unsigned long long)node_id, best->min_rtt, (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id, best->min_rtt,
ctx->current_list == 0 ? "direct" : "indirect", best->is_next_hop ? "[next_hop]" : ""); best->is_next_hop ? "[next_hop]" : "");
ctx->current_node_id = node_id; ctx->current_node_id = node_id;
if (conn_mgr_open_invite(ctx->instance, ctx->group->group_id, NULL, node_id, if (node_conn_direct_open(ctx->instance, node_id, topo_recovery_callback, ctx,
topo_recovery_callback, ctx, &ctx->current_handle) == -1) { &ctx->current_handle, NULL) == NCD_ERR) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: conn_mgr_open_invite %016llx failed", (unsigned long long)node_id); DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: node_conn_direct_open %016llx failed", (unsigned long long)node_id);
*best = ctx->nodes[ctx->current_list][ctx->count[ctx->current_list] - 1]; ctx->count[ctx->current_list]--; *best = ctx->nodes[ctx->count - 1]; ctx->count--;
ctx->current_node_id = 0; ctx->current_node_id = 0;
continue; continue;
} }
@ -176,24 +151,23 @@ void topo_recovery_add_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq
group->recovery_list = ctx; group->recovery_list = ctx;
} }
int list = node_has_direct_ip(group, nq) ? 0 : 1;
uint16_t rtt = topo_get_chain_rtt(nq); uint16_t rtt = topo_get_chain_rtt(nq);
if (ctx->count[list] >= ctx->capacity[list]) { if (ctx->count >= ctx->capacity) {
size_t new_cap = ctx->capacity[list] * 2; size_t new_cap = ctx->capacity * 2;
struct TOPO_RECOVERY_NODE* new_nodes = u_realloc(ctx->nodes[list], new_cap * sizeof(struct TOPO_RECOVERY_NODE)); struct TOPO_RECOVERY_NODE* new_nodes = u_realloc(ctx->nodes, new_cap * sizeof(struct TOPO_RECOVERY_NODE));
if (!new_nodes) return; if (!new_nodes) return;
ctx->nodes[list] = new_nodes; ctx->capacity[list] = new_cap; ctx->nodes = new_nodes; ctx->capacity = new_cap;
} }
ctx->nodes[list][ctx->count[list]].node_id = node_id; ctx->nodes[ctx->count].node_id = node_id;
ctx->nodes[list][ctx->count[list]].min_rtt = rtt; ctx->nodes[ctx->count].min_rtt = rtt;
ctx->nodes[list][ctx->count[list]].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0); ctx->nodes[ctx->count].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0);
ctx->count[list]++; ctx->count++;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "recovery: next=%016llx add node %016llx list=%s rtt=%u is_nh=%d count=%zu", DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "recovery: next=%016llx add node %016llx rtt=%u is_nh=%d count=%zu",
(unsigned long long)next_hop, (unsigned long long)node_id, (unsigned long long)next_hop, (unsigned long long)node_id,
list == 0 ? "direct" : "indirect", rtt, node_id == next_hop ? 1 : 0, ctx->count[list]); rtt, node_id == next_hop ? 1 : 0, ctx->count);
} }
void topo_recovery_start(struct TOPO_GROUP* group) { void topo_recovery_start(struct TOPO_GROUP* group) {
@ -203,13 +177,13 @@ void topo_recovery_start(struct TOPO_GROUP* group) {
while (ctx) { while (ctx) {
struct TOPO_RECOVERY_CTX* next_ctx = ctx->next; struct TOPO_RECOVERY_CTX* next_ctx = ctx->next;
if (!ctx->started) { if (!ctx->started) {
if (ctx->count[0] + ctx->count[1] == 0) { if (ctx->count == 0) {
topo_recovery_ctx_remove(group, ctx); topo_recovery_ctx_remove(group, ctx);
topo_recovery_ctx_free(ctx); topo_recovery_ctx_free(ctx);
} else { } else {
ctx->started = 1; ctx->current_list = 0; ctx->started = 1;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: start next=%016llx direct=%zu indirect=%zu", DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: start next=%016llx count=%zu",
(unsigned long long)ctx->next_hop_id, ctx->count[0], ctx->count[1]); (unsigned long long)ctx->next_hop_id, ctx->count);
topo_recovery_try_next(ctx); topo_recovery_try_next(ctx);
started_ctxs++; started_ctxs++;
} }
@ -227,14 +201,13 @@ void topo_recovery_cancel_for_node(struct TOPO_GROUP* group, uint64_t node_id) {
while (*pp) { while (*pp) {
struct TOPO_RECOVERY_CTX* ctx = *pp; struct TOPO_RECOVERY_CTX* ctx = *pp;
int found = 0; int found = 0;
for (int list = 0; list < 2 && !found; list++) for (size_t i = 0; i < ctx->count && !found; i++)
for (size_t i = 0; i < ctx->count[list] && !found; i++) if (ctx->nodes[i].node_id == node_id) found = 1;
if (ctx->nodes[list][i].node_id == node_id) found = 1;
if (found) { if (found) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: cancel next=%016llx (node %016llx came up)", DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: cancel next=%016llx (node %016llx came up)",
(unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id);
if (ctx->current_node_id != 0 && ctx->current_handle) if (ctx->current_node_id != 0 && ctx->current_handle)
conn_mgr_close(ctx->current_handle); node_conn_direct_close(ctx->current_handle);
if (ctx->connect_timer) { uasync_cancel_timeout(group->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } if (ctx->connect_timer) { uasync_cancel_timeout(group->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; }
*pp = ctx->next; *pp = ctx->next;
topo_recovery_ctx_free(ctx); topo_recovery_ctx_free(ctx);
@ -250,7 +223,7 @@ void topo_recovery_cancel_all(struct TOPO_GROUP* group) {
while (ctx) { while (ctx) {
struct TOPO_RECOVERY_CTX* next = ctx->next; struct TOPO_RECOVERY_CTX* next = ctx->next;
if (ctx->current_node_id != 0 && ctx->current_handle) if (ctx->current_node_id != 0 && ctx->current_handle)
conn_mgr_close(ctx->current_handle); node_conn_direct_close(ctx->current_handle);
if (ctx->connect_timer) { uasync_cancel_timeout(group->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } if (ctx->connect_timer) { uasync_cancel_timeout(group->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; }
topo_recovery_ctx_free(ctx); topo_recovery_ctx_free(ctx);
ctx = next; ctx = next;

18
src/routing_layer/topo_recovery.h

@ -3,13 +3,13 @@
* @brief Восстановление каскадно отвалившихся узлов после разрыва ETCP-соединения. * @brief Восстановление каскадно отвалившихся узлов после разрыва ETCP-соединения.
* *
* Когда рвётся соединение, узлы достижимые только через него становятся недоступны. * Когда рвётся соединение, узлы достижимые только через него становятся недоступны.
* Модуль собирает их перед удалением из BGP-таблицы и пробует переподключиться: * Модуль собирает их перед удалением из BGP-таблицы и пробует переподключиться
* напрямую (ncd):
* 1. add_node — из hop_list вычисляет next_hop относительно failed_peer, * 1. add_node — из hop_list вычисляет next_hop относительно failed_peer,
* группирует узлы по next_hop в отдельные recovery-контексты * группирует узлы по next_hop в отдельные recovery-контексты
* 2. Классификация на direct (есть прямые IP) / indirect (нет) * 2. start — для каждого контекста запускает асинхронный перебор:
* 3. start — для каждого контекста запускает асинхронный перебор:
* сперва пробует next_hop-узел (is_next_hop=1), затем остальные по мин. RTT * сперва пробует next_hop-узел (is_next_hop=1), затем остальные по мин. RTT
* 4. Таймаут 2с на каждую попытку conn_mgr_connect_node * 3. Таймаут 2с на каждую попытку node_conn_direct_open
* *
* Группировка: <мы> -> <failed_peer N> -> <next_hop A, B...> * Группировка: <мы> -> <failed_peer N> -> <next_hop A, B...>
* Каждый next_hop со своим subtree — отдельный recovery-контекст. * Каждый next_hop со своим subtree — отдельный recovery-контекст.
@ -30,6 +30,7 @@ extern "C" {
struct TOPO_GROUP; struct TOPO_GROUP;
struct TOPO_GROUP_NODE; struct TOPO_GROUP_NODE;
struct NODE_CONN_DIRECT;
#define TOPO_RECOVERY_CONNECT_TIMEOUT_MS 2000 #define TOPO_RECOVERY_CONNECT_TIMEOUT_MS 2000
@ -44,12 +45,11 @@ struct TOPO_RECOVERY_CTX {
struct UTUN_INSTANCE* instance; struct UTUN_INSTANCE* instance;
struct TOPO_GROUP* group; struct TOPO_GROUP* group;
uint64_t next_hop_id; /* ключ группировки: node_id next-hop'а от failed_peer */ uint64_t next_hop_id; /* ключ группировки: node_id next-hop'а от failed_peer */
struct TOPO_RECOVERY_NODE* nodes[2]; /* [0]=с прямыми IP, [1]=без прямых IP */ struct TOPO_RECOVERY_NODE* nodes; /* кандидаты на восстановление (прямые) */
size_t count[2]; /* текущее количество в каждом списке */ size_t count; /* текущее количество */
size_t capacity[2]; /* выделенная ёмкость */ size_t capacity; /* выделенная ёмкость */
int current_list; /* 0=direct, 1=indirect */
uint64_t current_node_id; /* node_id в текущей попытке, 0=нет активной */ uint64_t current_node_id; /* node_id в текущей попытке, 0=нет активной */
struct CONN_MGR_HANDLE* current_handle; /* handle текущей попытки conn_mgr_connect_node */ struct NODE_CONN_DIRECT* current_handle; /* handle текущей попытки node_conn_direct_open */
void* connect_timer; /* внешний таймер 2с (uasync) */ void* connect_timer; /* внешний таймер 2с (uasync) */
uint8_t started; /* 0=сбор узлов (add_node), 1=перебор запущен */ uint8_t started; /* 0=сбор узлов (add_node), 1=перебор запущен */
}; };

130
src/transport_layer/node_conn_direct.c

@ -125,6 +125,29 @@ static struct TOPO_NODE* ncd_lookup_node(struct UTUN_INSTANCE* inst, uint64_t no
* с новым ключом, старый conn больше не нужен) * с новым ключом, старый conn больше не нужен)
*/ */
/* Создаёт один UDP-линк на use_sock → sa, с вытеснением stale-линка и дедупом. */
static int ncd_add_udp_link(struct ETCP_CONN* conn, struct ETCP_SOCKET* use_sock, struct sockaddr_storage* sa) {
struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, sa, 0);
if (stale && stale->etcp == conn) return 0;
if (stale && stale->etcp != conn
&& memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE))
{
if (!stale->etcp->links_up || !stale->etcp->initialized) {
struct ETCP_CONN* old = stale->etcp;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[ncd] evict stale link node=0x%016llx(old pk=%016llx) for new node=0x%016llx(pk=%016llx)",
(unsigned long long)old->peer_node_id, *(const uint64_t*)old->crypto_ctx.peer_public_key,
(unsigned long long)conn->peer_node_id, *(const uint64_t*)conn->crypto_ctx.peer_public_key);
etcp_link_close(stale);
etcp_cbk_fire(old, ETCP_CBK_EVENT_NODE_CHANGED);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[ncd] addr conflict: port already used by live conn 0x%016llx for node 0x%016llx",
(unsigned long long)stale->etcp->peer_node_id, (unsigned long long)conn->peer_node_id);
return 0;
}
}
return etcp_link_new(conn, use_sock, sa, 0) ? 1 : 0;
}
static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
struct ETCP_SOCKET* specific_sock) { struct ETCP_SOCKET* specific_sock) {
struct ETCP_CONN* conn = entry->conn; struct ETCP_CONN* conn = entry->conn;
@ -133,107 +156,72 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
uint64_t nid = entry->node_id; uint64_t nid = entry->node_id;
/* ---- IPv4 ---- */ /* ---- IPv4 ---- */
{
struct ETCP_SOCKET* socks[64]; int sock_count = 0;
if (specific_sock) {
if (specific_sock->local_addr.ss_family == AF_INET)
socks[sock_count++] = specific_sock;
} else {
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next)
if (s->local_addr.ss_family == AF_INET && s->type != CFG_SERVER_TYPE_PRIVATE)
socks[sock_count++] = s;
}
if (sock_count == 0) DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 sock_count=0 node=0x%016llx", (unsigned long long)nid);
if (sock_count > 0) {
int rr = 0;
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
v4_addrs++; v4_addrs++;
if (a->port == 0 || !(a->protocol & TOPO_PROTO_UDP)) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v4_skip++; continue; } if (a->port == 0 || !(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v4_skip++; continue; }
int zero = 1; for (int j = 0; j < 4; j++) if (a->addr[j] != 0) { zero = 0; break; } int zero = 1; for (int j = 0; j < 4; j++) if (a->addr[j] != 0) { zero = 0; break; }
if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; } if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; }
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET;
memcpy(&sin.sin_addr.s_addr, a->addr, 4); sin.sin_port = htons(a->port); memcpy(&sin.sin_addr.s_addr, a->addr, 4); sin.sin_port = htons(a->port);
struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin)); struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin));
struct ETCP_SOCKET* use_sock = socks[rr++ % sock_count];
{ if (a->protocol & TOPO_PROTO_TCP) {
struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, &sa, 0); for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) {
if (stale && stale->etcp == conn) continue; if (!s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue;
if (stale && stale->etcp != conn struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0);
&& memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE)) if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; }
{
if (!stale->etcp->links_up || !stale->etcp->initialized) {
struct ETCP_CONN* old = stale->etcp;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[ncd] evict stale link node=0x%016llx(old pk=%016llx) for new node=0x%016llx(pk=%016llx)",
(unsigned long long)old->peer_node_id, *(const uint64_t*)old->crypto_ctx.peer_public_key,
(unsigned long long)conn->peer_node_id, *(const uint64_t*)conn->crypto_ctx.peer_public_key);
etcp_link_close(stale);
etcp_cbk_fire(old, ETCP_CBK_EVENT_NODE_CHANGED);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[ncd] addr conflict: %d.%d.%d.%d:%d already used by live conn 0x%016llx for node 0x%016llx",
a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port,
(unsigned long long)stale->etcp->peer_node_id, (unsigned long long)conn->peer_node_id);
continue;
} }
} }
if (a->protocol & TOPO_PROTO_UDP) {
if (specific_sock && !specific_sock->is_tcp && specific_sock->local_addr.ss_family == AF_INET) {
link_count += ncd_add_udp_link(conn, specific_sock, &sa);
} else if (!specific_sock) {
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) {
if (s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue;
link_count += ncd_add_udp_link(conn, s, &sa);
} }
if (etcp_link_new(conn, use_sock, &sa, 0)) link_count++;
} }
} }
} }
/* ---- IPv6 ---- */ /* ---- IPv6 ---- */
{
struct ETCP_SOCKET* socks[64]; int sock_count = 0;
if (specific_sock) {
if (specific_sock->local_addr.ss_family == AF_INET6)
socks[sock_count++] = specific_sock;
} else {
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next)
if (s->local_addr.ss_family == AF_INET6 && s->type != CFG_SERVER_TYPE_PRIVATE)
socks[sock_count++] = s;
}
if (sock_count == 0) DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 sock_count=0 node=0x%016llx", (unsigned long long)nid);
if (sock_count > 0) {
int rr = 0;
for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) { for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) {
v6_addrs++; v6_addrs++;
if (a->port == 0 || !(a->protocol & TOPO_PROTO_UDP)) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v6_skip++; continue; } if (a->port == 0 || !(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v6_skip++; continue; }
int zero = 1; for (int j = 0; j < 16; j++) if (a->addr[j] != 0) { zero = 0; break; } int zero = 1; for (int j = 0; j < 16; j++) if (a->addr[j] != 0) { zero = 0; break; }
if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; } if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; }
int is_ll = (a->addr[0] == 0xfe && (a->addr[1] & 0xc0) == 0x80);
struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6;
memcpy(&sin6.sin6_addr, a->addr, 16); sin6.sin6_port = htons(a->port); memcpy(&sin6.sin6_addr, a->addr, 16); sin6.sin6_port = htons(a->port);
struct ETCP_SOCKET* use_sock = socks[rr++ % sock_count];
if ((a->addr[0] == 0xfe && (a->addr[1] & 0xc0) == 0x80)) sin6.sin6_scope_id = use_sock->netif_index; if (a->protocol & TOPO_PROTO_TCP) {
if (is_ll) { struct ETCP_SOCKET* sv = inst->etcp_sockets;
while (sv) { if (sv->local_addr.ss_family == AF_INET6 && sv->netif_index) { sin6.sin6_scope_id = sv->netif_index; break; } sv = sv->next; } }
struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6)); struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6));
{ for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) {
struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, &sa, 0); if (!s->is_tcp || s->local_addr.ss_family != AF_INET6 || s->type == CFG_SERVER_TYPE_PRIVATE) continue;
if (stale && stale->etcp == conn) continue; struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0);
if (stale && stale->etcp != conn if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; }
&& memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE))
{
if (!stale->etcp->links_up || !stale->etcp->initialized) {
struct ETCP_CONN* old = stale->etcp;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[ncd] evict stale v6 link node=0x%016llx(old) -> 0x%016llx(new)",
(unsigned long long)old->peer_node_id, (unsigned long long)conn->peer_node_id);
etcp_link_close(stale);
etcp_cbk_fire(old, ETCP_CBK_EVENT_NODE_CHANGED);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[ncd] addr conflict: v6 port=%d already used by live conn 0x%016llx for node 0x%016llx",
(int)a->port, (unsigned long long)stale->etcp->peer_node_id, (unsigned long long)conn->peer_node_id);
continue;
} }
} }
if (a->protocol & TOPO_PROTO_UDP) {
if (specific_sock && !specific_sock->is_tcp && specific_sock->local_addr.ss_family == AF_INET6) {
if (is_ll) sin6.sin6_scope_id = specific_sock->netif_index;
struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6));
link_count += ncd_add_udp_link(conn, specific_sock, &sa);
} else if (!specific_sock) {
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) {
if (s->is_tcp || s->local_addr.ss_family != AF_INET6 || s->type == CFG_SERVER_TYPE_PRIVATE) continue;
if (is_ll) sin6.sin6_scope_id = s->netif_index;
struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6));
link_count += ncd_add_udp_link(conn, s, &sa);
} }
if (etcp_link_new(conn, use_sock, &sa, 0)) link_count++;
} }
} }
} }
if (link_count == 0) { if (link_count == 0) {
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] create_links: node=0x%016llx v4=%d/skp=%d v6=%d/skp=%d → %d UDP links (TCP handled by cm_direct_add_links)", DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] create_links: node=0x%016llx v4=%d/skp=%d v6=%d/skp=%d → %d links (UDP+TCP)",
(unsigned long long)nid, v4_addrs, v4_skip, v6_addrs, v6_skip, link_count); (unsigned long long)nid, v4_addrs, v4_skip, v6_addrs, v6_skip, link_count);
} }
return link_count; return link_count;

6
src/utun_instance.c

@ -9,6 +9,7 @@
#include "route_lib.h" #include "route_lib.h"
#include "routing.h" #include "routing.h"
#include "topo_group.h" #include "topo_group.h"
#include "topo_group_invite.h"
#include "nat_detection.h" #include "nat_detection.h"
#include "etcp_connections.h" #include "etcp_connections.h"
#include "etcp.h" #include "etcp.h"
@ -216,7 +217,7 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u
} else { } else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "BGP module initialized"); DEBUG_INFO(DEBUG_CATEGORY_BGP, "BGP module initialized");
etcp_router_bind(instance, ETCP_RT_ID_CONN_MGR, conn_mgr_router_recv_handler); etcp_router_bind(instance, ETCP_RT_ID_CONN_MGR, conn_mgr_router_recv_handler);
etcp_bind(instance, ETCP_RT_ID_CONN_MGR, conn_mgr_direct_recv_handler); topo_group_invite_init(instance);
struct TOPO_GROUP* g = topo_groups_get_default(instance->topo_groups); struct TOPO_GROUP* g = topo_groups_get_default(instance->topo_groups);
if (instance->rt && g && g->local_node) { if (instance->rt && g && g->local_node) {
topo_group_update_my_nodeinfo(instance, g); topo_group_update_my_nodeinfo(instance, g);
@ -466,7 +467,8 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
/* Phase F: BGP */ /* Phase F: BGP */
if (instance->topo_groups) { if (instance->topo_groups) {
etcp_router_unbind(instance, ETCP_RT_ID_CONN_MGR); etcp_router_unbind(instance, ETCP_RT_ID_CONN_MGR);
etcp_unbind(instance, ETCP_RT_ID_CONN_MGR); etcp_unbind(instance, ETCP_RT_ID_GROUP_INVITE);
topo_group_invite_cancel_all(instance);
topo_groups_destroy(instance); topo_groups_destroy(instance);
} }
broadcast_destroy_instance(instance); broadcast_destroy_instance(instance);

6
tests/test_conn_mgr.c

@ -68,8 +68,8 @@ static void ccb(struct CONN_MGR_HANDLE* h, uint64_t id, uint64_t gid,
static void test1(void* arg) { static void test1(void* arg) {
(void)arg; if (result) return; (void)arg; if (result) return;
if (!topo_node_find_by_id(topo_groups_get_default(g_a->topo_groups), g_nid_b)) { uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test1, "t1"); return; } if (!topo_node_find_by_id(topo_groups_get_default(g_a->topo_groups), g_nid_b)) { uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test1, "t1"); return; }
fprintf(stderr, "Test 1: direct — conn_mgr_open_invite(B)\n"); fflush(stderr); fprintf(stderr, "Test 1: direct — conn_mgr_open(B)\n"); fflush(stderr);
conn_mgr_open_invite(g_a, TOPO_GROUP_UTUN, NULL, g_nid_b, ccb, NULL, NULL); conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_nid_b, ccb, NULL, NULL);
uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test2, "t2a"); uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test2, "t2a");
} }
static void test2(void* arg) { static void test2(void* arg) {
@ -145,7 +145,7 @@ static void test3(void* arg) {
(void)arg; if (result) return; (void)arg; if (result) return;
fprintf(stderr, "Test 2: DB node (B2) — direct connect\n"); fflush(stderr); fprintf(stderr, "Test 2: DB node (B2) — direct connect\n"); fflush(stderr);
cdone2 = 0; cevent2 = 0; cdone2 = 0; cevent2 = 0;
conn_mgr_open_invite(g_a2, TOPO_GROUP_UTUN, NULL, g_nid_b2, ccb2, NULL, NULL); conn_mgr_open(g_a2, TOPO_GROUP_UTUN, g_nid_b2, ccb2, NULL, NULL);
uasync_set_timeout(ua2, 20, NULL, (timeout_callback_t)test3_check, "t3a"); uasync_set_timeout(ua2, 20, NULL, (timeout_callback_t)test3_check, "t3a");
} }
static void test3_check(void* arg) { static void test3_check(void* arg) {

8
tests/test_conn_mgr_already_connected.c

@ -1,8 +1,8 @@
/** /**
* @file test_conn_mgr_already_connected.c * @file test_conn_mgr_already_connected.c
* @brief conn_mgr: подключение к уже-соединённому узлу через conn_mgr_open_invite * @brief conn_mgr: подключение к уже-соединённому узлу через conn_mgr_open
* *
* Проверяет, что conn_mgr_open_invite находит существующее ETCP-соединение * Проверяет, что conn_mgr_open находит существующее ETCP-соединение
* и сразу вызывает callback с CONN_EVENT_UP. * и сразу вызывает callback с CONN_EVENT_UP.
*/ */
#include <stdio.h> #include <stdio.h>
@ -70,8 +70,8 @@ static void test_find_conn(void* arg) {
static void test_mgr_connect(void* arg) { static void test_mgr_connect(void* arg) {
(void)arg; (void)arg;
cb_fired = 0; cb_ok = 0; cb_fired = 0; cb_ok = 0;
fprintf(stderr, "Test 2: conn_mgr_open_invite(B)\n"); fflush(stderr); fprintf(stderr, "Test 2: conn_mgr_open(B)\n"); fflush(stderr);
conn_mgr_open_invite(g_a, TOPO_GROUP_UTUN, NULL, g_b->node_id, conn_cb, NULL, NULL); conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_b->node_id, conn_cb, NULL, NULL);
uasync_call_soon(ua, NULL, test_check); uasync_call_soon(ua, NULL, test_check);
} }

21
tests/test_invite_group_create.c

@ -1,6 +1,6 @@
/** /**
* @file test_invite_group_create.c * @file test_invite_group_create.c
* @brief Full invite flow: conn_mgr_open_invite → B is member → CONN_EVENT_JOIN * @brief Full invite flow: topo_group_invite_join → B is member → TGI_EVENT_JOIN
*/ */
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -22,7 +22,7 @@
#include "../src/routing_layer/topo_group.h" #include "../src/routing_layer/topo_group.h"
#include "../src/routing_layer/topo_node.h" #include "../src/routing_layer/topo_node.h"
#include "../src/routing_layer/topo_node_sqlite.h" #include "../src/routing_layer/topo_node_sqlite.h"
#include "../src/routing_layer/conn_mgr.h" #include "../src/routing_layer/topo_group_invite.h"
#define TIMEOUT_TB 50000 #define TIMEOUT_TB 50000
#define POLL_MS 20 #define POLL_MS 20
@ -48,12 +48,11 @@ static char* ls(const char* p, const char* k) {
static int result = 0; static int result = 0;
static void ccb(struct CONN_MGR_HANDLE* h, uint64_t nid, uint64_t gid, static void ccb(uint64_t nid, uint64_t gid, int ev, void* arg) {
enum conn_mgr_event ev, void* arg) { (void)gid; (void)arg;
(void)h; (void)gid; (void)arg; fprintf(stderr, "CB: ev=%d node=0x%llx\n", ev, (unsigned long long)nid); fflush(stderr);
fprintf(stderr, "CB: ev=%d node=0x%llx\n", (int)ev, (unsigned long long)nid); fflush(stderr); if (ev == TGI_EVENT_JOIN) result = 1;
if (ev == CONN_EVENT_JOIN) result = 1; if (ev == TGI_EVENT_TIMEOUT) result = 2;
if (ev == CONN_EVENT_TIMEOUT) result = 2;
} }
static void to_cb(void* arg) { (void)arg; fprintf(stderr, "TIMEOUT\n"); fflush(stderr); result = 2; } static void to_cb(void* arg) { (void)arg; fprintf(stderr, "TIMEOUT\n"); fflush(stderr); result = 2; }
@ -141,11 +140,11 @@ int main(void) {
ni->v4_addrs = a4; ni->v4_addrs = a4;
uint64_t gid = 0x7777777700000001ULL; uint64_t gid = 0x7777777700000001ULL;
fprintf(stderr, "=== conn_mgr_open_invite gid=0x%016llX nid=0x%016llX ===\n", fprintf(stderr, "=== topo_group_invite_join gid=0x%016llX nid=0x%016llX ===\n",
(unsigned long long)gid, (unsigned long long)nid_b); fflush(stderr); (unsigned long long)gid, (unsigned long long)nid_b); fflush(stderr);
int r = conn_mgr_open_invite(a, gid, ni, nid_b, ccb, NULL, NULL); int r = topo_group_invite_join(a, gid, ni, nid_b, ccb, NULL);
fprintf(stderr, "conn_mgr_open_invite => %d\n", r); fflush(stderr); fprintf(stderr, "topo_group_invite_join => %d\n", r); fflush(stderr);
void* tt = uasync_set_timeout(ua, TIMEOUT_TB, NULL, to_cb, "to"); void* tt = uasync_set_timeout(ua, TIMEOUT_TB, NULL, to_cb, "to");
int el = 0; int el = 0;

Loading…
Cancel
Save