Browse Source

schema: rm is_dm, rename name→userinfo (json_flat), fix join_msg format

- Remove is_dm column from channels table (dm detection by member count)
- Rename member name column to userinfo — extensible json_flat format
- Add json_flat.c/h library for compact JSON serialization
- Fix join_sig message format: use ch_x25519+ch_ed25519 instead of channel_id string
- Remove name from join_msg body (now in userinfo via json_flat)
- Update chat_core, chat_sync, member_sync, db_manager to new format
- Add topo_strategy.txt — архитектурные заметки по топологии
topo_upd
Evgeny 2 months ago
parent
commit
3dda701c67
  1. 2
      lib/Makefile.am
  2. 87
      lib/json_flat.c
  3. 25
      lib/json_flat.h
  4. 25
      src/conn_mgr.c
  5. 21
      src/conn_mgr.h
  6. 11
      src/conn_mgr_doc.md
  7. 23
      src/route_ping.h
  8. 4
      src/topo_group.c
  9. 176
      src/topo_group_doc.md
  10. 8
      src/topo_node.c
  11. 1
      src/topo_node.h
  12. 102
      src/topo_node_sqlite.c
  13. 6
      src/topo_node_sqlite.h
  14. 89
      src/topo_strategy.txt
  15. 12
      tests/test_conn_mgr.c
  16. 19
      tools/chatgui/db/db_manager.cpp
  17. 2
      tools/chatgui/db/db_manager.h
  18. 2
      tools/chatgui/doc/db_schema.md
  19. 1
      tools/chatgui/libutun/CMakeLists.txt
  20. 1
      tools/chatgui/src/mainwindow.cpp
  21. 28
      tools/chatgui/transport/chat_core.c
  22. 1
      tools/chatgui/transport/chat_core.h
  23. 147
      tools/chatgui/transport/chat_sync.c
  24. 18
      tools/chatgui/transport/member_sync.c
  25. 6
      tools/chatgui/transport/member_sync.h
  26. 2
      tools/chatgui/transport/merkle_sync.h

2
lib/Makefile.am

@ -27,6 +27,8 @@ libuasync_a_SOURCES = \
radix.h \
serialize.c \
serialize.h \
json_flat.c \
json_flat.h \
tcp_io.c \
tcp_io.h \
sqlite3.c \

87
lib/json_flat.c

@ -0,0 +1,87 @@
#include "json_flat.h"
#include <string.h>
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
#define CHECK(p, end) if ((p) >= (end)) return -1
static int json_read_string(const char** pp, const char* end, char* out, size_t out_sz) {
const char* p = *pp;
CHECK(p, end);
if (*p != '"') return -1; p++;
size_t n = 0;
while (p < end && *p != '"') {
char ch = *p;
if (*p == '\\') { p++; CHECK(p, end);
switch (*p) {
case '"': ch = '"'; break;
case '\\': ch = '\\'; break;
case '/': ch = '/'; break;
case 'n': ch = '\n'; break;
case 'r': ch = '\r'; break;
case 't': ch = '\t'; break;
default: return -1;
}
}
if (out && n < out_sz - 1) out[n++] = ch;
p++;
}
CHECK(p, end); /* closing quote */
p++;
if (out && n < out_sz) out[n] = '\0';
else if (out && out_sz > 0) out[out_sz - 1] = '\0';
*pp = p;
return 0;
}
static int json_parse_pair(const char** pp, const char* end,
json_flat_cb cb, void* arg) {
const char* p = *pp;
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') { *pp = p; return 0; }
char key[128], val[256];
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1;
SKIP_WS(p, end); CHECK(p, end);
if (*p != ':') return -1; p++;
SKIP_WS(p, end); CHECK(p, end);
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1;
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; }
SKIP_WS(p, end); CHECK(p, end);
if (*p == ',') p++;
*pp = p;
return 0;
}
int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
if (!json) return -1;
const char* p = json, *end = json + strlen(json);
SKIP_WS(p, end); CHECK(p, end);
if (*p != '{') return -1; p++;
for (;;) {
int rc = json_parse_pair(&p, end, cb, arg);
if (rc < 0) return -1;
if (rc > 0) return 0; /* callback stopped */
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') return 0;
}
}
struct get_ctx { const char* key; char* out; size_t sz; int found; };
static int get_cb(const char* k, const char* v, void* arg) {
struct get_ctx* g = (struct get_ctx*)arg;
if (strcmp(k, g->key) == 0) { if (g->out) { size_t vl = strlen(v);
size_t cp = vl < g->sz - 1 ? vl : g->sz - 1; memcpy(g->out, v, cp); g->out[cp] = '\0'; }
g->found = 1; return 1; }
return 0;
}
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
struct get_ctx g = { key, value_out, value_sz, 0 };
if (json_flat_parse(json, get_cb, &g) != 0) return -1;
return g.found ? 0 : -1;
}

25
lib/json_flat.h

@ -0,0 +1,25 @@
/**
* @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
*
* Только строковые значения. Без malloc, без зависимостей.
*/
#ifndef JSON_FLAT_H
#define JSON_FLAT_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);
#ifdef __cplusplus
}
#endif
#endif

25
src/conn_mgr.c

@ -201,7 +201,6 @@ int conn_mgr_connect_node(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_
entry->state = CONN_MGR_STATE_CONNECTED;
entry->conn_type = CONN_TYPE_DIRECT;
entry->idle_timeout_ms = idle_timeout_ms;
entry->alien = target->alien;
entry->last_traffic_tb = get_time_tb();
{ struct cm_cb_node* cn = u_calloc(1, sizeof(struct cm_cb_node));
if (cn) { cn->cb = cb; cn->arg = cb_arg; entry->cb_list = cn; } }
@ -213,7 +212,6 @@ int conn_mgr_connect_node(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_
entry = cm_ensure_entry(mgr, node_id);
if (!entry) return CONN_MGR_ERR_INTERNAL;
entry->state = CONN_MGR_STATE_CONNECTING;
entry->alien = target->alien;
{ struct cm_cb_node* cn = u_calloc(1, sizeof(struct cm_cb_node));
if (cn) { cn->cb = cb; cn->arg = cb_arg; cn->next = entry->cb_list; entry->cb_list = cn; } }
entry->idle_timeout_ms = idle_timeout_ms;
@ -269,29 +267,6 @@ int conn_mgr_get_status(struct CONN_MGR* mgr, uint64_t node_id, uint8_t* out_sta
return CONN_MGR_OK;
}
int conn_mgr_add_alien_node(struct CONN_MGR* mgr, const uint8_t* nodeinfo_data, size_t len) {
if (!mgr || !nodeinfo_data || len < sizeof(struct TOPO_NODE)) return CONN_MGR_ERR_INTERNAL;
struct TOPO_GROUP* group = topo_groups_get_default(mgr->instance->topo_groups);
if (!group) return CONN_MGR_ERR_INTERNAL;
const struct TOPO_NODE* src = (const struct TOPO_NODE*)nodeinfo_data;
uint64_t node_id = src->node_id;
struct TOPO_NODEQ* existing = topo_node_find_by_id(group, node_id);
if (existing) { DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: alien node 0x%016llx already exists, ignoring", (unsigned long long)node_id); return CONN_MGR_OK; }
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) return CONN_MGR_ERR_INTERNAL;
memcpy(ni, nodeinfo_data, len < sizeof(*ni) ? len : sizeof(*ni));
ni->ref_count = 1;
struct TOPO_NODEQ* nq = (struct TOPO_NODEQ*)queue_entry_new(sizeof(struct TOPO_NODEQ));
if (!nq) { u_free(ni); return CONN_MGR_ERR_INTERNAL; }
nq->node = ni; topo_node_ref(ni);
nq->hash_node_id = ni->node_id;
nq->alien = 1; nq->dirty = 0; nq->last_ver = ni->ver;
nq->connectivity.ping_req_time = 0;
if (group->nodes) queue_data_put_with_index(group->nodes, &nq->ll);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: added alien node 0x%016llx", (unsigned long long)node_id);
return CONN_MGR_OK;
}
int conn_mgr_send(struct CONN_MGR* mgr, uint64_t node_id, struct ll_entry* entry) {
if (!mgr || !entry) return -1;
struct CONN_MGR_ENTRY* e = cm_find_entry(mgr, node_id);

21
src/conn_mgr.h

@ -49,9 +49,7 @@ struct TOPO_GROUP;
* дополнительный callback в список
* 4. conn_mgr_disconnect_node() отправляет DISCONNECT-пакет целевой ноде и
* сбрасывает conn_mgr_type в NODEINFO_Q
* 5. Для alien-нод (подключившихся к нам самостоятельно) используется
* conn_mgr_add_alien_node() вместо conn_mgr_connect_node()
* 6. conn_mgr_update_best_candidates() вызывается извне (обычно из route_connectivity)
* 5. conn_mgr_update_best_candidates() вызывается извне (обычно из route_connectivity)
* для обновления RTT кандидатов
*
* @note Внутренние таймеры (всё в timebase-единицах, 1 tb = 0.1 мс):
@ -266,7 +264,6 @@ struct CONN_MGR_ENTRY {
uint64_t node_id; ///< Идентификатор целевой ноды
uint8_t state; ///< Текущее состояние (enum CONN_MGR_STATE)
uint8_t conn_type; ///< Тип соединения (CONN_TYPE_DIRECT/REVERSE/INDIRECT)
uint8_t alien; ///< 1 — alien-нода (сама подключилась к нам), 0 — мы инициаторы
uint32_t idle_timeout_ms; ///< Таймаут неактивности (мс), 0 — без таймаута
uint64_t last_traffic_tb; ///< Время последнего трафика (tb) для idle-проверки
void* idle_timer; ///< Хендл таймера idle-проверки
@ -355,7 +352,6 @@ void conn_mgr_destroy(struct CONN_MGR* mgr);
* @note Если нода уже подключена — сразу вызывает callback с CONN_MGR_OK и возвращает CONN_MGR_OK.
* @note Если нода в процессе подключения — добавляет callback в список ожидания.
* @note Автоматически определяет наличие BGP-соединения (уже существующий ETCP-линк).
* @note Для alien-нод используйте conn_mgr_add_alien_node().
*/
int conn_mgr_connect_node(struct CONN_MGR* mgr, uint64_t node_id,
uint32_t idle_timeout_ms,
@ -397,21 +393,6 @@ int conn_mgr_set_idle_timeout(struct CONN_MGR* mgr, uint64_t node_id, uint32_t t
int conn_mgr_get_status(struct CONN_MGR* mgr, uint64_t node_id,
uint8_t* out_state, uint8_t* out_conn_type);
/* ==================== Alien-ноды ==================== */
/**
* @brief Регистрирует alien-ноду (подключившуюся к нам самостоятельно).
* @param mgr менеджер
* @param nodeinfo_data сырые данные NODEINFO (BGP-пакет)
* @param len длина данных
* @return CONN_MGR_OK или CONN_MGR_ERR_INTERNAL
*
* Alien-нода — это нода, которая сама инициировала INIT-подключение к нам.
* Для неё не запускаются фазы подключения, сразу устанавливается CONN_MGR_STATE_CONNECTED.
* Создаёт entry с флагом alien=1.
*/
int conn_mgr_add_alien_node(struct CONN_MGR* mgr, const uint8_t* nodeinfo_data, size_t len);
/**
* @brief Отправляет данные подключённой ноде через etcp_router.
* @param mgr менеджер

11
src/conn_mgr_doc.md

@ -40,9 +40,6 @@ entry->dgram = data; entry->len = len;
conn_mgr_send(mgr, node_id, entry); // entry освобождается внутри
```
### Alien-ноды
Для нод, которые сами подключились к нам, используется `conn_mgr_add_alien_node()` — не запускает фазы подключения, сразу устанавливает `CONNECTED`.
### Кандидаты-посредники
Внешний код (обычно `route_connectivity`) вызывает `conn_mgr_update_best_candidates()` для заполнения списка лучших посредников (топ-3 по RTT). Список используется фазой INDIRECT.
@ -59,7 +56,7 @@ conn_mgr_send(mgr, node_id, entry); // entry освобождается внут
| Структура | Описание |
|-----------|----------|
| `CONN_MGR` | Корневая структура менеджера. Хранит массив `entries[]`, топ-3 `best_candidates`, таймеры (`bg_ping_timer`, `candidate_ping_timer`), списки `reverse_pending`/`exchange_pending`, `next_request_id` |
| `CONN_MGR_ENTRY` | Запись о подключении к одной ноде: `state`, `conn_type`, `alien`, `idle_timeout_ms`, список коллбэков `cb_list`, массив `intermediaries[]`, состояния `local_scan_state`/`main_connect_state`, текущая `main.phase` |
| `CONN_MGR_ENTRY` | Запись о подключении к одной ноде: `state`, `conn_type`, `idle_timeout_ms`, список коллбэков `cb_list`, массив `intermediaries[]`, состояния `local_scan_state`/`main_connect_state`, текущая `main.phase` |
| `CONN_MGR_CANDIDATE` | Пара `{node_id, rtt}` — запись о кандидате-посреднике (packed) |
| `cm_cb_node` | Узел связного списка коллбэков для асинхронного результата подключения |
| `cm_reverse_pending` | Запись об ожидании REVERSE-подключения (связный список) |
@ -82,12 +79,6 @@ conn_mgr_send(mgr, node_id, entry); // entry освобождается внут
| `conn_mgr_set_idle_timeout(mgr, id, ms)` | Устанавливает/снимает idle-таймер для ноды. При неактивности дольше `ms` — автоотключение |
| `conn_mgr_get_status(mgr, id, &state, &type)` | Возвращает текущее состояние и тип соединения |
### Alien-ноды
| Функция | Описание |
|---------|----------|
| `conn_mgr_add_alien_node(mgr, data, len)` | Регистрирует чужую ноду (сама подключилась к нам): копирует `TOPO_NODE`, добавляет в группу, помечает `alien=1` |
### Кандидаты-посредники
| Функция | Описание |

23
src/route_ping.h

@ -1,3 +1,15 @@
/**
* @file route_ping.h
* @brief Удалённый пинг через BGP-посредника.
*
* A просит B пропинговать C и вернуть сводную статистику:
* A --PING_REQ--> B --ETCP ping--> C
* A <--PING_RESP-- B (sent/ok/avg_rtt)
*
* Промежуточный узел задаётся ЯВНО в to_conn — вызывающий сам выбирает,
* через кого слать запрос. Автоподбор посредника (nat_check_find_third_node)
* находится в nat_detection.c, не здесь.
*/
#ifndef ROUTE_PING_H
#define ROUTE_PING_H
@ -9,6 +21,11 @@ extern "C" {
#include <stddef.h>
#include "nat_detection.h"
/** Запросить у to_conn серию пингов на target_ip:target_port.
* cb вызывается всегда ровно 1 раз:
* success=1 если хоть один пинг успешен (PONG получен)
* success=0 при общем таймауте или все пинги провалились
* @param to_conn промежуточный узел — выбирается вызывающим явно */
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* to_conn,
uint32_t target_ip, uint16_t target_port,
@ -16,11 +33,17 @@ int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group
uint16_t wait_timeout_ms, route_ping_callback_t cb,
void* arg, const uint8_t* pubkey);
/** Отменить все ожидающие ответы (при остановке NAT_DETECTION). */
void route_ping_destroy_pending(struct NAT_DETECTION* nd);
/** Отменить pending для conn (при разрыве/реините соединения). */
void route_ping_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn);
/** Входящий PING_REQ — мы посредник: пингуем цель, шлём PING_RESP. */
void route_ping_handle_req(struct NAT_DETECTION* nd, struct TOPO_GROUP* group,
struct ETCP_CONN* from_conn, const uint8_t* data, size_t len);
/** Входящий PING_RESP — мы инициатор: находим pending по request_id, вызываем cb. */
void route_ping_handle_resp(struct NAT_DETECTION* nd,
struct ETCP_CONN* from_conn, const uint8_t* data, size_t len);

4
src/topo_group.c

@ -356,9 +356,8 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
struct TOPO_NODEQ* peer_nq = topo_node_find_by_id(group, conn->peer_node_id);
topo_group_add_to_senders(group, conn);
if (peer_nq && peer_nq->alien) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "peer 0x%016llx is alien, skipping route exchange", (unsigned long long)conn->peer_node_id); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s ALIEN=1 — SKIP", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s alien=%d", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id, peer_nq ? peer_nq->alien : -1);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id);
topo_group_send_table_request(group, conn);
}
@ -561,7 +560,6 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
nodeinfo1->connectivity.nat_status = PROBE_RESULT_UNKNOWN;
nodeinfo1->connectivity.real_status = PROBE_RESULT_UNKNOWN;
nodeinfo1->connectivity.ping_req_time = 0;
nodeinfo1->alien = 0;
queue_data_put_with_index(group->nodes, &nodeinfo1->ll);
}

