Browse Source
- 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
37 changed files with 1502 additions and 1118 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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`. |
||||
@ -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`. |
||||
@ -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; |
||||
} |
||||
@ -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 |
||||
Loading…
Reference in new issue