176
src/topo_group_doc.md

@ -11,7 +11,8 @@ BGP-подобный обмен топологией узлов между пи
Группа по умолчанию (UTUN) создаётся автоматически при `topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для chatgui). Узлы разных групп изолированы: peer из utun-группы не видит узлы чат-группы и наоборот.
Помимо обмена топологией, модуль выполняет:
- **NAT-детекцию** (NAT_INFO / NAT_CHECK_REQ): клиент запрашивает проверку своего NAT у сервера; сервер через третий узел пингует клиента и сообщает тип NAT (EIM/STRICT)
- **Сборку local_node** (`topo_group_update_my_nodeinfo`) — при старте и при изменениях конфигурации сокетов собирает информацию о себе (адреса, подсети, ключи)
- **NAT-детекцию** — выделена в отдельный самодостаточный модуль `nat_detection.h` (свой ETCP_ID 0x02, своя обработка пакетов); topo_group предоставляет делегат `topo_group_set_nat_check_local` для тестовых нужд
- **Поиск оптимального маршрута** до узла (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей (ETCP соединений)
- **Зондирование связности** — для новых узлов запускает `route_connectivity_probe_node()`
- **Персистентность в SQLite** — синхронизация узлов и membership в БД (таблицы nodes, node_addresses, peers_*)
@ -26,47 +27,49 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
Здесь же:
- `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — регистрирует приёмник пакетов топологии
- `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)` — подписывается на события on_up/on_down всех новых ETCP-соединений
- Открывается SQLite (если указан `db_path` в конфиге)
- Открывается SQLite (если указан `db_path` в конфиге), БД сохраняется в `instance->topo_sqlite_db`
- NAT-детекция инициализируется отдельно в `utun_instance.c` через `nat_detection_create(instance)`
### 2.2. Новое ETCP-соединение
При ETCP on_up автоматически вызывается `topo_group_new_conn(group, conn)`:
1. Добавляет conn в `senders_list` (через `topo_group_add_to_senders`)
2. Запускает NAT check для всех линков всех соединений
3. Отправляет `TOPO_SUBCMD_REQUEST_TABLE` — запрос полной таблицы
2. Отправляет `TOPO_SUBCMD_REQUEST_TABLE` — запрос полной таблицы
При приёме `TOPO_SUBCMD_REQUEST_TABLE` peer отправляет:
- NODEINFO c локальным узлом (local_node)
- Full table sync — все известные узлы, кроме тех, чей путь уже включает этого пира (защита от петель)
- `TOPO_SUBCMD_TABLE_COMPLETE` — метка завершения начальной синхронизации
- Full table sync — все известные узлы (с hop_count > 0), кроме тех, чей путь уже включает этого пира (защита от петель)
- `TOPO_SUBCMD_TABLE_COMPLETE` — метка завершения начальной синхронизации; при получении вызывает `etcp_set_routing_exchange_state(from_conn, 3)`
### 2.3. NODEINFO (обновление/создание узла)
`topo_group_process_nodeinfo(group, from, data, len)`:
1. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS`
1. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS`; при несовпадении — отправка `TOPO_SUBCMD_ERR_GROUP_MISMATCH`
2. Проверка `hop_count < MAX_HOPS(16)`
3. Проверка версии узла — если версия не новее (stale), только обновляет пути (hop_list) и выходит
4. Десериализация wire-формата → обновление/создание `TOPO_NODEQ`
5. Добавление пути (`topo_group_add_path`) через текущее соединение
6. Вставка в `routing` (`route_insert`) — только для UTUN-групп
7. Сохранение в SQLite (если есть)
8. Callback `node_updated_cb` (для chatgui member_sync)
9. Broadcast всем пирам, кроме hop_list (защита от петель)
10. Запуск зондирования связности для новых узлов с адресами
4. Десериализация wire-формата → обновление/создание `TOPO_NODEQ` (через `topo_node_deserialize`)
5. При создании нового узла: инициализация `connectivity` (PROBE_STATUS_NONE, PROBE_RESULT_UNKNOWN)
6. Добавление пути (`topo_group_add_path`) через текущее соединение, с очисткой старых путей содержащих peer_node_id (`topo_group_remove_path_by_hop`)
7. Вставка в `routing` (`route_insert`) — только для UTUN-групп
8. Сохранение в SQLite (если есть): `topo_node_sqlite_node_put` + для CHAT групп — `topo_node_sqlite_member_put`
9. Уведомление control_server (`control_server_notify_node_change`)
10. Callback `node_updated_cb` (для chatgui member_sync) — вызывается и при создании, и при обновлении
11. Broadcast всем пирам, кроме hop_list (защита от петель)
12. Запуск зондирования связности для новых узлов с адресами (`route_connectivity_probe_node`)
### 2.4. WITHDRAW (удаление узла)
`topo_group_process_withdraw(group, sender, data, len)`:
1. Поиск узла по `node_id`
2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление всех путей, содержащих wd_source в hop_list
3. Если путей не осталось (ret == 1): удаление из routing, уведомление control_server, broadcast withdraw дальше
4. Если группа CHAT — удаление member из SQLite
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 дальше
4. Если группа CHAT — удаление member из SQLite через `topo_node_sqlite_member_del`
### 2.5. Отключение соединения
При ETCP on_down вызывается `topo_group_remove_conn(group, conn)`:
1. Отмена pending ping-запросов для этого conn
2. Для всех узлов с путями через этот conn — `topo_group_remove_path`. Если узел стал полностью недоступен (нет путей) — удаляется из routing и nodes, с broadcast withdraw
1. Проверка наличия conn в senders_list (если уже удалён — выход)
2. Для всех узлов с путями через этот conn — `topo_group_remove_path`. Если узел стал полностью недоступен (нет путей) — удаляется из routing и nodes, устанавливается `dirty = 1`, отменяется connectivity probe, закрываются виртуальные каналы, broadcast withdraw
3. Удаление conn из `senders_list`
### 2.6. Поиск пути до узла
@ -74,17 +77,19 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
`topo_group_find_conn_for_node(group, node_id)` — перебирает `path`'ы узла, выбирает с минимальным `hop_count`. Предпочитает live-пути (links_up > 0). Если live-путей нет, fallback на любой путь с минимальным hop_count.
Интенсивно используется `etcp_router`:
- При маршрутизации пакетов к удалённому узлу (`src/etcp_router.c:262`)
- При маршрутизации пакетов к удалённому узлу
- При поиске next-hop для multihop соединений
### 2.7. NAT-детекция
1. При новом соединении вызывается `topo_group_start_link_nat_check` для каждого линка
2. Сервер ищет «третий узел» через `topo_group_find_third_node` — произвольный conn кроме проверяемого
3. Отправляет `PING_REQ` через третий узел на IP:port линка проверяемого клиента
4. По результату пинга: `NAT_TYPE_EIM` (если успех) или `NAT_TYPE_STRICT`
5. Результат сохраняется в `link->nat_type` и отправляется клиенту через `topo_group_send_nat_info`
6. Клиент обновляет свой `local_node` через `topo_group_handle_nat_info` и broadcast-ит изменения
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. Широковещательная рассылка
@ -98,26 +103,25 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
- CHAT-группа ожидает флаг = 0 (без подсетей)
- При несовпадении отправляется `TOPO_SUBCMD_ERR_GROUP_MISMATCH` и пакет игнорируется
### 2.10. Alien-узлы
Узлы, помеченные `alien=1`, не участвуют в обмене маршрутами: при `topo_group_new_conn` для alien-пира обмен пропускается.
## 3. API
### 3.1. Структуры
| Структура | Назначение |
|---|---|
| `TOPO_GROUPS` | Контейнер всех групп топологии экземпляра. Хранит `group_list` (ll_queue), memory_pool'ы для сериализации, sqlite3, callback `node_updated_cb` |
| `TOPO_GROUP` | Одна группа топологии. `group_id`, `group_type`, `nodes` (ll_queue узлов), `senders_list` (ll_queue conn'ов), `local_node` (TOPO_NODEQ) |
| `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_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_CONN_ITEM` | Элемент списка senders: conn + ll_entry — связка с senders_list |
| `TOPO_NODE` | Идентичность узла: pubkeys, имя, версия (ver), списки адресов (v4/v6 sock_meta, addrs). Разделяется между группами через ref_count |
| `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 |
| `TOPO_NODESUBNETS` | Подсети узла: списки TOPO_SUBNET4 / TOPO_SUBNET6 |
| `TOPO_CONNECTIVITY` | Состояние связности с узлом: probe_status, pending_count, rtt-метрики, статусы (interface/nat/real), probe_list |
| `TOPOMSG_NODEINFO_PKT` | Wire-формат пакета NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть |
| `TOPOMSG_WITHDRAW_PKT` | Wire-формат WITHDRAW: node_id (удаляемый) + wd_source (инициатор) |
| `TOPOMSG_NAT_INFO` | Wire-формат NAT_INFO: socket_id, IP, port (network byte order), nat_type |
| `TOPOMSG_NAT_CHECK_REQ` | Запрос проверки NAT от клиента к серверу: socket_id + interface IP:port |
| `TOPOMSG_TABLE_REQ` | Wire-формат запроса/завершения таблицы: cmd + subcmd |
| `TOPOMSG_ERR_GROUP_MISMATCH` | Wire-формат ошибки несоответствия типа группы: expected_type + received_flags |
| `nat_check_arg` | Контекст callback NAT-проверки: link + nat_ip + nat_port |
| `NAT_DETECTION` | Контекст NAT-детекции (отдельный модуль): ping_pending, next_ping_req_id, allow_nat_check_local |
### 3.2. Константы
@ -127,22 +131,29 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
| `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле + подсети |
| `TOPO_SUBCMD_REQUEST_TABLE` | 0x05 | Запрос полной таблицы |
| `TOPO_SUBCMD_WITHDRAW` | 0x06 | Узел стал недоступен |
| `TOPO_SUBCMD_NAT_INFO` | 0x09 | Информация о типе NAT клиента |
| `TOPO_SUBCMD_NAT_CHECK_REQ` | 0x0A | Запрос проверки NAT для сокета |
| `TOPO_SUBCMD_TABLE_COMPLETE` | 0x0B | Завершение начальной синхронизации |
| `TOPO_SUBCMD_ERR_GROUP_MISMATCH` | 0x0C | Ошибка несоответствия типа группы |
| `MAX_HOPS` | 16 | Максимальная длина hop-листа |
| `BGP_NODES_HASH_SIZE` | 256 | Размер хеш-таблицы nodes |
| `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id по умолчанию для utun-группы |
| `TOPO_GROUP_TYPE_UTUN` | 1 | Тип группы: VPN-сеть (подсети, маршруты) |
| `TOPO_GROUP_TYPE_CHAT` | 2 | Тип группы: чат (без подсетей) |
| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг NODEINFO: узел передаёт подсети |
| `PROBE_STATUS_NONE` | 0 | Связность: не проверялась |
| `PROBE_STATUS_IN_PROGRESS` | 1 | Связность: проверка идёт |
| `PROBE_STATUS_DONE` | 2 | Связность: проверка завершена |
| `PROBE_RESULT_UNKNOWN` | 0 | Результат проверки: неизвестно |
| `PROBE_RESULT_REACHABLE` | 1 | Результат проверки: доступен |
| `PROBE_RESULT_UNREACHABLE` | 2 | Результат проверки: недоступен |
### 3.3. Функции инициализации/завершения
| Функция | Назначение |
|---|---|
| `topo_groups_init(instance)` | Создаёт TOPO_GROUPS, memory_pool'ы, открывает SQLite, создаёт utun-группу по умолчанию, регистрирует ETCP-коллбэки |
| `topo_groups_destroy(instance)` | Освобождает все группы, memory_pool'ы, закрывает SQLite; снимает ETCP-регистрацию |
| `topo_group_create(instance, group_id, group_type)` | Создаёт TOPO_GROUP: nodes (hash queue), senders_list, local_node; вызывается из topo_groups_init / topo_groups_create_group |
| `topo_group_destroy(group)` | Освобождает senders_list, local_node, nodes очереди; узлы должны быть удалены заранее |
| `topo_groups_init(instance)` | Создаёт TOPO_GROUPS, memory_pool'ы, открывает SQLite (если db_path в конфиге), создаёт utun-группу по умолчанию, регистрирует ETCP-коллбэки |
| `topo_groups_destroy(instance)` | Освобождает все группы, memory_pool'ы, закрывает SQLite, отменяет connectivity probes, снимает ETCP-регистрацию |
| `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_destroy(group)` | Освобождает senders_list, local_node (free_lists), nodes очереди |
| `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создаёт новую группу и добавляет в group_list; для CHAT устанавливает channel_id |
| `topo_groups_find(g, group_id)` | Поиск группы по group_id через хеш-индекс group_list |
| `topo_groups_get_default(g)` | Возвращает utun-группу по умолчанию (group_id=TOPO_GROUP_UTUN) |
@ -151,8 +162,8 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
| Функция | Назначение |
|---|---|
| `topo_group_new_conn(group, conn)` | Добавляет conn в senders_list, запускает NAT check для всех линков, отправляет REQUEST_TABLE; alien-пиры пропускаются |
| `topo_group_remove_conn(group, conn)` | Отменяет NAT check и ping pending, удаляет все пути через conn, удаляет недоступные узлы, broadcast withdraw, убирает conn из senders_list |
| `topo_group_new_conn(group, conn)` | Добавляет conn в senders_list, отправляет REQUEST_TABLE |
| `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_etcp_conn_cbk(conn, arg)` | Callback при создании нового ETCP-соединения: подписывается на on_up/on_down через etcp_conn_add_up_cbk/down_cbk |
| `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 |
@ -161,41 +172,41 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
| Функция | Назначение |
|---|---|
| `topo_group_receive_cbk(from_conn, entry)` | Точка входа приёма пакетов топологии: диспетчеризация по subcmd (NODEINFO/WITHDRAW/REQUEST_TABLE/PING_REQ/PING_RESP/NAT_INFO/NAT_CHECK_REQ/TABLE_COMPLETE/ERR_GROUP_MISMATCH) |
| `topo_group_process_nodeinfo(group, from, data, len)` | Обработка NODEINFO: проверка группы/версии/hops, десериализация, обновление/создание TOPO_NODEQ, пути, routing, SQLite, broadcast, зондирование связности |
| `topo_group_process_withdraw(group, sender, data, len)` | Обработка WITHDRAW: удаление путей с wd_source, если узел недоступен — удаление из routing, broadcast |
| `topo_group_handle_request_table(group, conn)` | Обработка REQUEST_TABLE: отправляет NODEINFO (local_node) + full table sync + TABLE_COMPLETE |
| `topo_group_handle_nat_info(group, from_conn, data, len)` | Обработка NAT_INFO: обновляет nat_type в local_node (TOPO_SOCKMETA4), ETCP_SOCKET, при изменениях broadcast NODEINFO |
| `topo_group_handle_nat_check_req(group, from_conn, data, len)` | Обработка NAT_CHECK_REQ от клиента: находит линк и запускает NAT-проверку |
| `topo_group_receive_cbk(from_conn, entry)` | Точка входа приёма пакетов топологии: работает только с default-группой, диспетчеризация по subcmd (NODEINFO/WITHDRAW/REQUEST_TABLE/TABLE_COMPLETE/ERR_GROUP_MISMATCH) |
| `topo_group_process_nodeinfo(group, from, data, len)` | Обработка NODEINFO: проверка группы/версии/hops, десериализация, обновление/создание TOPO_NODEQ, пути, routing, SQLite, broadcast, зондирование связности, node_updated_cb |
| `topo_group_process_withdraw(group, sender, data, len)` | Обработка WITHDRAW: удаление путей с wd_source, если узел недоступен — полная очистка (routing, connectivity, paths, etcp_router), broadcast |
| `topo_group_handle_request_table(group, conn)` | Обработка REQUEST_TABLE: отправляет NODEINFO (local_node) + full table sync + add_to_senders + TABLE_COMPLETE |
### 3.6. Функции поиска
| Функция | Назначение |
|---|---|
| `topo_group_find_conn_for_node(group, node_id)` | Поиск оптимального ETCP_CONN до узла: min hop_count, предпочтение live-путям |
| `topo_group_find_third_node(group, exclude)` | Поиск произвольного conn'а (не exclude) для использования как посредника при NAT-детекции; исключает private/local conn'ы |
| `topo_node_find_by_id(group, node_id)` | Поиск TOPO_NODEQ в nodes по node_id через хеш-индекс |
| `topo_groups_find(g, group_id)` | Поиск группы по group_id |
### 3.7. Функции broadcast
| Функция | Назначение |
|---|---|
| `topo_group_send_nodeinfo(group, node, conn)` | Сериализует TOPO_NODEQ и отправляет NODEINFO одному conn |
| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW всем пирам (without exclude) |
| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW всем пирам (без exclude) |
| `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Рассылает WITHDRAW всем conn в senders_list, кроме exclude |
| `topo_group_send_full_table(group, conn)` | Отправляет все известные узлы (кроме тех, чей путь включает target) указанному conn |
| `topo_group_send_full_table(group, conn)` | Отправляет все известные узлы (с hop_count > 0), кроме тех, чей путь включает target, указанному conn |
| `topo_group_send_table_request(group, conn)` | Отправляет запрос полной таблицы (TOPO_SUBCMD_REQUEST_TABLE) |
| `topo_group_send_table_complete(group, conn)` | Отправляет метку завершения синхронизации (TOPO_SUBCMD_TABLE_COMPLETE) |
| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку несоответствия типа группы |
### 3.8. Функции NAT
### 3.8. Функции NAT (делегаты в nat_detection)
| Функция | Назначение |
|---|---|
| `topo_group_send_nat_info(conn, socket_id, nat_ip, nat_port, nat_type)` | Отправляет клиенту информацию о его NAT (результат проверки) |
| `topo_group_send_nat_check_req(conn, socket_id)` | Отправляет серверу запрос на проверку NAT для указанного socket_id |
| `topo_group_start_link_nat_check(group, link)` | Запускает NAT-проверку для одного линка через третий узел; проверяет is_local_subnet, пропускает private/local линки |
| `topo_group_request_nat_check_all(group)` | Запускает NAT check для всех линков всех соединений |
| `topo_group_set_nat_check_local(group, allow)` | Разрешить/запретить NAT check для локальных подсетей (127.x, 10.x, 192.168.x, 172.16-31.x) |
| `topo_group_set_nat_check_local(group, allow)` | Делегат в `nat_detection_set_allow_local(instance->nat_det, allow)` — разрешить/запретить NAT check для локальных подсетей (тесты) |
| `nat_detection_create(inst)` | Создаёт модуль NAT-детекции: etcp_bind на 0x02, подписка на conn_up/conn_down |
| `nat_detection_destroy(nd)` | Уничтожает модуль NAT-детекции |
| `nat_detection_link_ready(nd, link)` | Запускает NAT-чек для линка после INIT handshake |
| `nat_detection_cancel_for_conn(nd, conn)` | Отменяет все pending NAT-пинги для соединения |
| `nat_detection_request_check_all(nd)` | Принудительная перепроверка NAT для всех линков |
### 3.9. Функции путей
@ -205,15 +216,14 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
| `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 не в hop_list) |
| `topo_group_should_send_to(nq, target_id)` | Проверяет, нужно ли отправлять NODEINFO узла указанному пиру (target_id не во всех путях) |
### 3.10. Callbacks/вспомогательные
| Функция | Назначение |
|---|---|
| `topo_groups_set_node_updated_cb(groups, fn)` | Устанавливает callback при обновлении узла (используется chatgui для member_sync) |
| `topo_groups_set_sqlite_db(g, db)` | Устанавливает внешнюю БД SQLite |
| `nat_link_check_cb(success, avg_rtt, count_sent, count_ok, arg)` | Callback завершения NAT-проверки: устанавливает nat_type линка и отправляет результат клиенту |
| `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 для логов |
@ -230,36 +240,39 @@ struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт конт
| Модуль | Как используется |
|---|---|
| `topo_node.c/h` | Модель данных узла: TOPO_NODE, TOPO_NODEQ, ser/deserialize, find_by_id, update_my_nodeinfo, free_lists |
| `topo_node.c/h` | Модель данных узла: TOPO_NODE, TOPO_NODEQ, ser/deserialize, find_by_id, update_my_nodeinfo, free_lists, dump_all, format_all |
| `topo_node_sqlite.h` | Персистентность в SQLite: topo_node_sqlite_node_put, member_put, member_del, init |
| `nat_detection.h` | NAT-детекция: отдельный модуль с собственным ETCP_ID 0x02; делегат set_nat_check_local |
| `route_lib.h` | Таблица маршрутов: route_insert, route_delete |
| `route_ping.h` | NAT-детекция и проверка связности: route_ping_send_req_addr, handle_req, handle_resp, cancel_for_conn |
| `route_connectivity.h` | Зондирование связности узла: route_connectivity_probe_node, cancel_node, cancel_all |
| `etcp_api.h` | Регистрация приёмника: etcp_bind, etcp_add_new_conn_cbk, etcp_unbind |
| `etcp_connections.h` | Управление соединениями: etcp_conn_add_up_cbk, etcp_conn_add_down_cbk, ETCP_CONN, ETCP_LINK |
| `etcp.h` | Отправка пакетов: etcp_send |
| `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 |
| `control_server.h` | Уведомления мониторинга: control_server_notify_node_change, control_server_notify_node_removed |
| `conn_mgr.h` | Менеджер соединений: использует topo_group_find_conn_for_node для поиска путей |
| `utun_instance.h` | UTUN_INSTANCE: конфиг, node_id, ключи, rt, topo_groups |
| `utun_instance.h` | UTUN_INSTANCE: конфиг, node_id, ключи, rt, topo_groups, nat_det, topo_sqlite_db, control_srv |
| `secure_channel.h` | Ключи: sc_derive_ed25519_pubkey |
| `memory_pool.h` | Память для сериализации: memory_pool_init / destroy |
| `debug_config.h` | Логирование: DEBUG_INFO/WARN/ERROR/DEBUG с категорией BGP |
| `mem.h` | Аллокация: u_malloc, u_free, u_calloc |
| `platform_compat.h` | Кроссплатформенная совместимость |
### 4.2. Кто вызывает topo_group
| Вызывающий | Что вызывает | Для чего |
|---|---|---|
| `utun_instance.c` | `topo_groups_init`, `topo_groups_destroy` | Инициализация/завершение при старте/остановке utun |
| `etcp_connections.c` | `topo_group_send_nodeinfo` | При изменении конфигурации сокетов — broadcast обновлённого local_node |
| `etcp_connections.c` | `topo_group_update_my_nodeinfo` | При изменении конфигурации сокетов — обновление local_node |
| `etcp_router.c` | `topo_group_find_conn_for_node` | Поиск next-hop для маршрутов и multihop-соединений (~15 вызовов) |
| `conn_mgr.c` | `topo_group_find_conn_for_node` | Поиск пути для установки прямого/обратного/indirect соединения |
| `nat_detection.c` | `topo_group_update_my_nodeinfo` | При обновлении NAT-типа сокета — обновление local_node и broadcast |
| Тесты | `topo_groups_get_default`, `topo_group_find_conn_for_node`, `topo_node_find_by_id`, `topo_group_set_nat_check_local` | 9 тестовых файлов |
### 4.3. ETCP-коллбэки (верхний уровень)
Точка входа для пакетов топологии — `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` в `topo_groups_init`. Единственный обработчик на весь ETCP ID 0x01. Диспетчеризация внутри по subcmd (byte data[1]).
Точка входа для пакетов топологии — `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 подписывается на его события.
@ -280,26 +293,21 @@ cmd(1) | subcmd(1) | node_id(8) | wd_source(8) = 18 байт
cmd(1) | subcmd(1) = 2 байта
```
### 5.4. NAT_INFO
```
cmd(1) | subcmd(1) | socket_id(1) | nat_ip(4) | nat_port(2) | nat_type(1) = 10 байт
```
### 5.5. NAT_CHECK_REQ
```
cmd(1) | subcmd(1) | socket_id(1) | interface_ip(4) | interface_port(2) = 9 байт
```
### 5.6. ERR_GROUP_MISMATCH
### 5.4. ERR_GROUP_MISMATCH
```
cmd(1) | subcmd(1) | expected_type(1) | received_flags(1) = 4 байта
```
## 6. Примечания
- **Хранилище памяти**: В отличие от STCP, модуль не имеет отдельной БД. Использует внешний SQLite3 pointer (`topo_groups->topo_sqlite_db`), который может быть открыт самим модулем (если указан `db_path`) или передан из chatgui через `topo_groups_set_sqlite_db`.
- **Версионирование NODEINFO**: Версия (ver, uint8_t 1-255) сравнивается через циклическую арифметику `(int8_t)(cur_ver - new_ver) >= 0`. Версия 0 не используется (нет данных).
- **Защита от петель**: NODEINFO рассылается только тем пирам, чей node_id отсутствует в hop_list. WITHDRAW broadcast всем кроме отправителя.
- **NAT check для локальных сетей**: По умолчанию запрещён (`allow_nat_check_local=0`), можно включить через `topo_group_set_nat_check_local(group, 1)` для тестов.
- **Aliens**: Узлы с `alien=1` хранятся в nodes, имеют пути, но не инициируют обмен маршрутами при подключении (topo_group_new_conn пропускает).
- **Обновление при NAT-детекции**: При получении NAT_INFO, если изменился nat_type сокета с не-EIM на EIM (или наоборот) — обновляется `local_node`, увеличивается версия и broadcast-ится всем пирам.
- **Хранилище SQLite**: Открывается в `topo_groups_init` если в конфиге задан `db_path`. БД хранится как `instance->topo_sqlite_db` (sqlite3*), используется во всех группах. Файл: `{db_path}/chats.db`. Режим: WAL, foreign_keys=ON, synchronous=NORMAL.
- **Версионирование NODEINFO**: Версия (ver, uint8_t 1-255) сравнивается через циклическую арифметику `(int8_t)(cur_ver - new_ver) >= 0`. Версия 0 не используется (нет данных). Если версия stale — обновляются только пути без замены данных узла.
- **Защита от петель**: NODEINFO рассылается только тем пирам, чей node_id отсутствует во всех путях (hop_list). WITHDRAW broadcast всем кроме отправителя.
- **Связность узлов**: Для новых/обновлённых узлов с адресами автоматически запускается зондирование (`route_connectivity_probe_node`). При удалении узла (withdraw/remove_conn) пробы отменяются (`route_connectivity_cancel_node`).
- **Виртуальные каналы**: При удалении узла закрываются все виртуальные ETCP-каналы через `etcp_router_conn_close_all_for_node`.
- **Dirty-флаг**: При withdraw/remove_conn узлы помечаются `dirty = 1` перед удалением.
- **NAT check для локальных сетей**: По умолчанию запрещён (`allow_nat_check_local=0`), можно включить через `topo_group_set_nat_check_local(group, 1)` для тестов (делегирует в `nat_detection_set_allow_local`).
- **Conn_mgr поля**: `conn_mgr_type`, `conn_mgr_intermediaries[]`, `conn_mgr_intermediariy_count` — используются менеджером соединений для стратегии установки связи с узлом (direct/reverse/indirect).
- **Обновление local_node при NAT-детекции**: Модуль `nat_detection` после определения типа NAT вызывает `topo_group_update_my_nodeinfo()` для обновления local_node с новыми данными сокетов — это автоматически увеличивает версию, и NODEINFO broadcast-ится всем пирам через механизм изменения конфигурации сокетов.
(End of file - total 302 lines)

8
src/topo_node.c

@ -297,8 +297,8 @@ void topo_node_dump_all(struct TOPO_GROUP* group) {
int is_local = (group->local_node == nq);
const char* name_s = ni->node_name ? ni->node_name : "";
DEBUG_INFO(DEBUG_CATEGORY_BGP, "--- Node #%d: id=%016llx name=\"%s\" ver=%u dirty=%u alien=%u grp=%llu%s ---",
idx, (unsigned long long)ni->node_id, name_s, ni->ver, nq->dirty, nq->alien,
DEBUG_INFO(DEBUG_CATEGORY_BGP, "--- Node #%d: id=%016llx name=\"%s\" ver=%u dirty=%u grp=%llu%s ---",
idx, (unsigned long long)ni->node_id, name_s, ni->ver, nq->dirty,
(unsigned long long)ni->group_id, is_local ? " [LOCAL]" : "");
if (nq->best_socket) DEBUG_INFO(DEBUG_CATEGORY_BGP, " best_socket=%p", (void*)nq->best_socket);
@ -395,8 +395,8 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
#define FMT_ADD(fmt, ...) pos += snprintf(buf + pos, buf_size > (size_t)pos ? buf_size - pos : 0, fmt, ##__VA_ARGS__)
FMT_ADD("--- Node #%d: id=%016llx name=\"%s\" ver=%u dirty=%u alien=%u grp=%llu%s ---\n",
idx, (unsigned long long)ni->node_id, name_s, ni->ver, nq->dirty, nq->alien,
FMT_ADD("--- Node #%d: id=%016llx name=\"%s\" ver=%u dirty=%u grp=%llu%s ---\n",
idx, (unsigned long long)ni->node_id, name_s, ni->ver, nq->dirty,
(unsigned long long)ni->group_id, is_local ? " [LOCAL]" : "");
if (nq->best_socket) FMT_ADD(" best_socket=%p\n", (void*)nq->best_socket);

1
src/topo_node.h

@ -234,7 +234,6 @@ struct TOPO_NODEQ {
struct ll_queue* paths;
uint8_t dirty;
uint8_t last_ver;
uint8_t alien;
uint8_t conn_mgr_type;
uint64_t conn_mgr_intermediaries[CONN_MGR_MAX_INTERMEDIARIES];
uint8_t conn_mgr_intermediariy_count;

102
src/topo_node_sqlite.c

@ -57,18 +57,17 @@ int topo_node_sqlite_init(sqlite3* db) {
"CREATE UNIQUE INDEX IF NOT EXISTS idx_na_unique ON node_addresses(node_id, family, socket_id, addr_type);"
"CREATE TABLE IF NOT EXISTS channels ("
" channel_id TEXT PRIMARY KEY,"
" name TEXT NOT NULL,"
" owner_node_id INTEGER,"
" is_dm INTEGER DEFAULT 0,"
" x25519_pubkey BLOB NOT NULL,"
" x25519_privkey BLOB,"
" ed25519_pubkey BLOB NOT NULL,"
" ed25519_privkey BLOB,"
" signature BLOB NOT NULL,"
" last_read_msg_id INTEGER,"
" last_pos_msg_id INTEGER,"
" created_at INTEGER DEFAULT (unixepoch())"
" channel_id TEXT PRIMARY KEY," // ID канала
" name TEXT NOT NULL," // Название канала
" owner_node_id INTEGER," // node id создателя (надо выпилить)
" x25519_pubkey BLOB NOT NULL," // ключ для шифрования канала
" x25519_privkey BLOB," // если админ - то ключ канала
" ed25519_pubkey BLOB NOT NULL," // ключ для проверки подписи
" ed25519_privkey BLOB," // ключ для создания подписи
" signature BLOB NOT NULL," // подпись при создании канал приватным ключом канала channel_id(NUL-terminated) || name(NUL-terminated) || owner_node_id(8 байт, LE uint64) || x25519_pubkey(32 байта) || ed25519_pubkey(32 байта)
" last_read_msg_id INTEGER," // для скроллинга чата
" last_pos_msg_id INTEGER," // для скроллинга чата
" created_at INTEGER DEFAULT (unixepoch())" // когда создан
");";
char* err = NULL;
@ -79,14 +78,6 @@ int topo_node_sqlite_init(sqlite3* db) {
return -1;
}
/* migration: add socket_id column if not exists (ignore error for existing DBs) */
sqlite3_exec(db,
"ALTER TABLE node_addresses ADD COLUMN socket_id INTEGER NOT NULL DEFAULT 0",
NULL, NULL, NULL);
sqlite3_exec(db,
"CREATE UNIQUE INDEX IF NOT EXISTS idx_na_unique ON node_addresses(node_id, family, socket_id, addr_type)",
NULL, NULL, NULL);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite tables initialized");
return 0;
}
@ -234,7 +225,7 @@ int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_NODEQ* nq) {
}
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
const char* name, int is_dm, uint64_t owner_node_id,
const char* name, uint64_t owner_node_id,
const uint8_t* x25519_pub, const uint8_t* x25519_priv,
const uint8_t* ed25519_pub, const uint8_t* ed25519_priv,
const uint8_t* signature) {
@ -243,18 +234,19 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[512];
snprintf(sql, sizeof(sql),
"CREATE TABLE IF NOT EXISTS \"%s\" ("
" node_id INTEGER NOT NULL,"
" x25519_pubkey BLOB NOT NULL,"
" ed25519_pubkey BLOB NOT NULL,"
" join_sig BLOB NOT NULL,"
" join_ts INTEGER NOT NULL,"
" update_sig BLOB,"
" update_ts INTEGER DEFAULT 0,"
" name TEXT NOT NULL DEFAULT '',"
" creator_sig BLOB,"
" comment TEXT,"
" joined_at INTEGER DEFAULT (unixepoch()),"
"CREATE TABLE IF NOT EXISTS \"%s\" (" // таблица мемберов канала
" node_id INTEGER NOT NULL," // ид меббера группы
" x25519_pubkey BLOB NOT NULL," // ключ мембера для шифрования
" ed25519_pubkey BLOB NOT NULL," // ключ мембера для подписи
" join_sig BLOB NOT NULL," // подпись мембера неизменяемая (при присоединении) сh_x25519_pubkey(32) || ch_ed25519_pubkey(32) || node_id(8 LE) || node_x25519_pubkey(32) || join_ts(8 LE)
" join_ts INTEGER NOT NULL," // время присоединения
" update_sig BLOB," // подпись мембера изменяемая join_sig(64) || update_ts(8) || userinfo (NUL-terminated)
" update_ts INTEGER DEFAULT 0,"// время подписи
" userinfo TEXT NOT NULL DEFAULT ''," // имя/метаданные узла в канале (json_flat style)
" creator_sig BLOB," // валидация мембера админом канала
" adm_tags TEXT," // тэги которые может менять админ (json_flat style)
" adm_tags_sig BLOB," // подпись тэгов adm_tags (может быть подписана ключом суперадмина или админов - проверяем перебором)
" node_type INTEGER NOT NULL," // ENUM(0-'PRIV',1-'NAT',2-'EIM-NAT',3-'PUBLIC',4-SUPERNODE) тип ноды - private (только нода подключается), nat (односторонние подключения), eim-nat (доступны подключения но надо запрашивать ip), public - на прямых ip (но может быть недоступна в чебурнетах), supernode - на прямых ip, рекомендуемая для подключения
" PRIMARY KEY (node_id))", peers_tbl);
char* err = NULL;
sqlite3_exec(db, sql, NULL, NULL, &err);
@ -262,20 +254,19 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"INSERT OR REPLACE INTO channels(channel_id, name, owner_node_id, is_dm,"
"INSERT OR REPLACE INTO channels(channel_id, name, owner_node_id,"
" x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature)"
" VALUES(?,?,?,?,?,?,?,?,?)", -1, &stmt, NULL) != SQLITE_OK) return -1;
" VALUES(?,?,?,?,?,?,?,?)", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id);
sqlite3_bind_int(stmt, 4, is_dm);
sqlite3_bind_blob(stmt, 5, x25519_pub, 32, SQLITE_STATIC);
if (x25519_priv) sqlite3_bind_blob(stmt, 6, x25519_priv, 32, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 6);
sqlite3_bind_blob(stmt, 7, ed25519_pub, 32, SQLITE_STATIC);
if (ed25519_priv) sqlite3_bind_blob(stmt, 8, ed25519_priv, 32, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 8);
sqlite3_bind_blob(stmt, 9, signature, 64, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 4, x25519_pub, 32, SQLITE_STATIC);
if (x25519_priv) sqlite3_bind_blob(stmt, 5, x25519_priv, 32, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 5);
sqlite3_bind_blob(stmt, 6, ed25519_pub, 32, SQLITE_STATIC);
if (ed25519_priv) sqlite3_bind_blob(stmt, 7, ed25519_priv, 32, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 7);
sqlite3_bind_blob(stmt, 8, signature, 64, SQLITE_STATIC);
int rc = sqlite3_step(stmt); sqlite3_finalize(stmt);
return rc == SQLITE_DONE ? 0 : -1;
}
@ -283,14 +274,14 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* name,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* userinfo,
const uint8_t* creator_sig) {
if (!db || !channel_id || !x25519_pubkey || !ed25519_pubkey) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO \"%s\"(node_id, x25519_pubkey, ed25519_pubkey,"
" join_sig, join_ts, update_sig, update_ts, name, creator_sig)"
" join_sig, join_ts, update_sig, update_ts, userinfo, creator_sig)"
" VALUES(?,?,?,?,?,?,?,?,?)", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
@ -303,7 +294,7 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no
if (update_sig) sqlite3_bind_blob(stmt, 6, update_sig, PEERS_UPDATE_SIG_SIZE, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 6);
sqlite3_bind_int64(stmt, 7, (sqlite3_int64)update_ts);
sqlite3_bind_text(stmt, 8, name ? name : "", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 8, userinfo ? userinfo : "", -1, SQLITE_STATIC);
if (creator_sig) sqlite3_bind_blob(stmt, 9, creator_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 9);
int rc = sqlite3_step(stmt); sqlite3_finalize(stmt);
@ -359,12 +350,12 @@ int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t no
}
int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
char* name_out, int name_sz, int* is_dm, uint64_t* owner_node_id,
char* name_out, int name_sz, uint64_t* owner_node_id,
uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature) {
if (!db || !channel_id) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name, owner_node_id, is_dm, x25519_pubkey, ed25519_pubkey, signature"
"SELECT name, owner_node_id, x25519_pubkey, ed25519_pubkey, signature"
" FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
@ -373,10 +364,9 @@ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
if (txt) snprintf(name_out, name_sz, "%s", txt); else name_out[0] = '\0';
}
if (owner_node_id) *owner_node_id = (uint64_t)sqlite3_column_int64(stmt, 1);
if (is_dm) *is_dm = sqlite3_column_int(stmt, 2);
if (x25519_pub) memcpy(x25519_pub, sqlite3_column_blob(stmt, 3), 32);
if (ed25519_pub) memcpy(ed25519_pub, sqlite3_column_blob(stmt, 4), 32);
if (signature) memcpy(signature, sqlite3_column_blob(stmt, 5), 64);
if (x25519_pub) memcpy(x25519_pub, sqlite3_column_blob(stmt, 2), 32);
if (ed25519_pub) memcpy(ed25519_pub, sqlite3_column_blob(stmt, 3), 32);
if (signature) memcpy(signature, sqlite3_column_blob(stmt, 4), 64);
sqlite3_finalize(stmt);
return 0;
}
@ -390,7 +380,7 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
char sql[512];
snprintf(sql, sizeof(sql),
"SELECT node_id, x25519_pubkey, ed25519_pubkey,"
" join_sig, join_ts, update_sig, update_ts, name"
" join_sig, join_ts, update_sig, update_ts, userinfo"
" FROM \"%s\" ORDER BY node_id ASC", peers_tbl);
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
@ -407,9 +397,9 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
uint64_t join_ts = (uint64_t)sqlite3_column_int64(stmt, 4);
const uint8_t* update_sig = (const uint8_t*)sqlite3_column_blob(stmt, 5);
uint64_t update_ts = (uint64_t)sqlite3_column_int64(stmt, 6);
const char* pname = (const char*)sqlite3_column_text(stmt, 7);
const char* puserinfo = (const char*)sqlite3_column_text(stmt, 7);
if (!x25519 || !ed_pub) continue;
uint8_t nl = pname ? (uint8_t)strnlen(pname, 255) : 0;
uint8_t nl = puserinfo ? (uint8_t)strnlen(puserinfo, 255) : 0;
sqlite3_stmt* ast = NULL;
snprintf(sql, sizeof(sql),
@ -464,7 +454,7 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
uint64_t z = 0; memcpy(buf + off, &z, 8); off += 8;
}
buf[off++] = nl;
if (nl) { memcpy(buf + off, pname, nl); off += nl; }
if (nl) { memcpy(buf + off, puserinfo, nl); off += nl; }
buf[off++] = addr_cnt;
off += addr_sz;
(*cnt_ptr)++;

6
src/topo_node_sqlite.h

@ -19,7 +19,7 @@ int topo_node_sqlite_init(sqlite3* db);
int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_NODEQ* nq);
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
const char* name, int is_dm, uint64_t owner_node_id,
const char* name, uint64_t owner_node_id,
const uint8_t* x25519_pub, const uint8_t* x25519_priv,
const uint8_t* ed25519_pub, const uint8_t* ed25519_priv,
const uint8_t* signature);
@ -27,7 +27,7 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* name,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* userinfo,
const uint8_t* creator_sig);
int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64_t node_id,
@ -39,7 +39,7 @@ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts);
int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
char* name_out, int name_sz, int* is_dm, uint64_t* owner_node_id,
char* name_out, int name_sz, uint64_t* owner_node_id,
uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature);
int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,

89
src/topo_strategy.txt

@ -0,0 +1,89 @@
++ OOP:
class Animal {
public: // доступен всем
//private: // только внутри класса
//protected: // внутри класса и наследников
virtual void sound() {play(...);}// virtual — разрешает переопределение.
virtual void sound2() = 0;// если =0 - метод обязательно должен быть реализован в наследнике, а сам класс становится абстрактным и его экземпляры создавать нельзя.
Animal() {// конструктор - совпадает с именем класса
cout << "Created";
}
~Animal() {// деструктор
cout << "Deleted";
}
};
class Dog : public Animal {// наследование
public:
void sound() override {}// override — говорит компилятору, что метод действительно переопределяет базовый.
};
Стратегия обмена
узлы которые online живут только в пемяти. и синхронизируемся с ними используя topo_group.
routing layer:
- topo_group - обеспечивает корректное построение оптимальной таблицы роутинга (per group) между узлами в рамках установленных подключений. И корректировку этой таблицы при перестроении подключений.
- topo_group_discovery - обеспечивает оптимизацию подключений, а именно:
- периодически пингует узлы проверяя их доступность
для каждой группы админ назначает суперузлы: a b c ... стать усуперузлом - получить разрешение с админской подписью.
суперузел активирует bgp обмен с остаьлными суперузлами.
как:
суперузел - это по сути сервер для группы. который также синхронизируется с другими суперузлами
клиенты приоритетно подключаются к суперузлам:
- подключаются к кому-то
- запрашивают таблицу маршрутизации
1. Узлы за nat сами выбирают подходящие для себя точки подключения к сети.
2 шт с минимальным rtt
2 шт с минимальной загруженностью
в чате (gui) можно выбрать предпочитаемые узлы для подключения
2. узлы с eim nat
??? если не хватает узлов прямым адресом
3. узлы с прямыми ип
суть - выстраивают линки между собой для надежного обмена конфигурацией сети
как:
- в таблице узлов для каждой группы
У каждого узла проставляется доступность (можем ли мы каким-либо образом подключиться): acessible, reverse (при подключении с другой стороны), no
Единое обновление инфы об узле (per-group):
- обновляем тип узла в каждой группе где есть узел при обовлении типа нат узла. прямая доступность узла: yes/no.
=======================================
функционал группы.
Есть суперадмины группы - владеют приватным ключом суперадмина (один на группу).
Есть админы - владеют сгенерированным приватным ключом админа и подписанным суперадмином.
суперадмины могут делать revoke админу.
админские действия также распространяются по чату как и обычные сообщения.
ид группы = пубкей группы
юзер - join по ссылке (приватный ключ пира). получает ключи других пиров. Может только читать.
админ подписывает разрешение на писать (как и другие разрешения). подпись храним в таблице юзеров

12
tests/test_conn_mgr.c

@ -4,7 +4,6 @@
*
* Test 1: Direct via existing ETCP conn → CONN_TYPE_DIRECT
* Test 2: Status check → state/type
* Test 3: Alien node → TOPO_NODEQ.alien=1
*
* Pending:
* Test 4: New link UP callback (INIT to new peer via conn_mgr)
@ -89,17 +88,6 @@ static void test2(void* arg) {
fprintf(stderr, "Test 2: status state=%d type=%d\n", st, ty); fflush(stderr);
if (st == CONN_MGR_STATE_CONNECTED && ty == CONN_TYPE_DIRECT) fprintf(stderr, " OK: DIRECT\n");
else { fail("status mismatch"); return; }
test3(NULL);
}
static void test3(void* arg) {
(void)arg;
uint64_t aid = 0xEEEE000000000001ULL;
struct TOPO_NODE ni; memset(&ni, 0, sizeof(ni)); ni.node_id = aid; ni.ver = 1;
conn_mgr_add_alien_node(g_a->conn_mgr, (uint8_t*)&ni, sizeof(ni));
struct TOPO_NODEQ* nq = topo_node_find_by_id(topo_groups_get_default(g_a->topo_groups), aid);
fprintf(stderr, "Test 3: alien alien=%d\n", nq ? nq->alien : -1); fflush(stderr);
if (nq && nq->alien == 1) fprintf(stderr, " OK: alien flag set\n");
else { fail("alien flag not set"); return; }
fprintf(stderr, "=== ALL DONE ===\n"); fflush(stderr);
result = (result == 0) ? 1 : 2;
}

19
tools/chatgui/db/db_manager.cpp

@ -95,7 +95,7 @@ QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) {
QList<ChannelRow> DbManager::getChannels() const {
QList<ChannelRow> list;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT channel_id, name, owner_node_id, is_dm,"
"SELECT channel_id, name, owner_node_id,"
" x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature,"
" last_read_msg_id, last_pos_msg_id, created_at"
" FROM channels ORDER BY created_at ASC");
@ -105,15 +105,14 @@ QList<ChannelRow> DbManager::getChannels() const {
r.channelId = colText(stmt, 0);
r.name = colText(stmt, 1);
r.ownerNodeId = (quint64)sqlite3_column_int64(stmt, 2);
r.isDm = sqlite3_column_int(stmt, 3) != 0;
r.x25519_pubkey = colBlob(stmt, 4);
r.x25519_privkey = colBlob(stmt, 5);
r.ed25519_pubkey = colBlob(stmt, 6);
r.ed25519_privkey = colBlob(stmt, 7);
r.signature = colBlob(stmt, 8);
r.lastReadMsgId = sqlite3_column_int64(stmt, 9);
r.lastPosMsgId = sqlite3_column_int64(stmt, 10);
r.createdAt = sqlite3_column_int64(stmt, 11);
r.x25519_pubkey = colBlob(stmt, 3);
r.x25519_privkey = colBlob(stmt, 4);
r.ed25519_pubkey = colBlob(stmt, 5);
r.ed25519_privkey = colBlob(stmt, 6);
r.signature = colBlob(stmt, 7);
r.lastReadMsgId = sqlite3_column_int64(stmt, 8);
r.lastPosMsgId = sqlite3_column_int64(stmt, 9);
r.createdAt = sqlite3_column_int64(stmt, 10);
list.append(r);
}
sqlite3_finalize(stmt);

2
tools/chatgui/db/db_manager.h

@ -12,8 +12,6 @@ struct ChannelRow {
QString channelId;
QString name;
quint64 ownerNodeId = 0;
bool isDm = false;
QByteArray x25519_pubkey;
QByteArray x25519_privkey;
QByteArray ed25519_pubkey;

2
tools/chatgui/doc/db_schema.md

@ -109,7 +109,6 @@ CREATE TABLE IF NOT EXISTS channels (
channel_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
owner_node_id INTEGER,
is_dm INTEGER DEFAULT 0,
x25519_pubkey BLOB NOT NULL,
x25519_privkey BLOB,
ed25519_pubkey BLOB NOT NULL,
@ -126,7 +125,6 @@ CREATE TABLE IF NOT EXISTS channels (
| `channel_id` | Уникальный ID канала (напр. `"ch:general"`) |
| `name` | Отображаемое имя (напр. `"#general"`) |
| `owner_node_id` | ID создателя канала |
| `is_dm` | 1 = личная переписка (direct message) |
| `x25519_pubkey` | Публичный ключ канала (32 байта) |
| `x25519_privkey` | Приватный ключ канала, NULL если локальный узел не админ |
| `ed25519_pubkey` | Публичный ключ подписи канала (32 байта) |

1
tools/chatgui/libutun/CMakeLists.txt

@ -22,6 +22,7 @@ set(UASYNC_SOURCES
${LIB_DIR}/mem.c
${LIB_DIR}/radix.c
${LIB_DIR}/serialize.c
${LIB_DIR}/json_flat.c
${LIB_DIR}/tcp_io.c
${LIB_DIR}/sqlite3.c
${LIB_DIR}/liblmdb/mdb.c

1
tools/chatgui/src/mainwindow.cpp

@ -477,7 +477,6 @@ void MainWindow::onCreateGroupRequested() {
snprintf(req->channel_id, sizeof(req->channel_id), "%s", qPrintable(channelId));
snprintf(req->name, sizeof(req->name), "%s", qPrintable(name));
req->owner_node_id = ownerNodeId;
req->is_dm = 0;
memcpy(req->x25519_pubkey, x25519_pub, 32);
memcpy(req->x25519_privkey, x25519_priv, 32);
memcpy(req->ed25519_pubkey, ed25519_pub, 32);

28
tools/chatgui/transport/chat_core.c

@ -9,6 +9,7 @@
#include "chat_conn_mgr.h"
#include "db_sync.h"
#include "gui_bridge.h"
#include "../../../lib/json_flat.h"
#include "topo_node_sqlite.h"
#include "member_sync.h"
@ -275,11 +276,13 @@ void chat_core_update_my_name(const char* name) {
/* recompute join_sigs for all channels where I'm a member */
uint64_t myid = g_cc.my_node_id;
sqlite3_stmt* cs = NULL;
sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL);
sqlite3_prepare_v2(g_cc.db, "SELECT channel_id, x25519_pubkey, ed25519_pubkey FROM channels", -1, &cs, NULL);
if (cs) {
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(cs, 0);
if (!ch) continue;
const uint8_t* ch_x25519 = sqlite3_column_blob(cs, 1);
const uint8_t* ch_ed25519 = sqlite3_column_blob(cs, 2);
if (!ch || !ch_x25519 || !ch_ed25519) continue;
char tbl[80]; peers_table_name(ch, tbl, sizeof(tbl));
char buf[256]; snprintf(buf, sizeof(buf), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* ps = NULL;
@ -287,11 +290,10 @@ void chat_core_update_my_name(const char* name) {
sqlite3_bind_int64(ps, 1, (sqlite3_int64)myid);
if (sqlite3_step(ps) == SQLITE_ROW) {
uint8_t join_msg[256]; size_t mlen = 0;
mlen += snprintf((char*)join_msg + mlen, sizeof(join_msg) - mlen, "%s", ch) + 1;
memcpy(join_msg + mlen, ch_x25519, 32); mlen += 32;
memcpy(join_msg + mlen, ch_ed25519, 32); mlen += 32;
memcpy(join_msg + mlen, &myid, 8); mlen += 8;
memcpy(join_msg + mlen, g_cc.inst->my_keys.public_key, 32); mlen += 32;
{ const char* nm = name[0] ? name : "";
size_t nl = strlen(nm); memcpy(join_msg + mlen, nm, nl); mlen += nl; join_msg[mlen++] = '\0'; }
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
memcpy(join_msg + mlen, &join_ts, 8); mlen += 8;
uint8_t new_sig[64]; memset(new_sig, 0, 64);
@ -306,11 +308,12 @@ void chat_core_update_my_name(const char* name) {
}
EVP_PKEY_free(pkey);
}
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", name);
int r1 = topo_node_sqlite_member_put(g_cc.db, ch, myid, new_sig, join_ts, NULL, 0,
g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey, name, NULL);
g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey, juser, NULL);
if (r1 != 0) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: member_put(my_name) FAILED ch=%s rc=%d", CC_ID, ch, r1);
int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key,
g_cc.inst->my_ed25519_pubkey, new_sig, join_ts, NULL, 0, name, NULL, 0);
g_cc.inst->my_ed25519_pubkey, new_sig, join_ts, NULL, 0, juser, NULL, 0);
if (r2 != 0) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: member_sync_put(my_name) FAILED ch=%s rc=%d", CC_ID, ch, r2);
uint8_t evt[65]; uint8_t cl = (uint8_t)strlen(ch);
evt[0] = cl; memcpy(evt + 1, ch, cl);
@ -695,7 +698,7 @@ void chat_core_create_channel(struct chat_channel_create* req) {
/* записываем канал в БД ПЕРВЫМ — создаст полную схему peers_<ch_id> (11 колонок) */
int rc = topo_node_sqlite_channel_put(g_cc.db,
req->channel_id, req->name, req->is_dm, req->owner_node_id,
req->channel_id, req->name, req->owner_node_id,
req->x25519_pubkey, req->x25519_privkey,
req->ed25519_pubkey, req->ed25519_privkey,
req->signature);
@ -719,11 +722,10 @@ void chat_core_create_channel(struct chat_channel_create* req) {
{
uint64_t myid = g_cc.inst->node_id;
uint8_t join_msg[256]; size_t mlen = 0;
mlen += snprintf((char*)join_msg + mlen, sizeof(join_msg) - mlen, "%s", req->channel_id) + 1;
memcpy(join_msg + mlen, req->x25519_pubkey, 32); mlen += 32;
memcpy(join_msg + mlen, req->ed25519_pubkey, 32); mlen += 32;
memcpy(join_msg + mlen, &myid, 8); mlen += 8;
memcpy(join_msg + mlen, g_cc.inst->my_keys.public_key, 32); mlen += 32;
{ const char* nm = g_cc.inst->name[0] ? g_cc.inst->name : "";
size_t nl = strlen(nm); memcpy(join_msg + mlen, nm, nl); mlen += nl; join_msg[mlen++] = '\0'; }
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
memcpy(join_msg + mlen, &join_ts, 8); mlen += 8;
uint8_t join_sig[64];
@ -760,9 +762,11 @@ void chat_core_create_channel(struct chat_channel_create* req) {
s = s->next;
}
}
char juser3[256]; snprintf(juser3, sizeof(juser3), "{\"name\":\"%s\"}",
g_cc.inst->name[0] ? g_cc.inst->name : "");
int mrc = member_sync_put(g_cc.inst, req->channel_id, myid,
g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, g_cc.inst->name, my_addrs, my_addr_cnt);
join_sig, join_ts, NULL, 0, juser3, my_addrs, my_addr_cnt);
if (mrc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);

1
tools/chatgui/transport/chat_core.h

@ -51,7 +51,6 @@ struct chat_channel_create {
char channel_id[64];
char name[128];
uint64_t owner_node_id;
int is_dm;
uint8_t x25519_pubkey[32];
uint8_t x25519_privkey[32];
uint8_t ed25519_pubkey[32];

147
tools/chatgui/transport/chat_sync.c

@ -3,6 +3,7 @@
#include "chat_conn_mgr.h"
#include "gui_bridge.h"
#include "topo_node_sqlite.h"
#include "../../../lib/json_flat.h"
#include "member_sync.h"
#include "merkle_sync.h"
@ -916,9 +917,9 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
(void)pl; (void)len;
DEBUG_TRACE(DEBUG_CATEGORY_DB_SYNC, "%s: INFO_REQ ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
char name[128]; int is_dm; uint64_t owner; uint8_t x25519[32], ed_pub[32], ch_sig[64];
char name[128]; uint64_t owner; uint8_t x25519[32], ed_pub[32], ch_sig[64];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, name, (int)sizeof(name), &is_dm, &owner, x25519, ed_pub, ch_sig) != 0) {
ch_id, name, (int)sizeof(name), &owner, x25519, ed_pub, ch_sig) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, "%s: CHANNEL_INFO_REQ unknown ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
@ -933,7 +934,8 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
sqlite3* vdb = cs->inst->topo_sqlite_db;
if (topo_node_sqlite_member_get_join(vdb, ch_id, myid, my_join_sig, &my_join_ts) != 0) {
uint8_t join_msg[256]; size_t mlen = 0;
mlen += snprintf((char*)join_msg + mlen, sizeof(join_msg) - mlen, "%s", ch_id) + 1;
memcpy(join_msg + mlen, x25519, 32); mlen += 32;
memcpy(join_msg + mlen, ed_pub, 32); mlen += 32;
memcpy(join_msg + mlen, &myid, 8); mlen += 8;
memcpy(join_msg + mlen, cs->inst->my_keys.public_key, 32); mlen += 32;
my_join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
@ -946,13 +948,13 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
/* generate update_sig */
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts;
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}",
cs->inst->name[0] ? cs->inst->name : "");
uint8_t umsg[256]; size_t ulen = 0;
ulen += snprintf((char*)umsg + ulen, sizeof(umsg) - ulen, "%s", ch_id) + 1;
memcpy(umsg + ulen, &myid, 8); ulen += 8;
memcpy(umsg + ulen, cs->inst->my_keys.public_key, 32); ulen += 32;
memcpy(umsg + ulen, my_join_sig, 64); ulen += 64;
my_update_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8;
memcpy(umsg + ulen, my_join_sig, 64); ulen += 64;
size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1;
cs_ed25519_sign(cs->inst->my_ed25519_privkey, umsg, ulen, my_update_sig);
}
@ -961,7 +963,6 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
uint8_t nl = (uint8_t)strlen(name);
buf[boff++] = nl; memcpy(buf + boff, name, nl); boff += nl;
memcpy(buf + boff, &owner, 8); boff += 8;
buf[boff++] = (uint8_t)is_dm;
memcpy(buf + boff, x25519, 32); boff += 32;
memcpy(buf + boff, ed_pub, 32); boff += 32;
memcpy(buf + boff, ch_sig, 64); boff += 64;
@ -973,9 +974,10 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
}
memcpy(buf + boff, my_update_sig, 64); boff += 64;
memcpy(buf + boff, &my_update_ts, 8); boff += 8;
{ const char* iname = cs->inst->name[0] ? cs->inst->name : "";
uint8_t il = (uint8_t)strlen(iname);
buf[boff++] = il; memcpy(buf + boff, iname, il); boff += il; }
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}",
cs->inst->name[0] ? cs->inst->name : "");
uint8_t il = (uint8_t)strlen(juser);
buf[boff++] = il; memcpy(buf + boff, juser, il); boff += il;
cs_send(cs, ch_id, peer, buf, boff);
}
@ -987,11 +989,10 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
if (len < 1) { DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, "%s: CHANNEL_INFO_RESP too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint8_t nl = pl[0];
if (1 + nl + 8 + 1 + 32 + 32 + 64 + 1 + 64 + 8 + 1 > len) { DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, "%s: CHANNEL_INFO_RESP truncated len=%zu", CS_ID, len); return; }
if (1 + nl + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1 > len) { DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, "%s: CHANNEL_INFO_RESP truncated len=%zu", CS_ID, len); return; }
const uint8_t* p = pl + 1;
char name[128]; memcpy(name, p, nl); name[nl] = '\0'; p += nl;
uint64_t owner; memcpy(&owner, p, 8); p += 8;
uint8_t is_dm = *p++;
const uint8_t* x25519 = p; p += 32;
const uint8_t* ed_pub = p; p += 32;
const uint8_t* ch_sig = p; p += 64;
@ -1001,9 +1002,9 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
if (p + 72 > pl + len) return; inviter_join_sig = p; p += 64; memcpy(&inviter_join_ts, p, 8); p += 8; }
const uint8_t* inviter_update_sig = p; p += 64; uint64_t inviter_update_ts;
memcpy(&inviter_update_ts, p, 8); p += 8;
uint8_t inv_name_len = *p++;
char inv_name[128] = "";
if (p + inv_name_len <= pl + len) { memcpy(inv_name, p, inv_name_len); inv_name[inv_name_len] = '\0'; }
uint8_t inv_userinfo_len = *p++;
char inv_userinfo[128] = "";
if (p + inv_userinfo_len <= pl + len) { memcpy(inv_userinfo, p, inv_userinfo_len); inv_userinfo[inv_userinfo_len] = '\0'; }
DEBUG_DEBUG(DEBUG_CATEGORY_DB_SYNC, "%s: CHANNEL_INFO_RESP from peer=%016llx owner=%016llx ch_x25519=%016llx ch_ed=%016llx name=%s",
CS_ID, (unsigned long long)peer, (unsigned long long)owner,
@ -1023,7 +1024,7 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
/* save channel to local DB */
topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db,
ch_id, name, (int)is_dm, owner, x25519, NULL, ed_pub, NULL, ch_sig);
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
chat_core_ensure_channel_ready(ch_id);
DEBUG_INFO(DEBUG_CATEGORY_DB_SYNC, "%s: channel ready for sync ch=%s, db_sync will pick up via periodic check or active conn",
CS_ID, ch_id);
@ -1037,21 +1038,18 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
const uint8_t* inv_x25519 = inv_conn->crypto_ctx.peer_public_key;
const uint8_t* inv_ed = inv_conn->peer_ed25519_pubkey;
uint8_t ivmsg[256]; size_t ilen = 0;
ilen += snprintf((char*)ivmsg + ilen, sizeof(ivmsg) - ilen, "%s", ch_id) + 1;
memcpy(ivmsg + ilen, &peer, 8); ilen += 8;
memcpy(ivmsg + ilen, inv_x25519, 32); ilen += 32;
memcpy(ivmsg + ilen, &inviter_update_ts, 8); ilen += 8;
{ const char* nm = inv_name[0] ? inv_name : "";
size_t nl2 = strlen(nm); (void)nl2; }
if (inviter_join_sig) memcpy(ivmsg + ilen, inviter_join_sig, 64); else memset(ivmsg + ilen, 0, 64);
ilen += 64;
memcpy(ivmsg + ilen, &inviter_update_ts, 8); ilen += 8;
{ const char* nm = inv_userinfo[0] ? inv_userinfo : "";
size_t nl2 = strlen(nm); memcpy(ivmsg + ilen, nm, nl2 + 1); ilen += nl2 + 1; }
if (cs_ed25519_verify(inv_ed, ivmsg, ilen, inviter_update_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: CHANNEL_INFO_RESP invalid inviter_update_sig peer=%016llx ts=%llu",
CS_ID, (unsigned long long)peer, (unsigned long long)inviter_update_ts);
}
/* save inviter node_info to local DB */
sqlite3* vdb = cs->inst->topo_sqlite_db;
if (vdb && inv_name[0]) {
if (vdb && inv_userinfo[0]) {
sqlite3_stmt* ns = NULL;
sqlite3_prepare_v2(vdb,
"INSERT INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey) VALUES(?,?,?,?)"
@ -1059,7 +1057,7 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
" name=excluded.name, x25519_pubkey=excluded.x25519_pubkey, ed25519_pubkey=excluded.ed25519_pubkey",
-1, &ns, NULL);
if (ns) { sqlite3_bind_int64(ns, 1, (sqlite3_int64)peer);
sqlite3_bind_text(ns, 2, inv_name, -1, SQLITE_STATIC);
sqlite3_bind_text(ns, 2, inv_userinfo, -1, SQLITE_STATIC);
sqlite3_bind_blob(ns, 3, inv_x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(ns, 4, inv_ed, 32, SQLITE_STATIC);
sqlite3_step(ns); sqlite3_finalize(ns); }
@ -1093,8 +1091,9 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: [ADDR_SYNC] INFO_RESP DONE peer=%016llx: %d links, %d written",
CS_ID, (unsigned long long)peer, lk_count, lk_written);
}
int rc = topo_node_sqlite_member_put(vdb, ch_id, peer, inviter_join_sig, inviter_join_ts, inviter_update_sig, inviter_update_ts, inv_x25519, inv_ed, inv_name, inviter_join_sig);
int rc = topo_node_sqlite_member_put(vdb, ch_id, peer, inviter_join_sig, inviter_join_ts, inviter_update_sig, inviter_update_ts, inv_x25519, inv_ed, inv_userinfo, inviter_join_sig);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(inviter) FAILED ch=%s peer=%016llx rc=%d", CS_ID, ch_id, (unsigned long long)peer, rc);
char inv_name[128]; json_flat_get(inv_userinfo, "name", inv_name, sizeof(inv_name));
topo_node_sqlite_node_update_verified(vdb, peer, inv_name, inv_x25519, inv_ed, inviter_join_ts);
}
@ -1105,11 +1104,10 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
uint8_t join_sig[64]; uint64_t join_ts;
{
uint8_t msg[256]; size_t mlen = 0;
mlen += snprintf((char*)msg + mlen, sizeof(msg) - mlen, "%s", ch_id) + 1;
memcpy(msg + mlen, x25519, 32); mlen += 32;
memcpy(msg + mlen, ed_pub, 32); mlen += 32;
memcpy(msg + mlen, &myid, 8); mlen += 8;
memcpy(msg + mlen, my_x25519, 32); mlen += 32;
{ const char* nm = cs->inst->name[0] ? cs->inst->name : "";
size_t nl = strlen(nm); memcpy(msg + mlen, nm, nl); mlen += nl; msg[mlen++] = '\0'; }
join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
memcpy(msg + mlen, &join_ts, 8); mlen += 8;
cs_ed25519_sign(cs->inst->my_ed25519_privkey, msg, mlen, join_sig);
@ -1123,9 +1121,10 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
memcpy(jbuf + joff, cs->inst->my_ed25519_pubkey, 32); joff += 32;
memcpy(jbuf + joff, join_sig, 64); joff += 64;
memcpy(jbuf + joff, &join_ts, 8); joff += 8;
{ const char* nm = cs->inst->name[0] ? cs->inst->name : "";
uint8_t nml = (uint8_t)strlen(nm);
jbuf[joff++] = nml; memcpy(jbuf + joff, nm, nml); joff += nml; }
char juser2[256]; snprintf(juser2, sizeof(juser2), "{\"name\":\"%s\"}",
cs->inst->name[0] ? cs->inst->name : "");
uint8_t nml = (uint8_t)strlen(juser2);
jbuf[joff++] = nml; memcpy(jbuf + joff, juser2, nml); joff += nml;
/* add local addresses */
uint8_t addr_cnt = 0; size_t ac_pos = joff;
jbuf[joff++] = 0;
@ -1178,33 +1177,39 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
const uint8_t* ed_pub = p; p += 32;
const uint8_t* join_sig = p; p += 64;
uint64_t join_ts; memcpy(&join_ts, p, 8); p += 8;
uint8_t name_len = *p++;
char joiner_name[128] = "";
if (p + name_len <= pl + len) { memcpy(joiner_name, p, name_len); joiner_name[name_len] = '\0'; p += name_len; }
uint8_t userinfo_len = *p++;
char joiner_userinfo[128] = "";
if (p + userinfo_len <= pl + len) { memcpy(joiner_userinfo, p, userinfo_len); joiner_userinfo[userinfo_len] = '\0'; p += userinfo_len; }
uint8_t addr_cnt = *p++;
/* verify join_sig */
uint8_t ch_x25519[32], ch_ed25519[32];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db, ch_id,
NULL, 0, NULL, ch_x25519, ch_ed25519, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: JOIN channel not in DB ch=%s", CS_ID, ch_id);
return;
}
uint8_t vmsg[256]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed25519, 32); vlen += 32;
memcpy(vmsg + vlen, &node_id, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
{ const char* nm = joiner_name[0] ? joiner_name : "";
size_t nl = strlen(nm); memcpy(vmsg + vlen, nm, nl); vlen += nl; vmsg[vlen++] = '\0'; }
memcpy(vmsg + vlen, &join_ts, 8); vlen += 8;
if (cs_ed25519_verify(ed_pub, vmsg, vlen, join_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: JOIN invalid sig node=0x%016llx ch=%s — signed(node=%016llx x25519=%016llx ed=%016llx name=%s ts=%llu)", CS_ID,
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: JOIN invalid sig node=0x%016llx ch=%s — signed(node=%016llx x25519=%016llx ed=%016llx ts=%llu)", CS_ID,
(unsigned long long)node_id, ch_id,
(unsigned long long)node_id, *(const uint64_t*)x25519, *(const uint64_t*)ed_pub, joiner_name, (unsigned long long)join_ts);
(unsigned long long)node_id, *(const uint64_t*)x25519, *(const uint64_t*)ed_pub, (unsigned long long)join_ts);
return;
}
sqlite3* db = cs->inst->topo_sqlite_db;
int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, joiner_name, NULL);
int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, joiner_userinfo, NULL);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(joiner) FAILED ch=%s node=0x%016llx rc=%d", CS_ID, ch_id, (unsigned long long)node_id, rc);
topo_node_sqlite_node_update_verified(db, node_id, joiner_name, x25519, ed_pub, join_ts);
{ char j_name[128]; json_flat_get(joiner_userinfo, "name", j_name, sizeof(j_name));
topo_node_sqlite_node_update_verified(db, node_id, j_name, x25519, ed_pub, join_ts); }
chat_conn_mgr_add_node(chat_conn_mgr_get(), node_id);
/* save joiner node_info to local DB */
if (db && joiner_name[0]) {
if (db && joiner_userinfo[0]) {
sqlite3_stmt* ns = NULL;
sqlite3_prepare_v2(db,
"INSERT INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey) VALUES(?,?,?,?)"
@ -1212,7 +1217,7 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
" name=excluded.name, x25519_pubkey=excluded.x25519_pubkey, ed25519_pubkey=excluded.ed25519_pubkey",
-1, &ns, NULL);
if (ns) { sqlite3_bind_int64(ns, 1, (sqlite3_int64)node_id);
sqlite3_bind_text(ns, 2, joiner_name, -1, SQLITE_STATIC);
sqlite3_bind_text(ns, 2, joiner_userinfo, -1, SQLITE_STATIC);
sqlite3_bind_blob(ns, 3, x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(ns, 4, ed_pub, 32, SQLITE_STATIC);
sqlite3_step(ns); sqlite3_finalize(ns); }
@ -1271,12 +1276,12 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
memcpy(ubuf + uoff, ed_pub, 32); uoff += 32;
memcpy(ubuf + uoff, join_sig, 64); uoff += 64;
memcpy(ubuf + uoff, &join_ts, 8); uoff += 8;
ubuf[uoff++] = name_len;
memcpy(ubuf + uoff, joiner_name, name_len); uoff += name_len;
ubuf[uoff++] = userinfo_len;
memcpy(ubuf + uoff, joiner_userinfo, userinfo_len); uoff += userinfo_len;
ubuf[uoff++] = addr_cnt;
size_t addr_data_sz = (size_t)(p - (pl + 8 + 32 + 32 + 64 + 8 + 1 + name_len + 1));
size_t addr_data_sz = (size_t)(p - (pl + 8 + 32 + 32 + 64 + 8 + 1 + userinfo_len + 1));
if (uoff + addr_data_sz <= sizeof(ubuf)) {
memcpy(ubuf + uoff, pl + 8 + 32 + 32 + 64 + 8 + 1 + name_len + 1, addr_data_sz);
memcpy(ubuf + uoff, pl + 8 + 32 + 32 + 64 + 8 + 1 + userinfo_len + 1, addr_data_sz);
uoff += addr_data_sz;
}
cs_propagate(cs, ch_id, peer, ubuf, uoff);
@ -1336,18 +1341,23 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
uint64_t update_ts; memcpy(&update_ts, p, 8); p += 8;
(void)update_sig; (void)update_ts;
uint8_t nl = *p++;
char peer_name[256] = "";
if (nl && p + nl <= pl + len) { memcpy(peer_name, p, nl); peer_name[nl] = '\0'; p += nl; }
char peer_userinfo[256] = "";
if (nl && p + nl <= pl + len) { memcpy(peer_userinfo, p, nl); peer_userinfo[nl] = '\0'; p += nl; }
uint8_t ac = *p++;
/* verify join_sig if present */
if (join_sig) {
uint8_t ch_x2[32], ch_ed2[32];
if (topo_node_sqlite_channel_get(db, ch_id,
NULL, 0, NULL, ch_x2, ch_ed2, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: WELCOME channel not in DB ch=%s", CS_ID, ch_id);
continue;
}
uint8_t vmsg[256]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
memcpy(vmsg + vlen, ch_x2, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed2, 32); vlen += 32;
memcpy(vmsg + vlen, &node_id, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
{ const char* nm = peer_name[0] ? peer_name : "";
size_t nls = strlen(nm); memcpy(vmsg + vlen, nm, nls); vlen += nls; vmsg[vlen++] = '\0'; }
memcpy(vmsg + vlen, &join_ts, 8); vlen += 8;
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ed_pub, 32);
if (pkey) {
@ -1365,9 +1375,10 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
}
}
int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_name, NULL);
int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_userinfo, NULL);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(welcome) FAILED ch=%s node=0x%016llx rc=%d", CS_ID, ch_id, (unsigned long long)node_id, rc);
topo_node_sqlite_node_update_verified(db, node_id, peer_name, x25519, ed_pub, join_ts);
{ char pn[256]; json_flat_get(peer_userinfo, "name", pn, sizeof(pn));
topo_node_sqlite_node_update_verified(db, node_id, pn, x25519, ed_pub, join_ts); }
chat_conn_mgr_add_node(chat_conn_mgr_get(), node_id);
@ -1442,18 +1453,23 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
const uint8_t* ed_pub = p; p += 32;
const uint8_t* join_sig = p; p += 64;
uint64_t join_ts; memcpy(&join_ts, p, 8); p += 8;
uint8_t name_len = *p++;
char peer_name[128] = "";
if (p + name_len <= pl + len) { memcpy(peer_name, p, name_len); peer_name[name_len] = '\0'; p += name_len; }
uint8_t userinfo_len = *p++;
char peer_userinfo[128] = "";
if (p + userinfo_len <= pl + len) { memcpy(peer_userinfo, p, userinfo_len); peer_userinfo[userinfo_len] = '\0'; p += userinfo_len; }
uint8_t ac = *p++;
/* verify join_sig */
uint8_t ch_x3[32], ch_ed3[32];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db, ch_id,
NULL, 0, NULL, ch_x3, ch_ed3, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: PEER_UPSERT channel not in DB ch=%s", CS_ID, ch_id);
return;
}
uint8_t vmsg[256]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
memcpy(vmsg + vlen, ch_x3, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed3, 32); vlen += 32;
memcpy(vmsg + vlen, &node_id, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
{ const char* nm = peer_name[0] ? peer_name : "";
size_t nl = strlen(nm); memcpy(vmsg + vlen, nm, nl); vlen += nl; vmsg[vlen++] = '\0'; }
memcpy(vmsg + vlen, &join_ts, 8); vlen += 8;
if (cs_ed25519_verify(ed_pub, vmsg, vlen, join_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: PEER_UPSERT invalid sig node=0x%016llx", CS_ID,
@ -1463,11 +1479,12 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
sqlite3* db = cs->inst->topo_sqlite_db;
int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_name, NULL);
int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_userinfo, NULL);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(peer_upsert) FAILED ch=%s node=0x%016llx rc=%d", CS_ID, ch_id, (unsigned long long)node_id, rc);
topo_node_sqlite_node_update_verified(db, node_id, peer_name, x25519, ed_pub, join_ts);
{ char pn2[128]; json_flat_get(peer_userinfo, "name", pn2, sizeof(pn2));
topo_node_sqlite_node_update_verified(db, node_id, pn2, x25519, ed_pub, join_ts); }
chat_conn_mgr_add_node(chat_conn_mgr_get(), node_id);
if (db && peer_name[0]) {
if (db && peer_userinfo[0]) {
sqlite3_stmt* ns = NULL;
sqlite3_prepare_v2(db,
"INSERT INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey) VALUES(?,?,?,?)"
@ -1475,7 +1492,7 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
" name=excluded.name, x25519_pubkey=excluded.x25519_pubkey, ed25519_pubkey=excluded.ed25519_pubkey",
-1, &ns, NULL);
if (ns) { sqlite3_bind_int64(ns, 1, (sqlite3_int64)node_id);
sqlite3_bind_text(ns, 2, peer_name, -1, SQLITE_STATIC);
sqlite3_bind_text(ns, 2, peer_userinfo, -1, SQLITE_STATIC);
sqlite3_bind_blob(ns, 3, x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(ns, 4, ed_pub, 32, SQLITE_STATIC);
sqlite3_step(ns); sqlite3_finalize(ns); }

18
tools/chatgui/transport/member_sync.c

@ -139,7 +139,7 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
char sql[512]; snprintf(sql, sizeof(sql),
"SELECT node_id, x25519_pubkey, ed25519_pubkey,"
" join_sig, join_ts, update_sig, update_ts, name"
" join_sig, join_ts, update_sig, update_ts, userinfo"
" FROM \"%s\""
" WHERE (node_id & %lld) == %lld ORDER BY node_id ASC",
peers_tbl, (long long)mask, (long long)prefix);
@ -254,11 +254,9 @@ static int _member_apply_items(void* ctx, const char* ns,
/* verify update_sig if available */
if (usig && uts && jsig && ed) {
uint8_t vmsg[256]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ns) + 1;
memcpy(vmsg + vlen, &nid, 8); vlen += 8;
memcpy(vmsg + vlen, x25, 32); vlen += 32;
memcpy(vmsg + vlen, &uts, 8); vlen += 8;
memcpy(vmsg + vlen, jsig, 64); vlen += 64;
memcpy(vmsg + vlen, &uts, 8); vlen += 8;
size_t nl2 = nm ? strlen(nm) : 0; memcpy(vmsg + vlen, nm ? nm : "", nl2 + 1); vlen += nl2 + 1;
{ EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ed, 32);
if (pkey) {
EVP_MD_CTX* ver = EVP_MD_CTX_new();
@ -332,18 +330,18 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
const uint8_t* ed25519,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const char* name,
const char* userinfo,
const uint8_t* addrs_data, int addr_count) {
if (!inst || !ch_id) return -1;
DEBUG_TRACE(DEBUG_CATEGORY_DB_SYNC, "%s: put ch=%s nid=%016llx name=%s ac=%d", MS_ID, ch_id, (unsigned long long)member_id, name ? name : "", addr_count);
DEBUG_TRACE(DEBUG_CATEGORY_DB_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s ac=%d", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", addr_count);
sqlite3* db = _db(inst); if (!db) return -1;
int rc = topo_node_sqlite_member_put(db, ch_id, member_id,
join_sig, join_ts, update_sig, update_ts,
x25519, ed25519, name, NULL);
x25519, ed25519, userinfo, NULL);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put FAILED ch=%s id=0x%016llx name=%s rc=%d",
MS_ID, ch_id, (unsigned long long)member_id, name ? name : "", rc);
DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put FAILED ch=%s id=0x%016llx userinfo=%s rc=%d",
MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", rc);
return -1;
}

6
tools/chatgui/transport/member_sync.h

@ -84,9 +84,9 @@ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* c
* x25519 — X25519 публичный ключ (32 байта)
* ed25519 — Ed25519 публичный ключ (32 байта)
* join_sig — Ed25519 подпись join-сообщения (64 байта)
* сообщение: ch_id || '\0' || node_id(8LE) || x25519(32) || name || '\0' || join_ts(8LE)
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE)
* join_ts — NTP-синхронизированное время подписи (unix epoch seconds)
* name — имя узла (из подписанного сообщения)
* userinfo — метаданные узла (имя) в канале
* addrs_data — адреса в wire-формате: [family:1][ip:4|16][port:2]...
* addr_count — количество адресов (0 если нет)
*
@ -101,7 +101,7 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
const uint8_t* ed25519,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const char* name,
const char* userinfo,
const uint8_t* addrs_data, int addr_count);
/* Удалить мембера из канала и пересчитать дерево. */

2
tools/chatgui/transport/merkle_sync.h

@ -120,7 +120,7 @@ struct merkle_sync_data_ops {
* *len — [in] размер буфера, [out] записанный размер
*
* Wire-формат для мемберов:
* [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][name_len:1][name: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][addr_cnt:1][addrs:var]...
*
* Возвращает 0 при успехе, <0 при ошибке, -2 если буфер мал.
*/

Loading…
Cancel
Save