Browse Source
- chat_headless_control.c: новая команда invite_to (inviter-side invite по адресам) - chat_client.py / chatcli: клиентский метод и CLI-команда invite_to - tools/chat_invite_test.py: headless-тест инвайтера-side (CHANNEL_INVITE → autojoin → member_sync) - topo: guard node_id=0 в cm_open/process_nodeinfo + детализация send_nodeinfo warn - cleanup: удалить устаревшие doc/*.txt, *.md заметкиtopo_upd
22 changed files with 361 additions and 1633 deletions
@ -1,344 +0,0 @@
|
||||
Архитектура P2P Direct Connection (установка оптимального подключения между узлами) |
||||
|
||||
Цель: в большой сети узлов каждый узел умеет находить быстрые подключения |
||||
к любому другому узлу — либо прямое (direct), либо через минимальное число |
||||
промежуточных узлов с минимальным суммарным RTT. Оба узла пробуют доступные |
||||
варианты пингом и выбирают лучший. |
||||
|
||||
|
||||
=== Что уже есть в коде (используем, не дублируем) === |
||||
|
||||
1. NODEINFO уже содержит поле tranzit_nodes + структуру NODEINFO_TRANZIT_NODE |
||||
(node_id, rtt, link_q) — "лучшие транзитные узлы, выбирается/обновляется узлом", |
||||
но пока не заполняется и не используется. |
||||
|
||||
2. Пинг-инфраструктура: route_ping_send_req_addr (запрос удалённого пинга через |
||||
третий узел), route_ping_handle_resp (приём результата). Также есть |
||||
etcp_send_ping_to_socket — прямой пинг с конкретного сокета на конкретный адрес |
||||
с pubkey для шифрования. |
||||
|
||||
3. NAT-детекция уже работает: проверка типа NAT через третий узел, обмен NAT_INFO. |
||||
|
||||
4. Path system: NODEINFO_PATH с hop_count, routes → ROUTE_ENTRY → NODEINFO_Q → paths — |
||||
уже выбирается путь для маршрутизации пакетов. |
||||
|
||||
5. Константа ROUTE_SUBCMD_CONN_REQ (0x0B) зарезервирована в route_bgp.h, но не реализована. |
||||
|
||||
6. NODEINFO_Q уже имеет best_socket, last_ping_time, last_rtt. |
||||
|
||||
|
||||
=== Общий алгоритм (на примере узлов A и B) === |
||||
|
||||
A ──(ETCP через релейные узлы)──> B [текущий путь, hop_count > 1] |
||||
|
||||
1. A замечает большой объём трафика к B → запускает CONN_REQ |
||||
2. A шлёт B через существующий ETCP-путь: свои адреса-кандидаты + лучших транзитных соседей |
||||
3. B получает CONN_REQ, сразу начинает пинговать адреса A, собирает свои данные |
||||
4. B шлёт CONN_RESP: свои адреса + транзитных соседей + результаты пробных пингов |
||||
5. A получает CONN_RESP, пингует все адреса B |
||||
6. Оба вычисляют лучший вариант (прямой или релейный с минимальным RTT) |
||||
7. Оба шлют друг другу CONN_RESULT с выбором |
||||
8. Оба пытаются создать ETCP link к выбранному адресу (если прямой) |
||||
9. Лишние/худшие линки закрываются позже — все линки в рамках одного ETCP_CONN |
||||
|
||||
|
||||
=== Фаза 1: Обмен кандидатами (CONN_REQ → CONN_RESP) === |
||||
|
||||
Узел-инициатор A собирает и отправляет: |
||||
- Свои адреса-кандидаты: из своих ETCP_SOCKET (interface_addr + nat_addr). |
||||
Включаются только сокеты с NAT_VERIFIED_* / EIM / PUBLIC (type >= NAT_VERIFIED_UNKNOWN). |
||||
Для каждого: ip, port, type, socket_id. |
||||
- Лучших транзитных соседей: top-N directly-connected узлов (hop_count == 1) |
||||
отсортированных по NODEINFO_Q.last_rtt (лучший RTT первым). |
||||
|
||||
Узел B при получении CONN_REQ: |
||||
- Сохраняет кандидатов A |
||||
- Сразу запускает пробные пинги ко всем адресам A (etcp_send_ping_to_socket с pubkey) |
||||
- Собирает свои адреса-кандидаты |
||||
- Собирает своих транзитных соседей |
||||
- Шлёт CONN_RESP со своими данными + уже готовыми результатами проб |
||||
|
||||
|
||||
=== Фаза 2: Пробы (CONN_RESP → Пинги) === |
||||
|
||||
Узел A при получении CONN_RESP: |
||||
- Запускает пинги ко всем адресам B: |
||||
каждый адрес B пингуется с каждого своего сокета (N×M проб) |
||||
- Использует etcp_send_ping_to_socket с pubkey узла B (из NODEINFO) |
||||
- Пинг идёт напрямую по UDP (не через ETCP) — проверяет реальную достижимость |
||||
включая NAT traversal |
||||
- Ждёт все результаты либо таймаут |
||||
|
||||
Параллельно B тоже завершает свои пробы (запущенные на шаге 3). |
||||
|
||||
|
||||
=== Фаза 3: Выбор лучшего пути (→ CONN_RESULT) === |
||||
|
||||
Каждая сторона независимо вычисляет: |
||||
|
||||
best = {mode: none, rtt: 65535} |
||||
|
||||
Прямые варианты: |
||||
for each (мой_сокет, peer_addr) где probe.ok: |
||||
if probe.rtt < best.rtt: |
||||
best = {direct, my_sock_idx, peer_addr_idx, probe.rtt} |
||||
|
||||
Релейные варианты: |
||||
for each общий_транзитный_узел R (доступен и A и B): |
||||
// RTT(A↔R) берём из своих замеров (A знает RTT до своих соседей) |
||||
// RTT(R↔B) берём из транзитных соседей B (присланы в CONN_RESP) |
||||
total_rtt = A→R_rtt + R→B_rtt |
||||
if total_rtt < best.rtt: |
||||
best = {relay, R, total_rtt} |
||||
|
||||
Обе стороны приходят к одинаковому выводу (информация симметрична). |
||||
|
||||
Шлют CONN_RESULT с выбором. |
||||
Если прямой вариант — оба пытаются создать ETCP link к адресу пира. |
||||
Если релейный — используют существующий путь (уже работает). |
||||
|
||||
|
||||
=== Триггер запуска negotiation === |
||||
|
||||
В route_pkt (routing.c) при отправке пакета узлу с hop_count > 1: |
||||
- Накапливаем счётчик байт к этому узлу (per-node counter в bgp) |
||||
- При превышении порога (например 64KB) и если negotiation ещё не запущен — стартуем |
||||
- Cooldown: не чаще чем раз в 30 секунд для одной пары узлов |
||||
- Если negotiation уже в процессе для этой пары — не дублируем |
||||
|
||||
|
||||
=== Tranzit nodes — автоматическое заполнение === |
||||
|
||||
В route_bgp_update_my_nodeinfo (route_node.c) при каждом обновлении NODEINFO: |
||||
- Сканируем directly-connected соседей (из bgp->nodes, где hop_count == 1) |
||||
- Сортируем по NODEINFO_Q.last_rtt (лучший RTT первым) |
||||
- Берём top-N (до 4) и упаковываем в NODEINFO.tranzit_nodes как массив |
||||
NODEINFO_TRANZIT_NODE (node_id, rtt, link_q) |
||||
|
||||
Плюс периодический refill (раз в ~10 сек) для свежести RTT замеров. |
||||
|
||||
|
||||
=== Структуры данных (новые, route_p2pconn.h) === |
||||
|
||||
#define P2P_MAX_ADDRS 8 |
||||
#define P2P_MAX_RELAYS 8 |
||||
#define P2P_MAX_PROBES (P2P_MAX_ADDRS * P2P_MAX_ADDRS) // до 64 |
||||
|
||||
#define P2P_PHASE_WAIT_RESP 0 // ждём CONN_RESP от пира |
||||
#define P2P_PHASE_PROBING 1 // пингуем адреса пира |
||||
#define P2P_PHASE_SELECTING 2 // выбор лучшего, отправка CONN_RESULT |
||||
#define P2P_PHASE_DONE 3 // завершено |
||||
|
||||
// Один адрес-кандидат |
||||
struct P2P_ADDR { |
||||
uint32_t ip; // network byte order |
||||
uint16_t port; |
||||
uint8_t type; // NAT_VERIFIED_* |
||||
uint8_t socket_id; |
||||
}; |
||||
|
||||
// Один транзитный узел |
||||
struct P2P_RELAY { |
||||
uint64_t node_id; |
||||
uint16_t rtt; // x0.1ms |
||||
uint16_t link_q; |
||||
}; |
||||
|
||||
// Результат одного пробного пинга |
||||
struct P2P_PROBE_RESULT { |
||||
uint8_t my_sock_idx; // индекс в my_addrs[] |
||||
uint8_t peer_addr_idx; // индекс в peer_addrs[] |
||||
uint16_t rtt; // x0.1ms, 0 = fail |
||||
}; |
||||
|
||||
// Состояние одних переговоров (хранится в хеш-таблице bgp->p2p_negotiations) |
||||
struct P2P_NEGOTIATION { |
||||
struct ll_entry ll; |
||||
uint32_t request_id; |
||||
uint64_t peer_node_id; |
||||
uint8_t phase; |
||||
|
||||
// Мои данные |
||||
struct P2P_ADDR my_addrs[P2P_MAX_ADDRS]; |
||||
uint8_t my_addr_count; |
||||
struct P2P_RELAY my_relays[P2P_MAX_RELAYS]; |
||||
uint8_t my_relay_count; |
||||
|
||||
// Данные пира (заполняются из CONN_RESP) |
||||
struct P2P_ADDR peer_addrs[P2P_MAX_ADDRS]; |
||||
uint8_t peer_addr_count; |
||||
struct P2P_RELAY peer_relays[P2P_MAX_RELAYS]; |
||||
uint8_t peer_relay_count; |
||||
|
||||
// Результаты проб |
||||
struct P2P_PROBE_RESULT probes[P2P_MAX_PROBES]; |
||||
uint8_t probe_total; // сколько всего запланировано |
||||
uint8_t probe_done; // сколько завершилось (ok + fail) |
||||
|
||||
// Лучший выбор |
||||
uint8_t best_mode; // 1=direct, 2=relay |
||||
uint64_t best_relay; // node_id релея (если mode=relay) |
||||
uint16_t best_rtt; // x0.1ms |
||||
uint8_t best_my_sock_idx; // индекс в my_addrs (если direct) |
||||
uint8_t best_peer_addr_idx;// индекс в peer_addrs (если direct) |
||||
|
||||
void* timeout_timer; // общий таймаут на всю negotiation |
||||
}; |
||||
|
||||
// Счётчик трафика per-node (для триггера, хранится в bgp) |
||||
struct P2P_TRAFFIC_COUNTER { |
||||
uint64_t node_id; |
||||
uint64_t bytes_sent; // накоплено байт |
||||
uint64_t last_negotiation_time; // время последней попытки (0 = не было) |
||||
}; |
||||
|
||||
|
||||
=== Протокольные пакеты (добавляются в route_bgp.h) === |
||||
|
||||
ROUTE_SUBCMD_CONN_REQ 0x0B // запрос прямого подключения (уже зарезервирован) |
||||
ROUTE_SUBCMD_CONN_RESP 0x0C // ответ с адресами + результаты проб |
||||
ROUTE_SUBCMD_CONN_RESULT 0x0E // финальный выбор |
||||
|
||||
// Пакет CONN_REQ (A → B) |
||||
struct BGP_CONN_REQ { |
||||
uint8_t cmd; // ETCP_ID_ROUTE_ENTRY |
||||
uint8_t subcmd; // ROUTE_SUBCMD_CONN_REQ |
||||
uint32_t request_id; // для корреляции |
||||
uint8_t addr_count; // число адресов-кандидатов |
||||
uint8_t relay_count; // число транзитных узлов |
||||
uint8_t reserved[2]; |
||||
// далее динамически (размер = addr_count*8 + relay_count*12): |
||||
// [addr_count × {ip[4] port[2] type[1] socket_id[1]}] |
||||
// [relay_count × {node_id[8] rtt[2] link_q[2]}] |
||||
}; |
||||
|
||||
// Пакет CONN_RESP (B → A) |
||||
struct BGP_CONN_RESP { |
||||
uint8_t cmd; // ETCP_ID_ROUTE_ENTRY |
||||
uint8_t subcmd; // ROUTE_SUBCMD_CONN_RESP |
||||
uint32_t request_id; |
||||
uint8_t addr_count; // адреса B |
||||
uint8_t relay_count; // транзитные узлы B |
||||
uint8_t probe_count; // готовые результаты проб B→A |
||||
uint8_t reserved; |
||||
// [addr_count × {ip[4] port[2] type[1] socket_id[1]}] |
||||
// [relay_count × {node_id[8] rtt[2] link_q[2]}] |
||||
// [probe_count × {peer_addr_idx[1] my_sock_idx[1] rtt[2] ok[1]}] |
||||
}; |
||||
|
||||
// Пакет CONN_RESULT (A ↔ B, финальный) |
||||
struct BGP_CONN_RESULT { |
||||
uint8_t cmd; |
||||
uint8_t subcmd; // ROUTE_SUBCMD_CONN_RESULT |
||||
uint32_t request_id; |
||||
uint8_t chosen_mode; // 1=direct, 2=relay |
||||
uint8_t reserved; |
||||
uint16_t chosen_rtt; // x0.1ms |
||||
// Если direct: |
||||
uint32_t peer_ip; // IP пира к которому подключаемся |
||||
uint16_t peer_port; |
||||
uint8_t my_socket_id; // свой сокет для подключения |
||||
uint8_t peer_socket_id; // сокет пира |
||||
// Если relay (оверлей тех же байт): |
||||
// uint64_t relay_node_id; |
||||
}; |
||||
|
||||
|
||||
=== API модуля route_p2pconn === |
||||
|
||||
// Запуск negotiation к узлу peer_node_id. |
||||
// Вызывается из route_pkt при превышении порога трафика. |
||||
int p2p_start_negotiation(struct ROUTE_BGP* bgp, uint64_t peer_node_id); |
||||
|
||||
// Проверка: запущена ли уже negotiation для этой пары |
||||
int p2p_is_negotiating(struct ROUTE_BGP* bgp, uint64_t peer_node_id); |
||||
|
||||
// Сбор своих адресов-кандидатов (из etcp_sockets) |
||||
int p2p_collect_my_addrs(struct ROUTE_BGP* bgp, struct P2P_ADDR* out, uint8_t max); |
||||
|
||||
// Сбор лучших транзитных соседей (из directly-connected nodes) |
||||
int p2p_collect_my_relays(struct ROUTE_BGP* bgp, struct P2P_RELAY* out, uint8_t max); |
||||
|
||||
// Обработчики входящих пакетов (вызываются из route_bgp_receive_cbk): |
||||
void p2p_handle_conn_req(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, |
||||
const uint8_t* data, size_t len); |
||||
void p2p_handle_conn_resp(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, |
||||
const uint8_t* data, size_t len); |
||||
void p2p_handle_conn_result(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, |
||||
const uint8_t* data, size_t len); |
||||
|
||||
// Отмена negotiation при удалении conn |
||||
void p2p_cancel_for_conn(struct ROUTE_BGP* bgp, struct ETCP_CONN* conn); |
||||
|
||||
// Очистка всех negotiation (при destroy bgp) |
||||
void p2p_destroy_all(struct ROUTE_BGP* bgp); |
||||
|
||||
|
||||
=== Схема состояний negotiation === |
||||
|
||||
IDLE ──(трафик > порог)──→ WAIT_RESP ──(CONN_RESP получен)──→ PROBING |
||||
↑ ↑ │ |
||||
│ (таймаут) (все пробы готовы) |
||||
│ ↓ │ |
||||
└──────────────────────────── DONE ←──(CONN_RESULT отправлен)── SELECTING |
||||
|
||||
|
||||
=== Интеграция (какие файлы меняются) === |
||||
|
||||
Новые файлы: |
||||
src/route_p2pconn.h — структуры, константы, прототипы |
||||
src/route_p2pconn.c — вся логика negotiation |
||||
|
||||
Изменения в существующих: |
||||
src/route_bgp.h — добавить ROUTE_SUBCMD_CONN_RESP 0x0C, |
||||
ROUTE_SUBCMD_CONN_RESULT 0x0E, |
||||
структуры пакетов BGP_CONN_REQ/RESP/RESULT |
||||
src/route_bgp.c — в route_bgp_receive_cbk добавить обработку |
||||
новых subcmd. В struct ROUTE_BGP добавить |
||||
поле p2p_negotiations (очередь/хеш negotiation) |
||||
и p2p_traffic_counters (per-node counters) |
||||
src/route_node.c — в route_bgp_update_my_nodeinfo заполнять |
||||
tranzit_nodes из RTT directly-connected соседей |
||||
src/routing.c — в route_pkt добавить накопление счётчика трафика |
||||
и вызов p2p_start_negotiation при превышении порога |
||||
src/Makefile.am — добавить route_p2pconn.c в сборку |
||||
|
||||
|
||||
=== Внутренняя логика route_p2pconn.c === |
||||
|
||||
p2p_start_negotiation(bgp, peer_node_id): |
||||
1. Проверить что нет активной negotiation для этой пары |
||||
2. Проверить cooldown (30 сек) |
||||
3. Создать struct P2P_NEGOTIATION, заполнить my_addrs, my_relays |
||||
4. Сформировать BGP_CONN_REQ и отправить через существующий путь к пиру |
||||
5. Поставить таймаут на всю negotiation (например 5 сек) |
||||
|
||||
p2p_handle_conn_req(bgp, from_conn, data, len): |
||||
1. Распарсить BGP_CONN_REQ |
||||
2. Сохранить адреса и релеи инициатора в P2P_NEGOTIATION |
||||
3. Собрать свои my_addrs, my_relays |
||||
4. Запустить пробные пинги к адресам инициатора (p2p_start_probes) |
||||
5. Когда пробы готовы — отправить CONN_RESP с результатами |
||||
|
||||
p2p_start_probes(neg): |
||||
1. Для каждой пары (мой_сокет, peer_addr) запланировать пинг |
||||
2. Вызвать etcp_send_ping_to_socket с pubkey пира |
||||
3. В коллбэке сохранить результат в probes[], инкрементировать probe_done |
||||
4. Когда probe_done == probe_total → вызвать p2p_select_best |
||||
|
||||
p2p_select_best(neg): |
||||
1. Прямые: найти пару (my_sock, peer_addr) с минимальным rtt > 0 |
||||
2. Релейные: для каждого общего транзитного узла посчитать суммарный rtt, |
||||
найти минимум |
||||
3. Сравнить прямой vs релейный лучший rtt |
||||
4. Записать выбор в neg->best_* |
||||
5. Отправить CONN_RESULT пиру |
||||
6. Вызвать p2p_establish_link(neg) |
||||
|
||||
p2p_establish_link(neg): |
||||
1. Если best_mode == direct: |
||||
- Найти/создать ETCP_CONN к пиру (по node_id) |
||||
- Вызвать etcp_link_new с chosen address |
||||
2. Если best_mode == relay: |
||||
- Ничего не делаем, существующий путь уже работает |
||||
3. Пометить negotiation как DONE |
||||
@ -1,22 +0,0 @@
|
||||
Архитектура таблицы c быстрой репликацией между несколькими узлами: |
||||
|
||||
1. В конец таблицы каждый узел может самостоятельно добавлять данные (со своим timestamp, желательно время правильное) |
||||
2. узлы между собой синхронизируются, распространяя обновления по соседям |
||||
|
||||
3. TTL: изменение имеет время жизни. узел удаляет собственные записи которые не были отправлены никому в течении суток |
||||
|
||||
4. Формат записи: |
||||
|
||||
ID (64bit monotonic autoincrement) |
||||
chain_hash (256bit) - цепочка хешей. считается так: chain_hash[index+1]=hash(chain_hash[index],ID[index],timestamp[index],datahash[index]). если хеш совпадает - это признак того что все строки выше синхронизированы. |
||||
timestamp (64bit) - время создания записи. строки должны сортироваться по возрастанию concat(datahash(младшая часть числа,64 bit)+timestamp(старшая часть)) |
||||
datahash (64bit) - хеш данных этой строки |
||||
data (varchar) - данные в формате json |
||||
|
||||
алгоритм репликации (2 узла): |
||||
узел A запрашивает синхронизацию и передает свой last id |
||||
test_id= min(mast id, peer last id) |
||||
узел B передает: test_id, chain_hash(test id), hash(test id-1), hash(test id-2), hash(test id-4), hash(test id-8) итд 16,32,..., до первого элемента включитально (лимитируем вылетевший индекс первым элементом) |
||||
узел A - сравнивает, находит проверяемый диапазон id, отправляет другому узлу 16 (можно больше) хешей (линейно разбив проверяемый диапазон на более короткие поддиапазоны). таким образом узлы уточняют первый ид который не совпал. |
||||
Когда первая различающияся запись найдена, узел отправляет хеш этой и n (например 32) последующих записей (если записей много). если записей мало (<4) то узел отправляет сразу содержимое записей. |
||||
(надо додумать алгоритм, чтобы оптимизировать количество итераций - лучше передать больше данных за раз чем много итераций с ожиданием ответной стороны) |
||||
@ -1,224 +0,0 @@
|
||||
# db_sync — Distributed append-only table with SQLite + P2P sync |
||||
|
||||
## 1. Назначение |
||||
|
||||
Реплицировать append-only таблицу JSON-записей между всеми пирами P2P-сети. Каждый пир в итоге должен иметь идентичный набор записей. Модуль поддерживает несколько независимых инстансов (таблиц), каждый идентифицируется хешем `SHA256(name || id_be)[0:8]`. |
||||
|
||||
## 2. Ключевые свойства |
||||
|
||||
- **Криптографическая цепь.** `chain_hash[N] = SHA256(chain_hash[N-1] || id || timestamp || author || author_signature)`. Записи упорядочены `ORDER BY timestamp, author_signature`. Первые 8 байт — `chain_hash8` — используется для быстрого сравнения в протоколе. |
||||
- **Ed25519-подписи.** Каждая запись подписана автором. Записи без подписи или с неверной подписью отвергаются. |
||||
- **Append-only.** Записи не редактируются и не удаляются явно, только TTL-очистка собственных неотправленных записей. |
||||
- **PUSH — мгновенная доставка.** При локальном `insert_signed` запись немедленно шлётся всем пирам с `sync_state >= 1` (steady state). |
||||
- **Sync — сравнение цепей.** При старте/реконнекте стороны обмениваются хешами цепей для поиска расхождений. |
||||
- **Multi-instance.** Несколько независимых таблиц внутри одного процесса (разные hash). |
||||
|
||||
## 3. Архитектура протокола |
||||
|
||||
### 3.1. На что оптимизирован |
||||
|
||||
99% времени новые записи просто дописываются в конец. Самый частый сценарий — пир A добавил сообщение, пир B получил его и вставил в конец своей цепи. Протокол должен: |
||||
- Доставлять новые записи немедленно (PUSH) |
||||
- При реконнекте быстро понять "у нас всё совпадает до позиции N, добрось хвост" (hash_MATCH) |
||||
- При реальном расхождении бинарным поиском найти точку и слить (divergence + REFINE) |
||||
|
||||
### 3.2. Общие идеи реализации |
||||
|
||||
**Криптографическая цепь** — гарантия целостности. Если `chain_hash8(N)` совпадает на двух пирах, цепь идентична до позиции N (вероятность коллизии 2^-64). При вставке не в конец — каскадный пересчёт `chain_hash` всех последующих записей через `db_cascade_from`. |
||||
|
||||
**PUSH** — рабочий механизм доставки в steady state. Запись, вставленная локально, немедленно уходит всем синхронизированным пирам. ACK_PUSH подтверждает получение и обновляет delivery_chain. |
||||
|
||||
**Sync** — полное сравнение цепей при старте/реконнекте. Три ветки: peer_empty (пир пуст — отдать всё), hash_MATCH (цепи совпали до позиции N — отдать хвост), divergence (цепи разошлись — найти точку расхождения). |
||||
|
||||
**Sparse checkpoints** — бинарный поиск расхождения. INIT_RESP возвращает хеши в степенях двойки от tp (tp-1, tp-2, tp-4, ..., до 16 шт). Это позволяет за O(log N) сравнений сузить диапазон, не передавая хеш каждой записи. |
||||
|
||||
**REFINE** — финальное сужение. Если sparse-хешей INIT_RESP недостаточно (диапазон >1), REFINE запрашивает до 16 дополнительных хешей. Когда диапазон ≤1 — сразу SEND_DATA. |
||||
|
||||
**Каскадное уведомление.** При вставке записи не в конец `synced_pos` всех остальных пиров сбрасывается до позиции вставки — им потребуется пересинхронизация. |
||||
|
||||
### 3.3. Фазы протокола |
||||
|
||||
**Фаза А — Инициализация.** `db_sync_instance_add` читает таблицу из SQLite, проверяет целостность цепи (`db_verify_chain` — автофикс при расхождении), и немедленно шлёт `INIT_SYNC(my_count)` всем подключённым пирам с `sync_state == 0`. Если связь появилась позже — `conn_up` делает то же самое. |
||||
|
||||
**Фаза Б — Сравнение цепей.** Получатель INIT_SYNC вычисляет `tp = min(my_count, peer_count)`, tp-- если >0 (последняя гарантированно общая позиция), и возвращает INIT_RESP: `peer_ch8` на позиции tp, `sc` (количество sparse-хешей), sparse-хеши на позициях tp-2^k. |
||||
|
||||
**Фаза В — Три ветки:** |
||||
|
||||
| Ветка | Условие | Сценарий | Действие | |
||||
|-------|---------|----------|----------| |
||||
| peer_empty | `peer_ch8==0 && sc==0` | Пир пуст (0 записей) | Отправить все свои записи через SEND_DATA | |
||||
| hash_MATCH | `my_ch8 == peer_ch8` | Цепи идентичны до tp | Отправить хвост [tp+1..mc) через SEND_DATA | |
||||
| divergence | `my_ch8 != peer_ch8` | Разные истории | Анализ sparse-хешей → REFINE → SEND_DATA | |
||||
|
||||
**Фаза Г — REFINE.** Инициатор анализирует sparse-хеши: `ds` — последняя совпавшая позиция, `de` — первая разошедшаяся. Если `de-ds ≤ 1` → сразу SEND_DATA (hc=0). Иначе → REFINE с до 16 своих хешей, равномерно распределённых в [ds..de]. Получатель сравнивает со своей цепью, находит точку совпадения, шлёт SEND_DATA от этой точки. |
||||
|
||||
**Фаза Д — SEND_DATA.** Получатель вставляет записи с проверкой Ed25519-подписи, делает `db_cascade_from(fix_from)`, уведомляет остальных пиров о сдвиге цепи (сброс их synced_pos). Если у получателя после вставки записей больше чем у отправителя — proactive push-back (шлёт свой хвост). Когда все записи получены → SYNC_DONE с итоговым count и chain_hash8. |
||||
|
||||
**Фаза Е — SYNC_DONE.** Сравнение итогового count и chain_hash8. Не совпало — ретрай всей процедуры с начала (до 3 раз, потом give up с partial sync). Совпало — `sync_state=2`, синхронизация завершена. |
||||
|
||||
### 3.4. PUSH — отдельный от sync механизм |
||||
|
||||
PUSH матчится по `hash` — если у пира нет si с таким же hash, PUSH не доставляется. Это позволяет изолировать тестирование sync-протокола от PUSH: вставлять данные через si с уникальным hash (PUSH не уходит — нет получателя), затем удалять tmp si (данные в SQLite сохраняются), создавать si с общим hash — instance_add запускает чистый sync. |
||||
|
||||
## 4. Peer management |
||||
|
||||
- При поднятии ETCP-соединения для каждого инстанса добавляется `SI_PEER` и запускается синхронизация. |
||||
- При разрыве соединения `sync_state` пира сбрасывается в 0. |
||||
- `peer_check` таймер (каждые 5с) перебирает активных пиров и запускает синхронизацию для тех, у кого `sync_state == 0`. Выбирается пир с минимальным `synced_pos` — двигаемся от самого старого несинхронизированного участка. |
||||
- PUSH рассылается только пирам в состоянии `sync_state >= 1`. |
||||
|
||||
## 5. Сообщения протокола |
||||
|
||||
| Сообщение | Wire-формат | Описание | |
||||
|-----------|-------------|----------| |
||||
| `INIT_SYNC (0x01)` | `[type:1][count:4]` | Инициатор шлёт количество записей | |
||||
| `INIT_RESP (0x02)` | `[type:1][tp:4][ch8:8][sc:1][(pos:4,ch8:8)*sc]` | tp, chain_hash8 на tp, sc sparse-хешей | |
||||
| `REFINE (0x03)` | `[type:1][from:4][to:4][hc:1][(pos:4,ch8:8)*hc]` | hc=0 → сразу SEND_DATA; hc>0 → до 16 хешей в диапазоне | |
||||
| `SEND_DATA (0x04)` | `[type:1][from:4][count:2][(id:8,ts:8,author:8,dlen:4,data,sig_len:1,sig)*count]` | Пакет до 32 записей | |
||||
| `PUSH (0x05)` | `[type:1][id:8][ts:8][author:8][dlen:4][data][sig_len:1=64][sig:64]` | Рассылка одной записи synced-пирам | |
||||
| `ACK_PUSH (0x06)` | `[type:1][ts:8][author:8]` | Подтверждение PUSH, обновление delivery_chain | |
||||
| `SYNC_DONE (0x07)` | `[type:1][count:4][ch8:8]` | Финальный count + chain_hash8 | |
||||
| `ERROR (0x08)` | `[type:1][code:1]` | Коды: 0x01=NOT_FOUND, 0x02=DISABLED | |
||||
|
||||
Все сообщения маршрутизируются через ETCP service `0x20` с префиксом `[svc:1][hash_be:8][payload]`. |
||||
|
||||
## 6. Wire-формат записи |
||||
|
||||
`[id:8][ts:8][author_node_id:8][dlen:4][data:json][sig_len:1=64][sig:ed25519:64]` |
||||
|
||||
Дубликаты определяются по `(timestamp, author_signature)`. |
||||
|
||||
## 7. Структуры данных |
||||
|
||||
```c |
||||
struct DB_SYNC { |
||||
struct UTUN_INSTANCE* inst; |
||||
sqlite3* db; |
||||
uint8_t shared_db; |
||||
uint64_t last_connected_tb; |
||||
struct DB_SYNC_INSTANCE* instances; |
||||
int instance_count, instance_capacity; |
||||
void* peer_check_timer; |
||||
uint8_t enabled; |
||||
}; |
||||
|
||||
struct DB_SYNC_INSTANCE { |
||||
struct DB_SYNC* db_sync; |
||||
uint64_t hash; |
||||
char table_name[64]; |
||||
uint64_t next_id; |
||||
uint64_t last_timestamp_ms; |
||||
uint8_t enabled; |
||||
void* ttl_timer; |
||||
struct SI_PEER* peers; |
||||
int peer_count, peer_capacity; |
||||
db_sync_insert_cb on_insert; |
||||
void* on_insert_arg; |
||||
}; |
||||
|
||||
struct SI_PEER { |
||||
uint64_t node_id; |
||||
uint32_t synced_pos; // последняя общая позиция (0-based) |
||||
uint8_t sync_state; // 0=idle, 1=syncing, 2=synced |
||||
uint8_t sync_retry_count; // счётчик retry SYNC_DONE mismatch |
||||
uint64_t sync_start_tb; // время начала sync (для timeout) |
||||
}; |
||||
``` |
||||
|
||||
## 8. API |
||||
|
||||
### Глобальный жизненный цикл |
||||
|
||||
```c |
||||
int db_sync_init(struct UTUN_INSTANCE* inst); |
||||
void db_sync_destroy(struct UTUN_INSTANCE* inst); |
||||
``` |
||||
|
||||
`db_sync_init` — открывает SQLite по пути `<db_path>/chats.db`, биндит ETCP service `0x20`, вешает коллбэки соединений, стартует `peer_check` таймер (5с). Если `db_sync_enabled = 0` — disabled-режим. |
||||
|
||||
`db_sync_destroy` — отменяет таймеры, анбиндит сервис, снимает коллбэки, закрывает SQLite. |
||||
|
||||
### Управление инстансами |
||||
|
||||
```c |
||||
struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* table_name, uint64_t hash); |
||||
void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si); |
||||
``` |
||||
|
||||
`db_sync_instance_add` — создаёт/регистрирует инстанс. Создаёт SQLite-таблицу (если нет), читает цепь, `db_verify_chain` (автофикс), стартует TTL-таймер. Если есть подключённые пиры с `sync_state == 0` — немедленно шлёт `INIT_SYNC`. |
||||
|
||||
`db_sync_instance_remove` — деактивирует: `enabled=0`, отменяет TTL-таймер, освобождает peers, удаляет из массива. Таблица БД не удаляется. |
||||
|
||||
### Операции с данными |
||||
|
||||
```c |
||||
int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* json_data, size_t len, |
||||
const uint8_t* sig, size_t sig_len, uint64_t ts); |
||||
uint32_t db_sync_count(struct DB_SYNC_INSTANCE* si); |
||||
uint64_t db_sync_get_last_timestamp(struct DB_SYNC_INSTANCE* si); |
||||
uint64_t db_sync_next_timestamp(struct DB_SYNC_INSTANCE* si); |
||||
``` |
||||
|
||||
`db_sync_insert_signed` — вставляет подписанную запись. `sig` — 64 байта Ed25519. Проверяет подпись, проверяет дубликат, вычисляет chain_hash, вставляет с каскадным пересчётом, рассылает PUSH synced-пирам. Возвращает: 0=успех, 1=дубликат, -1=ошибка, -2=неверная подпись. |
||||
|
||||
### Чтение |
||||
|
||||
```c |
||||
typedef void (*db_sync_select_cb)(void* arg, uint64_t id, uint64_t timestamp, |
||||
const char* data, size_t data_len, uint64_t author, |
||||
const uint8_t* author_sig, size_t sig_len, |
||||
int delivered_peers, const char* delivery_chain); |
||||
int db_sync_select(struct DB_SYNC_INSTANCE* si, uint32_t offset, uint32_t limit, |
||||
db_sync_select_cb cb, void* arg); |
||||
``` |
||||
|
||||
### Callback на вставку |
||||
|
||||
```c |
||||
typedef void (*db_sync_insert_cb)(struct DB_SYNC_INSTANCE* si, |
||||
uint64_t record_timestamp, |
||||
const char* json_data, size_t len, |
||||
uint64_t author_node_id, void* arg); |
||||
void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb cb, void* arg); |
||||
``` |
||||
|
||||
### Верификация цепи |
||||
|
||||
```c |
||||
int db_sync_chain_verify(struct DB_SYNC_INSTANCE* si); |
||||
``` |
||||
|
||||
Возвращает 0 если все chain_hash корректны, 1 если расхождение. |
||||
|
||||
## 9. Конфигурация |
||||
|
||||
| Параметр | По умолчанию | Описание | |
||||
|----------|-------------|----------| |
||||
| `db_sync_enabled` | `0` | Включить модуль синхронизации | |
||||
| `db_sync_ttl` | `86400` | TTL неотправленных собственных записей (сек) | |
||||
| `db_path` | — | Путь к БД; файл `<db_path>/chats.db` | |
||||
|
||||
## 10. Константы |
||||
|
||||
| Константа | Значение | Описание | |
||||
|-----------|---------|----------| |
||||
| `ETCP_RT_ID_DB_SYNC` | `0x20` | ETCP service ID | |
||||
| `DB_REFINE_HASHES` | `16` | Макс. хешей в REFINE | |
||||
| `DB_SEND_DATA_MAX` | `32` | Макс. записей в SEND_DATA | |
||||
| `DB_SIG_SIZE` | `64` | Размер Ed25519 подписи | |
||||
| `DB_SYNC_PEER_CHECK_INTERVAL` | `5` | Интервал проверки пиров (сек) | |
||||
| `DB_SYNC_SYNC_TIMEOUT` | `15` | Таймаут ожидания ответа при sync (сек) | |
||||
| `DB_SYNC_TTL_INTERVAL` | `3600` | Интервал TTL-очистки (сек) | |
||||
|
||||
## 11. Тестирование sync-протокола |
||||
|
||||
PUSH доставляет записи немедленно и независимо от sync. Чтобы тестировать чистый sync-протокол, нужно чтобы PUSH не вмешивался: |
||||
|
||||
1. Вставить данные через si с **уникальным hash** — PUSH уходит, но не доставляется (нет получателя с таким hash) |
||||
2. `remove_si()` — данные сохраняются в SQLite, si деактивирован |
||||
3. Создать si с **общим hash** на обеих сторонах — `instance_add` запускает чистый sync-протокол |
||||
|
||||
Этот паттерн используется для тестирования всех трёх веток INIT_RESP: |
||||
- **peer_empty**: одна сторона с данными, другая пустая |
||||
- **hash_MATCH**: обе имеют общий префикс, но у одной больше записей |
||||
- **divergence**: независимые вставки на обеих сторонах до синхронизации |
||||
@ -1,29 +0,0 @@
|
||||
Возможности utun: |
||||
|
||||
- роутинг между нодами - напрямую (предпочтительный вариант), или через наилучшего кандидата. |
||||
Наилучший кандидат - узел с минимальным overall score. |
||||
overall score = правило выбираемок пользователем |
||||
% loss multiplier: |
||||
<0.3% - x1 |
||||
<1% - x0.5 |
||||
<5% - x0.2 |
||||
<10% - x0.1 |
||||
> - x0.03 |
||||
- либо minimum delay |
||||
|
||||
Формат данных: |
||||
todo... |
||||
|
||||
tun -> (src/dst ip) -> routing table -> next hop -> transmit to -> etcp -> routing -> tun |
||||
|
||||
routing: выбирает по dst ip next hop. |
||||
next hop = {type + conrol struct}. type= {1-tun, 2-etcp} |
||||
|
||||
|
||||
|
||||
|
||||
control dgram = connect request - если нет прямого соединения |
||||
при установке соединения сервер отправляет ip:port клиенту |
||||
|
||||
routing dgram: |
||||
<routing c.s.> -> <type, dgram> |
||||
@ -1,9 +0,0 @@
|
||||
Задача сделать механизм маршрутизации между узлами для групповых чатов. |
||||
|
||||
Узлы для всех чатов находятся в одной таблице - это nodeinfo (информация о подключении) |
||||
В другой таблице есть список узлов конкретного чата. |
||||
|
||||
надо сделать возможность хранить одновременно несколько групп узлов. в группу узлов добавить тип - utun или чатовая. |
||||
если utun - в группе используется роутинг (ipv4/ipv6). |
||||
если чатовая - то только связность между узлами. |
||||
|
||||
@ -1,3 +0,0 @@
|
||||
При добавлении узла группы в БД дополнительно |
||||
- проверяем текущие подключения |
||||
- если узел найден в активных подключениях то добавляем его в группу topo_group |
||||
@ -1,50 +0,0 @@
|
||||
# route6_lib — библиотека IPv6-маршрутизации на radix tree |
||||
|
||||
## 1. Назначение |
||||
Локальная таблица маршрутизации IPv6 для поиска longest-prefix match по заданному адресу. |
||||
Используется при пробросе трафика в TUN-интерфейс — по destination IP определяем узел-владелец |
||||
подсети и отправляем пакет ему через ETCP. |
||||
|
||||
Индексируется по `node_id` через `ll_queue`-хеш — это позволяет эффективно удалять все маршруты |
||||
узла при его withdraw из BGP-топологии. |
||||
|
||||
## 2. Как пользоваться |
||||
```c |
||||
struct ROUTE6_TABLE *rt = route6_table_create(ua); |
||||
|
||||
// При получении BGP-апдейта с новыми подсетями узла: |
||||
route6_insert(rt, nodeq); |
||||
|
||||
// При withdraw узла: |
||||
route6_delete(rt, nodeq); |
||||
|
||||
// При пробросе IPv6-пакета: |
||||
const struct ROUTE6_DATA *rd = route6_lookup(rt, dst_addr); |
||||
if (rd) forward_to_etcp(rd->v_node_info, pkt); |
||||
``` |
||||
|
||||
Ключевой нюанс: `make_key6` формирует 17-байтовый ключ в формате sockaddr-like |
||||
(первый байт = длина = 17, остальные 16 — адрес). Radix tree сравнивает ключи с отступом 1 байт |
||||
(RADIX_OFF), пропуская заголовок длины. |
||||
|
||||
## 3. API |
||||
|
||||
### Структуры |
||||
| Структура | Назначение | |
||||
|-----------|------------| |
||||
| `ROUTE6_DATA` | Запись маршрута: ключ+маска (17 байт sockaddr-like), `prefix_length`, `node_id` (для хеша), `v_node_info` (указатель на узел-владелец), `rn_nodes[2]` (scratch для radix) | |
||||
| `ROUTE6_TABLE` | Таблица: `rnh` (radix tree head), `queue` (ll_queue с хеш-индексом по `node_id`) | |
||||
|
||||
### Функции |
||||
| Функция | Назначение | |
||||
|---------|------------| |
||||
| `route6_table_create(ua)` | Создать таблицу: `rn_inithead` + `queue_new` с хеш-индексом по `node_id` | |
||||
| `route6_table_destroy(table)` | Удалить все маршруты (radix+queue), освободить таблицу | |
||||
| `route6_insert(table, node)` | Вставить все `v6_subnets` узла в radix tree и queue (с хеш-индексом) | |
||||
| `route6_delete(table, node)` | Удалить из radix tree все маршруты с заданным `node_id` (поиск через хеш) | |
||||
| `route6_lookup(table, addr)` | Longest-prefix match по 16-байтовому адресу, возвращает `ROUTE6_DATA*` или NULL | |
||||
|
||||
### Зависимости |
||||
- `lib/radix.h` — radix tree (longest-match lookup) |
||||
- `lib/ll_queue.h` — lock-free очередь с хеш-индексом для поиска по `node_id` |
||||
- `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_SUBNET6` |
||||
@ -1,42 +0,0 @@
|
||||
Ключевые изменения в понимании |
||||
|
||||
paths |
||||
|
||||
route_bgp_process_nodeinfo(bgp, from_conn, data, entry->len): |
||||
проверяет текущую версию nodeinfo, если совпадает - только bgp_update local nodelist |
||||
если версия новая или нет узла - spread: |
||||
bgp_update local nodelist |
||||
realloc: добавляет next hop: hoplist += prev_node_id, hop_count++ |
||||
bgp_spread |
||||
|
||||
1. функция распространения маршрута |
||||
bgp_spread(struct ROUTE_BGP bgp, struct NODEINFO* n) |
||||
send to: |
||||
- все подключения, если не найден uid подключения в hoplist |
||||
|
||||
|
||||
bgp_update local nodelist: |
||||
- обновляем саму node |
||||
- удалеям conn где lash hop=prev_node_id |
||||
- добавляем новый conn с новым hoplist |
||||
|
||||
2. withdraw: |
||||
bgp_withdraw(struct ROUTE_BGP bgp, uint64_t node_to_del, uint64_t wd_source) - wd_source это узел который захотел withdraw. |
||||
- находим у себя node to del. удаляем если в hoplist найден wd_node (или мы = wd_node) |
||||
если удалили - распространяем по всем линкам с этими же аргументами |
||||
|
||||
|
||||
|
||||
=============================== |
||||
определение nat |
||||
|
||||
определяем что оба узла в локальной сети: |
||||
-если обе ноды имеют локальные адреса сокетов пробуем пинг по локальным адресам |
||||
|
||||
если узел подключился и его сокетовый ip:port отличается от connection ip:port - устанавливаем флаг nat. и инициируем проверку пингом третьим узлом: |
||||
проверка считается успешной если по результату пинга ip источника пинга отличается от ip запросившего пинг узла. |
||||
|
||||
проверка типа nat узла который подключился: |
||||
- пингуем узел с целью зафиксировать свой ip (в сокете может быть например 0.0.0.0) |
||||
- даём команду третьему узлу пингануть этот узел. |
||||
- по результатам двух пингов определяем тип nat |
||||
@ -1,68 +0,0 @@
|
||||
# route_lib – библиотека таблицы IPv4-маршрутизации |
||||
|
||||
## 1. Назначение |
||||
Ведёт таблицу IPv4-маршрутов в памяти: динамический массив `ROUTE_ENTRY`, отсортированный по network-адресу с поддержкой вставки/удаления и LPM-поиска (Longest Prefix Match). Используется модулем `routing` для принятия решения, куда слать пакет — какому узлу через ETCP или локально в TUN. |
||||
|
||||
Таблица не содержит логики синхронизации — наполнение маршрутами идёт через BGP (`topo_node`). |
||||
|
||||
## 2. Как пользоваться |
||||
```c |
||||
struct ROUTE_TABLE* rt = route_table_create(); |
||||
|
||||
// Вставка подсетей узла (из BGP): |
||||
route_insert(rt, topo_nodeq); |
||||
|
||||
// Поиск маршрута (LPM): |
||||
struct ROUTE_ENTRY* e = route_lookup(rt, dest_ip); |
||||
if (e && e->v_node_info && e->v_node_info->hop_count > 0) { |
||||
// Отправить через ETCP узлу e->v_node_info->node->node_id |
||||
} else { |
||||
// Локальный маршрут — отдать в TUN |
||||
} |
||||
|
||||
// Удаление маршрутов узла: |
||||
route_delete(rt, topo_nodeq); |
||||
|
||||
route_table_destroy(rt); |
||||
``` |
||||
|
||||
**Ключевые нюансы:** |
||||
- Адреса в `ROUTE_ENTRY.network` хранятся в **big-endian** (сетевой порядок) |
||||
- Записи отсортированы по `network` ASC, при равенстве — по `prefix_length` DESC (более специфичные раньше) |
||||
- Алгоритм LPM: бинарный поиск с fallback-проверкой соседней записи |
||||
- `v_node_info == NULL` или `hop_count == 0` означает **локальный маршрут** |
||||
- Пересечение маршрутов запрещено — `route_insert` проверяет `check_route_overlap_in_table` |
||||
- Ёмкость массива динамически расширяется (×2) при переполнении |
||||
|
||||
## 3. API |
||||
|
||||
### Структуры |
||||
|
||||
| Структура | Поля | Описание | |
||||
|-----------|------|----------| |
||||
| `ROUTE_ENTRY` | `network` (uint32_t, BE), `prefix_length` (uint8_t), `v_node_info` (TOPO_GROUP_NODE*) | Запись маршрута. `v_node_info==NULL` → локальный | |
||||
| `ROUTE_TABLE` | `entries`, `count`, `capacity`, `dynamic_subnets`, `local_subnets`, `stats` | Таблица маршрутизации. Динамический массив + статистика | |
||||
|
||||
### Функции |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `route_table_create()` | Выделяет `ROUTE_TABLE`, `entries` на `INITIAL_CAPACITY=100`, счётчики в 0 | |
||||
| `route_table_destroy(table)` | Освобождает `entries`, `dynamic_subnets`, `local_subnets`, саму таблицу | |
||||
| `route_insert(table, node)` | Вставляет все v4-подсети узла (`TOPO_GROUP_NODE`) с сортировкой. Проверяет overlap. Расширяет capacity при необходимости | |
||||
| `route_delete(table, node)` | Удаляет все записи, где `v_node_info == node`, со сдвигом массива | |
||||
| `route_lookup(table, dest_ip)` | LPM-поиск. Возвращает `ROUTE_ENTRY*` или NULL. Инкрементит `stats` (hits/misses) | |
||||
| `route_table_print(table)` | Печатает содержимое таблицы в лог | |
||||
| `parse_subnet(str, network, prefix_length)` | Парсит `"a.b.c.d/n"` → network (BE) + prefix_length. Возвращает 0/-1 | |
||||
| `is_local_subnet(ip)` | Проверяет, является ли IP локальным/мультикастовым/зарезервированным (0=true). RFC1918 + 0.0.0.0/8 + 127.0.0.0/8 + АPIPA + multicast + broadcast + CGNAT (100.64.0.0/10) | |
||||
| `route_add_local_subnet(table, network, prefix_length)` | **Объявлена в .h, но не реализована** | |
||||
|
||||
### Внутренние функции |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `prefix_to_mask(prefix)` | Преобразует длину префикса в маску: `/24` → `0xFFFFFF00` | |
||||
| `routes_overlap(n1, p1, n2, p2)` | Проверяет, совпадают ли две подсети (одинаковые network+prefix) | |
||||
| `check_route_overlap_in_table(net, pre, entries, count)` | Проверяет overlap со всеми существующими записями | |
||||
| `binary_search_insert_pos(entries, count, net, pre)` | Находит позицию для вставки с сохранением сортировки | |
||||
| `binary_search_lpm(table, dest_ip)` | Бинарный LPM-поиск с fallback на соседнюю запись | |
||||
@ -1,11 +0,0 @@
|
||||
Установка прямого подключения между узлами |
||||
|
||||
1. узел собирает адреса - кандидаты для подключения: |
||||
список пар адрес-порт: |
||||
- адреса и порты из конфига сокетов (если не 0.0.0.0) |
||||
- адреса и порты из сокетов (nat) где nat = EIM |
||||
список ретрансляторов узлов-кандидатов: |
||||
- rtt, nodeid, адрес-порт |
||||
|
||||
|
||||
пары ip:port передаются на peer в команде "direct connection request" ROUTE_SUBCMD_CONN_REQ 0x0B |
||||
@ -1,315 +0,0 @@
|
||||
# topo_group |
||||
|
||||
## 1. Назначение |
||||
|
||||
BGP-подобный обмен топологией узлов между пирами uTun через ETCP. Каждый узел хранит таблицу |
||||
известных узлов и при подключении нового пира синхронизирует её с ним. Изменения информации об |
||||
узле и обнаружение недоступности распространяются всем пирам (NODEINFO/WITHDRAW). |
||||
|
||||
Модуль поддерживает **изолированные группы** — разные «пространства имён» узлов с разной |
||||
семантикой: |
||||
- **TOPO_GROUP_TYPE_UTUN (1)** — VPN-сеть: обмен маршрутами (подсети) между узлами, NAT-детекция, |
||||
проверка связности. |
||||
- **TOPO_GROUP_TYPE_CHAT (2)** — чат-группа: только узлы, без подсетей, персистентность в SQLite. |
||||
|
||||
Группа по умолчанию (UTUN, `group_id = TOPO_GROUP_UTUN`) создаётся автоматически при |
||||
`topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для |
||||
chatgui). Узлы разных групп изолированы, маршрутизация пакетов внутри группы ведётся по |
||||
`group_id` из wire-заголовков. |
||||
|
||||
Помимо обмена топологией, модуль выполняет: |
||||
- **Сборку local_node** (`topo_group_update_my_nodeinfo`) — при старте и при изменениях сокетов/active-mode. |
||||
- **NAT-детекцию** — выделена в отдельный модуль `nat_detection.h` (свой ETCP_ID 0x02). |
||||
- **Поиск оптимального маршрута** (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей. |
||||
- **Зондирование связности** — `route_connectivity_probe_node()` для новых узлов. |
||||
- **Авто-подключение к узлам CHAT-группы** — `topo_group_connect` (бесконечный цикл до успеха). |
||||
- **Восстановление каскадно отвалившихся узлов** — `topo_recovery` (после разрыва соединения). |
||||
- **Invite/join** — `topo_group_invite` (прямое подключение + проверка членства). |
||||
- **Персистентность в SQLite** — `topo_node_sqlite` (таблицы nodes, node_addresses, peers_*). |
||||
- **Per-group broadcast** — `broadcast` (дёшевая рассылка данных с TTL внутри группы). |
||||
|
||||
## 2. Как пользоваться |
||||
|
||||
### 2.1. Инициализация (при старте utun) |
||||
|
||||
```c |
||||
struct TOPO_GROUPS* g = topo_groups_init(instance); // контейнер + utun-группа по умолчанию |
||||
``` |
||||
Здесь же: |
||||
- Открывается SQLite (`{db_path}/chats.db`), если задан `db_path`; БД — `instance->topo_sqlite_db`. |
||||
- `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — приёмник пакетов топологии. |
||||
- `etcp_add_conn_status_cbk(instance, topo_group_conn_status, g)` — события UP/DOWN/DELETE всех соединений. |
||||
- `etcp_add_socket_cbk(...)` — изменения адресов/статуса сокетов. |
||||
- `utun_add_activity_cbk(...)` — смена active/standby режима. |
||||
|
||||
### 2.2. Новое ETCP-соединение |
||||
|
||||
При ETCP UP (`topo_group_conn_status` → `topo_group_new_conn` для каждой группы): |
||||
1. Дедупликация (тот же conn стреляет UP дважды — UDP- и TCP-линк). |
||||
2. `topo_recovery_cancel_for_node` — узел появился в сети, отменяем его recovery. |
||||
3. Обновление peer-узла: `conn_presence/conn_up |= NCONN_DIRECT`. |
||||
4. `topo_group_add_to_senders` + `topo_group_send_table_request` (запрос полной таблицы). |
||||
5. `topo_group_connect_on_up` (авто-подключение CHAT). |
||||
|
||||
На `REQUEST_TABLE` пир отвечает: NODEINFO о себе (local_node) + full table sync (все узлы, кроме |
||||
тех, чей путь уже включает этого пира — защита от петель) + `TABLE_COMPLETE`. При получении |
||||
`TABLE_COMPLETE` вызывается `etcp_set_routing_exchange_state(from_conn, 3)`. |
||||
|
||||
### 2.3. NODEINFO (обновление/создание узла) |
||||
|
||||
`topo_group_process_nodeinfo(group, from, data, len)`: |
||||
1. Проверка `group_id` пакета (`ni->group_id == group->group_id`). |
||||
2. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS` (CHAT → 0, UTUN → 1); |
||||
при несовпадении — `TOPO_SUBCMD_ERR_GROUP_MISMATCH`. |
||||
3. Проверка `hop_count < MAX_HOPS`; пропуск собственного `node_id`. |
||||
4. Проверка версии: если `(int8_t)(last_ver - new_ver) >= 0` (stale) — только обновляется путь. |
||||
5. Десериализация wire-формата → `TOPO_NODE` (глобальная идентичность) + подсети + hop_list. |
||||
6. Верификация идентичности: `node_id == sc_derive_node_id_from_pubkey(public_key)` и |
||||
Ed25519 self-signature (`x25519_self_sig`) над каноническим сообщением. |
||||
7. Сохранение в глобальный реестр `node_registry` (дедупликация по node_id, ref_count). |
||||
8. Обновление/создание `TOPO_GROUP_NODE` (per-group): paths, last_ver, subnets. |
||||
9. `topo_group_remove_path_by_hop` + `topo_group_add_path` (через текущий conn). |
||||
10. `conn_presence/conn_up |= NCONN_BGP` (+ `NCONN_DIRECT` если прямой). |
||||
11. `route_insert` — только для UTUN-групп. |
||||
12. SQLite: `topo_node_sqlite_member_exists` → `addrs_put`; для CHAT — `member_placeholder_put` |
||||
(source=1, вне merkle-хеша); `nodeinfo_updated`. |
||||
13. `control_server_notify_node_change`. |
||||
14. Callback `node_updated_cb` (legacy, в текущем коде потребителей нет). |
||||
15. Fire BGP node event callbacks (`TOPO_NODE_EVENT_NEW`/`UPDATE`) — используют member_sync, media_delivery. |
||||
16. `topo_fire_nodeinfo_cbk` — глобальные nodeinfo-подписчики. |
||||
17. Broadcast NODEINFO всем пирам, кроме самого узла и тех, чей node_id есть в hop_list (антипетля). |
||||
18. `route_connectivity_probe_node` для новых/изменённых узлов с адресами. |
||||
|
||||
### 2.4. WITHDRAW (удаление узла) |
||||
|
||||
`topo_group_process_withdraw(group, sender, data, len)`: |
||||
1. Поиск `TOPO_GROUP_NODE` по `node_id`. |
||||
2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление путей, содержащих wd_source в hop_list. |
||||
3. Если путей не осталось: сброс `conn_presence/conn_up`, `route_delete` (UTUN), уведомление |
||||
control_server, отмена connectivity probe, освобождение paths и group-полей, удаление из |
||||
`nodes`, `etcp_router_conn_close_all_for_node`, broadcast withdraw (кроме отправителя), |
||||
fire `TOPO_NODE_EVENT_REMOVE`. |
||||
4. **BGP не удаляет member-запись из SQLite** — удаление из `peers_*` только через битую подпись |
||||
при верификации (member_sync). |
||||
|
||||
### 2.5. Отключение соединения |
||||
|
||||
При ETCP DOWN/DELETE (`topo_group_remove_conn`): |
||||
1. Проверка наличия conn в senders_list (если нет — выход). |
||||
2. Для каждого узла с путями через conn: вычисляется next_hop из hop_list, `remove_path`. Если |
||||
узел стал недоступен — `topo_recovery_add_node` (для каскадных, кроме самого пира), полная |
||||
очистка (как в WITHDRAW) + broadcast withdraw. |
||||
3. Удаление conn из `senders_list`. |
||||
4. Если были каскадные узлы — `topo_recovery_start`. |
||||
5. `topo_group_connect_on_down`. |
||||
|
||||
### 2.6. Поиск пути до узла |
||||
|
||||
`topo_group_find_conn_for_node(group, node_id)` — перебирает paths узла, выбирает min `hop_count`, |
||||
предпочитая live-пути (`conn->links_up > 0`); fallback на любой путь. Интенсивно используется |
||||
`etcp_router` и `conn_mgr` (next-hop для маршрутов и multihop-соединений). |
||||
|
||||
## 3. API |
||||
|
||||
### 3.1. Структуры |
||||
|
||||
| Структура | Назначение | |
||||
|---|---| |
||||
| `TOPO_GROUPS` | Контейнер всех групп. `group_list` (hash), `node_registry` (глобальный реестр TOPO_NODE*, hash), memory_pool'ы (v4/v6 sock_meta, addr, subnet), `node_updated_cb` (legacy) | |
||||
| `TOPO_GROUP` | Одна группа. `group_id`, `group_type`, `nodes` (queue TOPO_GROUP_NODE), `senders_list` (queue conn'ов), `local_node`, `ed25519_public_key`, `channel_id` (CHAT), `conn_mgr`, `recovery_list`, `connect`, `broadcast`, `node_cbks` | |
||||
| `TOPO_NODE` | Глобальная идентичность узла (в `node_registry`): pubkeys, `x25519_self_sig`, name, v4/v6 sock_meta+addrs, `client_type`, `client_activity`, `ver`, `group_ref_count` | |
||||
| `TOPO_GROUP_NODE` | Per-group запись узла: `node_id`, `subnets`, `paths`, `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) | |
||||
| `TOPO_NODEPATH` | Путь до узла через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]` | |
||||
| `TOPO_NODESUBNETS` | Подсети узла: списки `TOPO_SUBNET4`/`TOPO_SUBNET6` | |
||||
| `TOPO_CONNECTIVITY` | Связность: probe_status, rtt-метрики, статусы (interface/nat/real), `probe_deferred_wait` (standby) | |
||||
| `TOPOMSG_NODEINFO_PKT` | Wire NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть | |
||||
| `TOPOMSG_WITHDRAW_PKT` | Wire WITHDRAW: `group_id`, `node_id` (удаляемый), `wd_source` (инициатор) | |
||||
| `TOPOMSG_TABLE_REQ` | Wire запроса/завершения таблицы: `group_id` | |
||||
| `TOPOMSG_ERR_GROUP_MISMATCH` | Wire ошибки типа группы: `group_id`, `expected_type`, `received_flags` | |
||||
| `NAT_DETECTION` | Контекст NAT-детекции (отдельный модуль) | |
||||
|
||||
### 3.2. Константы |
||||
|
||||
| Константа | Значение | Назначение | |
||||
|---|---|---| |
||||
| `ETCP_ID_TOPO_ENTRY` | 0x01 | ETCP ID пакетов топологии | |
||||
| `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле | |
||||
| `TOPO_SUBCMD_REQUEST_TABLE` | 0x05 | Запрос полной таблицы | |
||||
| `TOPO_SUBCMD_WITHDRAW` | 0x06 | Узел стал недоступен | |
||||
| `TOPO_SUBCMD_TABLE_COMPLETE` | 0x0B | Завершение начальной синхронизации | |
||||
| `TOPO_SUBCMD_ERR_GROUP_MISMATCH` | 0x0C | Ошибка несоответствия типа группы | |
||||
| `MAX_HOPS` | 16 | Максимальная длина hop-листа | |
||||
| `BGP_NODES_HASH_SIZE` / `TOPO_NODE_REGISTRY_HASH_SIZE` | 256 | Хеш-таблицы nodes / node_registry | |
||||
| `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id utun-группы по умолчанию | |
||||
| `TOPO_GROUP_TYPE_UTUN` / `TOPO_GROUP_TYPE_CHAT` | 1 / 2 | Тип группы | |
||||
| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг NODEINFO: узел передаёт подсети | |
||||
| `NCONN_DIRECT` / `NCONN_INDIRECT` / `NCONN_BGP` | 1/2/4 | Биты наличия соединений (presence/up) | |
||||
| `CONN_TYPE_DIRECT` / `REVERSE` / `INDIRECT` / `NONE` | 1/2/3/0 | Стратегия conn_mgr | |
||||
| `CLIENT_TYPE_SERVER` / `DESKTOP` / `MOBILE` | 0/1/2 | Тип клиента | |
||||
| `CLIENT_ACTIVITY_STANDBY` / `ACTIVE` | 0/1 | Режим (standby/active) | |
||||
| `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы | |
||||
| `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы | |
||||
| `TOPO_NODE_EVENT_NEW/UPDATE/REMOVE` | 0/1/2 | События BGP node callbacks | |
||||
| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников для INDIRECT | |
||||
|
||||
### 3.3. Инициализация/завершение |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_groups_init(instance)` | Контейнер + node_registry + пулы + SQLite + utun-группа + ETCP/activity коллбэки | |
||||
| `topo_groups_destroy(instance)` | Отвязка коллбэков, destroy групп, node_registry, пулов, закрытие SQLite | |
||||
| `topo_group_create(instance, group_id, group_type)` | Создать TOPO_GROUP: nodes/senders/local_node/conn_mgr/broadcast | |
||||
| `topo_group_destroy(group)` | recovery_cancel_all, connect_destroy, broadcast_destroy, conn_mgr_destroy, очистка nodes/local_node | |
||||
| `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создать группу + добавить в group_list + new_conn для существующих соединений + connect_init (CHAT) | |
||||
| `topo_groups_find(g, group_id)` / `topo_groups_get_default(g)` | Поиск группы / utun-группа | |
||||
| `topo_group_on_activity_change(instance, active, arg)` | Обновить local_node и broadcast его по всем группам/пирам | |
||||
|
||||
### 3.4. Жизненный цикл соединения |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_group_new_conn(group, conn)` | Дедуп, отмена recovery, обновить peer, add_to_senders, request_table, connect_on_up | |
||||
| `topo_group_remove_conn(group, conn)` | Удалить пути, удалить недоступные узлы (recovery/broadcast), убрать conn, connect_on_down | |
||||
| `topo_group_conn_status(conn, status, arg)` | Callback status: UP→new_conn, DOWN/DELETE→remove_conn для каждой группы | |
||||
|
||||
### 3.5. Обработка пакетов |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_group_receive_cbk(from_conn, entry)` | Точка входа: извлекает `group_id`, находит группу, диспетчеризация по subcmd | |
||||
| `topo_group_process_nodeinfo(group, from, data, len)` | Полный цикл NODEINFO (см. 2.3) | |
||||
| `topo_group_process_withdraw(group, sender, data, len)` | Удаление путей/узла по WITHDRAW (см. 2.4) | |
||||
| `topo_group_handle_request_table(group, conn)` | local_node + full table + add_to_senders + TABLE_COMPLETE | |
||||
|
||||
### 3.6. Поиск и пути |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_group_find_conn_for_node(group, node_id)` | Оптимальный ETCP_CONN (min hop_count, предпочтение live) | |
||||
| `topo_node_find_by_id(group, node_id)` | Поиск TOPO_GROUP_NODE по node_id (hash) | |
||||
| `topo_group_add_path(nq, conn, hop_list, hop_count, cumulative_rtt)` | Создать TOPO_NODEPATH + inline hop_list | |
||||
| `topo_group_remove_path(nq, conn)` | Удалить путь; возвращает 1 если путей не осталось | |
||||
| `topo_group_remove_path_by_hop(nq, wd_source)` | Удалить пути, содержащие wd_source в hop_list | |
||||
| `topo_group_should_send_to(nq, target_id)` | Проверка антипетли (target_id не во всех путях) | |
||||
|
||||
### 3.7. Broadcast |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_group_send_nodeinfo(group, node, conn, cumulative_rtt)` | Сериализовать и отправить NODEINFO одному conn | |
||||
| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW (wd_source = self) | |
||||
| `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Broadcast WITHDRAW всем, кроме exclude | |
||||
| `topo_group_send_full_table(group, conn)` | Все известные узлы (антипетля) указанному conn | |
||||
| `topo_group_send_table_request/complete(group, conn)` | REQUEST_TABLE / TABLE_COMPLETE | |
||||
| `topo_group_send_err_group_mismatch(group, conn, ...)` | ERR_GROUP_MISMATCH | |
||||
|
||||
### 3.8. Коллбэки (подписки) |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_group_add_node_cbk/remove_node_cbk(group, fn, arg)` | BGP node events (NEW/UPDATE/REMOVE) — consumer: member_sync, media_delivery | |
||||
| `utun_add_nodeinfo_cbk/remove_nodeinfo_cbk(instance, fn, arg)` | Глобальная подписка на изменения nodeinfo (per-instance) | |
||||
| `topo_fire_nodeinfo_cbk(instance, group, node)` | Вызвать глобальные nodeinfo-подписчики | |
||||
| `topo_groups_set_node_updated_cb(groups, fn)` | Legacy callback (в текущем коде потребителей нет) | |
||||
|
||||
### 3.9. NAT (делегаты в nat_detection) |
||||
|
||||
| Функция | Назначение | |
||||
|---|---| |
||||
| `topo_group_set_nat_check_local(group, allow)` | Делегат в `nat_detection_set_allow_local` | |
||||
|
||||
### 3.10. Связанные модули |
||||
|
||||
| Модуль | Назначение | |
||||
|---|---| |
||||
| `topo_group_connect` | Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл, standby-aware) | |
||||
| `topo_group_invite` | Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP, ETCP_ID 0x13) | |
||||
| `topo_recovery` | Восстановление каскадно отвалившихся узлов после разрыва соединения | |
||||
| `broadcast` | Per-group рассылка данных с TTL (ETCP_ID_BROADCAST), `broadcast_init/сброс` | |
||||
| `topo_node` | Модель данных: TOPO_NODE/TOPO_GROUP_NODE, ser/deserialize, node_registry, sig-msg | |
||||
| `topo_node_sqlite` | Персистентность: nodes, node_addresses, peers_*, member block/owner/placeholder | |
||||
|
||||
## 4. Архитектурные связи |
||||
|
||||
### 4.1. Внешние зависимости |
||||
|
||||
| Модуль | Как используется | |
||||
|---|---| |
||||
| `topo_node.c/h` | Модель узла, ser/deserialize, node_registry, update_my_nodeinfo, sign_self | |
||||
| `topo_node_sqlite.h` | Персистентность (node_put, member_placeholder_put, addrs_put, nodeinfo_updated) | |
||||
| `nat_detection.h` | NAT-детекция (отдельный модуль, ETCP_ID 0x02) | |
||||
| `route_lib.h` | route_insert / route_delete | |
||||
| `route_connectivity.h` | probe_node / cancel_node / cancel_all | |
||||
| `etcp_api.h` | etcp_bind, etcp_add_conn_status_cbk, etcp_add_socket_cbk, etcp_unbind | |
||||
| `etcp_connections.h` | ETCP_CONN, conn->peer_node_id, links_up | |
||||
| `etcp.h` | etcp_send, etcp_set_routing_exchange_state | |
||||
| `etcp_router.h` | etcp_router_conn_close_all_for_node | |
||||
| `control_server.h` | notify_node_change / notify_node_removed | |
||||
| `conn_mgr.h` | conn_mgr_init/destroy (per-group), поиск путей | |
||||
| `secure_channel.h` | sc_derive_ed25519_pubkey, sc_derive_node_id_from_pubkey, sc_ed25519_verify | |
||||
| `broadcast.h` | broadcast_init/destroy (per-group) | |
||||
| `memory_pool.h` | Пулы для сериализации | |
||||
| `debug_config.h` | Логирование, категория BGP | |
||||
| `mem.h` | u_malloc/u_free/u_calloc | |
||||
|
||||
### 4.2. Кто вызывает topo_group |
||||
|
||||
| Вызывающий | Что вызывает | Для чего | |
||||
|---|---|---| |
||||
| `utun_instance.c` | `topo_groups_init/destroy` | Инициализация/завершение | |
||||
| `etcp_connections.c` | `topo_group_send_nodeinfo` | Переотправка local_node при смене сокетов | |
||||
| `etcp_router.c` | `topo_group_find_conn_for_node` | next-hop для маршрутов | |
||||
| `conn_mgr_*.c` | `topo_group_find_conn_for_node`, `topo_fire_nodeinfo_cbk` | Пути для соединений | |
||||
| `nat_detection.c` | `topo_group_send_nodeinfo`, `topo_group_update_my_nodeinfo` | Обновление local_node после NAT-чека | |
||||
| `member_sync.c` | `topo_group_add_node_cbk` | BGP → member_sync → node_props_changed | |
||||
| `media_delivery.c` | `topo_group_add_node_cbk` | События узлов для медиа | |
||||
| `route_connectivity.c` | `topo_fire_nodeinfo_cbk` | Уведомление об изменении узла | |
||||
|
||||
## 5. Форматы пакетов (wire) |
||||
|
||||
### 5.1. NODEINFO |
||||
``` |
||||
cmd(1) | subcmd(1) | TOPOMSG_NODE | [node_name] | [v4_sock_meta[]] | [v4_addrs[]] | [v6_sock_meta[]] | [v6_addrs[]] | [v4_subnets[]] | [v6_subnets[]] | [hop_list[]] |
||||
``` |
||||
`TOPOMSG_NODE` содержит `group_id`, `node_id`, `ver`, pubkeys, `x25519_self_sig(64)`, счётчики, |
||||
`hop_count`, `client_type`, `client_activity`, `cumulative_rtt`. |
||||
|
||||
### 5.2. WITHDRAW |
||||
``` |
||||
cmd(1) | subcmd(1) | group_id(8) | node_id(8) | wd_source(8) = 26 байт |
||||
``` |
||||
|
||||
### 5.3. REQUEST_TABLE / TABLE_COMPLETE |
||||
``` |
||||
cmd(1) | subcmd(1) | group_id(8) = 10 байт |
||||
``` |
||||
|
||||
### 5.4. ERR_GROUP_MISMATCH |
||||
``` |
||||
cmd(1) | subcmd(1) | group_id(8) | expected_type(1) | received_flags(1) = 12 байт |
||||
``` |
||||
|
||||
## 6. Примечания |
||||
|
||||
- **Глобальный реестр идентичности**: `TOPO_NODE` (pubkeys, адреса, имя) хранится один раз в |
||||
`node_registry` и разделяется между группами через `group_ref_count`. Per-group данные — в |
||||
`TOPO_GROUP_NODE` (paths, subnets, connectivity, conn_presence/up). |
||||
- **Версионирование NODEINFO**: `ver` (uint8_t) сравнивается циклически `(int8_t)(cur - new) >= 0`; |
||||
stale-версия только обновляет путь, не заменяя данные узла. |
||||
- **Верификация идентичности**: `node_id == derive(x25519)` + Ed25519 self-signature над каноническим |
||||
сообщением (`topo_node_build_sig_msg`). Несовпадение → пакет отвергается как forgery. |
||||
- **Защита от петель**: NODEINFO рассылается пирам, чей node_id отсутствует в hop_list всех путей; |
||||
WITHDRAW broadcast всем, кроме отправителя. |
||||
- **Hop-лист хранится в пути**: `TOPO_NODEPATH` содержит inline `hop_list[hop_count]` сразу после |
||||
структуры; `next_hop` для recovery вычисляется из него. |
||||
- **Связность**: для новых узлов с адресами запускается `route_connectivity_probe_node`; при |
||||
удалении — `route_connectivity_cancel_node`. В standby пробы откладываются (`PROBE_STATUS_DEFERRED`). |
||||
- **Виртуальные каналы**: при удалении узла закрываются `etcp_router_conn_close_all_for_node`. |
||||
- **CHAT-группа пишет плейсхолдер** `source=1` (pubkeys из BGP, без подписей) — вне merkle-хеша; |
||||
полная запись приходит позже через merkle-путь (member_sync). |
||||
- **BGP не удаляет member из SQLite**: работает только со своей RAM-таблицей; удаление из `peers_*` |
||||
— только битая подпись (member_sync). |
||||
- **NAT check для локальных сетей**: по умолчанию запрещён (`allow_nat_check_local=0`), включается |
||||
для тестов через `topo_group_set_nat_check_local`. |
||||
- **Обновление local_node при NAT/active-изменениях**: `nat_detection` и `topo_group_on_activity_change` |
||||
вызывают `topo_group_update_my_nodeinfo` → новая версия → NODEINFO broadcast всем пирам. |
||||
@ -1,89 +0,0 @@
|
||||
++ 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 по ссылке (приватный ключ пира). получает ключи других пиров. Может только читать. |
||||
админ подписывает разрешение на писать (как и другие разрешения). подпись храним в таблице юзеров |
||||
@ -1,289 +0,0 @@
|
||||
# etcp |
||||
|
||||
## 1. Назначение |
||||
|
||||
Ядро протокола ETCP — TCP-подобный надёжный транспорт поверх UDP с шифрованием (AES-CCM + X25519), multi-link (несколько каналов между двумя узлами), фрагментацией/сборкой пакетов, ретрансмиссией с экспоненциальным backoff, BBR congestion control и burst-измерением пропускной способности. |
||||
|
||||
Модуль управляет **одним ETCP-соединением** (struct `ETCP_CONN`) между локальным и удалённым узлом. Соединение может иметь несколько линков (struct `ETCP_LINK`), работающих через разные сокеты / сетевые пути. |
||||
|
||||
Этапы жизни соединения: |
||||
``` |
||||
создание (pending, state=0) → первый линк проинициализирован → ready (state=1) → close (state=2, detach) → ref_count=0 → полное освобождение ресурсов |
||||
``` |
||||
|
||||
## 2. Как пользоваться |
||||
|
||||
### 2.1. Типовой сценарий |
||||
|
||||
1. **Создание:** `etcp_connection_create(instance, name)` — создаёт `ETCP_CONN` в состоянии `pending` (state=0), размещает в очереди `instance->connections` с индексом peer_node_id=0. |
||||
2. **Добавление линков:** `etcp_link_new(etcp, socket, remote_addr, is_server)` — создаёт `ETCP_LINK`, добавляет в список `etcp->links`. |
||||
3. **Обмен ключами / INIT:** через `etcp_connections.c` происходит обмен INIT_REQUEST/INIT_RESPONSE, установка `peer_node_id`, инициализация `secure_channel` (X25519 + AES-CCM). |
||||
4. **Готовность:** `etcp_conn_ready()` → `etcp_conn_queue_set_ready()` — переиндексирует соединение в очереди `instance->connections` с реальным `peer_node_id`, переводит `state=1`, запускает callback'и `ready_cbks`, таймер метрик. |
||||
5. **Отправка данных:** `etcp_int_send(etcp, data, len)` — аллоцирует память из `data_pool`, создаёт `ETCP_FRAGMENT`, помещает в `input_queue`. |
||||
6. **Приём данных:** потребитель читает из `etcp->output_queue` — там лежат собранные `ETCP_FRAGMENT` с полезной нагрузкой. |
||||
7. **Закрытие:** `etcp_connection_close(etcp)` — 2-фазное: Phase 1 — detach от внешнего мира (линки, таймеры, роутинг), Phase 2 — отложенное освобождение ресурсов (очереди, пулы памяти, callback'и, struct) когда ref_count=0. |
||||
|
||||
### 2.2. Ключевые концепции |
||||
|
||||
#### Очереди данных (data flow) |
||||
|
||||
``` |
||||
┌──────────────┐ |
||||
et cp_int_send ──► │ input_queue │ входная очередь (ETCP_FRAGMENT из io_pool) |
||||
└──────┬───────┘ |
||||
│ input_queue_cb: перенос во inflight с присвоением seq |
||||
┌──────▼───────┐ |
||||
│ input_send_q │ очередь на отправку (INFLIGHT_PACKET из inflight_pool) |
||||
└──────┬───────┘ |
||||
│ etcp_request_pkt: формирует ETCP_DGRAM с секциями ACK+PAYLOAD |
||||
┌──────▼───────┐ |
||||
│input_wait_ack│ ожидание ACK (INFLIGHT_PACKET, index по seq) |
||||
└──────┬───────┘ |
||||
│ etcp_ack_recv: ACK получен → освобождение inflight |
||||
[удалён] |
||||
``` |
||||
|
||||
``` |
||||
Приём: |
||||
┌──────────┐ |
||||
etcp_conn_input ──► │ recv_q │ очередь сборки (ETCP_FRAGMENT, index по seq) |
||||
└────┬─────┘ |
||||
│ etcp_output_try_assembly: поиск непрерывной последовательности |
||||
┌────▼──────┐ |
||||
│output_queue│ собранные пакеты для потребителя |
||||
└───────────┘ |
||||
``` |
||||
|
||||
- **`input_queue`** (ETCP_FRAGMENT) — пакеты от приложения к отправке. Callback `input_queue_cb` переносит их в `input_send_q`, создавая `INFLIGHT_PACKET` с seq. |
||||
- **`input_send_q`** (INFLIGHT_PACKET, hash-индекс по seq) — ожидающие отправки (новые или ретрансмиссии). Callback `input_send_q_cb` вызывает `etcp_conn_process_send_queue`. |
||||
- **`input_wait_ack`** (INFLIGHT_PACKET, hash-индекс по seq) — отправленные пакеты, ожидающие ACK. Callback `wait_ack_cb` запускает retrans-таймер. |
||||
- **`recv_q`** (ETCP_FRAGMENT, hash-индекс по seq) — принятые фрагменты, ждущие сборки. |
||||
- **`output_queue`** — собранные непрерывные пакеты для потребителя. |
||||
- **`ack_q`** (ACK_PACKET, hash-индекс по seq) — неотправленные ACK подтверждения, ожидающие piggyback. |
||||
- **`transit_queues`** — транзитные очереди для роутинга (хеш по `src_node_id:dst_node_id`). |
||||
|
||||
#### Ретрансмиссия с экспоненциальным backoff |
||||
|
||||
- Таймаут ретрансмиссии: `timeout = (rtt_avg_10 * K1 + jitter * K2) / 16`, где `K1=32`, `K2=32`. |
||||
- Минимальный timeout: 50 tb (5ms), максимальный: 10000 tb (1000ms). |
||||
- Пакеты в `input_wait_ack` упорядочены по времени отправки (FIFO). Проверка (`ack_timeout_check`) идёт с головы: как только найден пакет с неистекшим таймаутом — взводится таймер до его истечения, дальше не сканируем. |
||||
- Ретрансмит: перенос из `input_wait_ack` в `input_send_q`, инкремент `send_count`, добавление в `send_hist`. |
||||
- При ретрансмите старый линк (`last_link`) получает `total_retransmissions++` и вычитание из `inflight_bytes/packets`. |
||||
|
||||
#### RTT/Jitter/Bandwidth метрики |
||||
|
||||
- **RTT:** измеряется через секцию `ETCP_SECTION_TIMESTAMP` — результирующий `rtt_last` на соединение берётся как среднее по всем линкам (rtt_sum/cnt), `rtt_avg_10` — максимум RTT по линкам. |
||||
- **Jitter:** per-link экспоненциальное сглаживание разности RTT: `jitter += (|new_rtt - prev_rtt| * 65536 - jitter) / 32`. На соединении — среднее по линкам. |
||||
- **TT (transmission time):** время доставки отправленных пакетов, вычисляется из timestamp-секции. |
||||
- **RT (recv time):** время доставки принятых пакетов. |
||||
- **Bandwidth:** per-link, обновляется из BBR pacing rate и burst-измерений. |
||||
|
||||
#### Burst bandwidth measurement |
||||
|
||||
- Отправитель формирует пачку из `BURST_PACKET_COUNT` (12) пакетов, каждый содержит секцию `MEAS_TS` (type=0x07) с burst_id, flags, порядковым номером и µs-таймстемпом. |
||||
- Флаги: `IS_FIRST`, `IS_LAST`, `IS_FILLER` (если нет данных — пакет заполняется FILLER-секцией). |
||||
- Пропускаются первые `BURST_SKIP_COUNT` (3) пакета для исключения переходных процессов при замере inter-packet gap. |
||||
- Приёмник собирает времена прихода, вычисляет min/avg gap между пакетами, отправляет ответ `MEAS_RESP` (type=0x08) с gap_avg, gap_min, pkt_count. |
||||
- Bandwidth вычисляется как: `BW = pkt_size * 8000 / gap_min` (Kbps), BDP = `BW * 1000 / 8 * min_rtt_sec`. |
||||
- Минимальный интервал между burst: 500ms. Таймаут ожидания ответа: 2s. |
||||
- Ответ может быть piggyback'нут в обычный пакет (вне активного burst). |
||||
|
||||
#### Channel timestamp handling |
||||
|
||||
- Секция `ETCP_SECTION_TIMESTAMP` (type=0x06, 5 байт): добавляется когда есть новые данные о времени приёма от пира. |
||||
- Содержит: ret_ts (текущее время пира на момент получения нашего пакета) и dt (разница local_time - timestamp пакета). |
||||
- Позволяет вычислять RTT, jitter, tt, rt на всех линках, а также `recv_dt_avg_rx/tx` — экспоненциально сглаженные оценки односторонних задержек. |
||||
|
||||
#### 2-phase deferred cleanup |
||||
|
||||
- **Phase 1** (синхронно в `etcp_connection_close`): detach от внешнего мира — закрытие линков, отмена таймеров, остановка метрик, удаление из роутинга, удаление из очереди `instance->connections`, установка `state=2`. |
||||
- **Phase 2** (отложено через `uasync_call_soon`): освобождение очередей, memory_pool'ов, callback-цепочек, struct. Выполняется когда `ref_count == 0`. Если есть внешние ссылки — ждёт `etcp_conn_ref_free()`. |
||||
- `ref_count` нужен для безопасного доступа к conn из асинхронных контекстов (коллбэки, роутинг, BGP). |
||||
|
||||
#### Backpressure (пороговое ожидание) |
||||
|
||||
- `input_queue` имеет порог 0 (ждёт полного освобождения) — новые пакеты добавляются только когда очередь пуста. |
||||
- `etcp_conn_process_send_queue` пробует протолкнуть данные из `input_queue` → `input_send_q` когда `wait_ack_bytes <= optimal_inflight`. |
||||
- При ACK вызывается `input_queue_try_resume` — если `wait_ack_bytes <= optimal_inflight` и input_send_q пуста, возобновляет `input_queue_cb`. |
||||
|
||||
#### Normalizer (фрагментация/сборка) |
||||
|
||||
- `struct PKTNORM` — per-connection фрагментатор/дефрагментатор. Разбивает большие пакеты на части ≤ `frag_size` (MTU - overhead). |
||||
- При reinit/reset несобранные фрагменты возвращаются в normalizer для повторной обработки (`etcp_return_inflight_to_normalizer`). |
||||
|
||||
### 2.3. State machine |
||||
|
||||
| Поле | Значения | Описание | |
||||
|------|----------|----------| |
||||
| `state` | 0=pending, 1=ready, 2=deleted | Внешнее состояние conn | |
||||
| `initialized` | 0/1 | Хотя бы один линк прошёл обмен ключами | |
||||
| `reset_done` | 0/1 | 0=reinit разрешён, 1=соединение стабильно | |
||||
| `tx_state` | DATA_WAIT(1)/LINK_WAIT(2) | DATA_WAIT=можно отправлять, LINK_WAIT=все линки busy | |
||||
| `links_up` | 0/1 | Хотя бы один линк в статусе up | |
||||
| `got_initial_pkt` | 0/1 | Получен ли первый пакет (seq=1) после reset | |
||||
| `routing_exchange_active` | 0-4 | Статус BGP-обмена | |
||||
|
||||
## 3. API |
||||
|
||||
### 3.1. Основные структуры |
||||
|
||||
#### `struct ETCP_CONN` (`etcp.h:140`) |
||||
|
||||
Главная управляющая структура соединения. Содержит: |
||||
- **Состояние:** `state`, `ref_count`, `initialized`, `reset_done`, `links_up`, `tx_state`. |
||||
- **Очереди:** `input_queue`, `input_send_q`, `input_wait_ack`, `recv_q`, `output_queue`, `ack_q`, `transit_queues`. |
||||
- **Пулы памяти:** `inflight_pool` (для INFLIGHT_PACKET), `io_pool` (для ETCP_FRAGMENT). |
||||
- **Линки:** связный список `links`, `last_rr_link` (round-robin для load balancer). |
||||
- **Крипто:** `crypto_ctx` (secure_channel), `peer_ed25519_pubkey`. |
||||
- **Нормалайзер:** `normalizer` (PKTNORM) — фрагментация/сборка. |
||||
- **Метрики:** `rtt_last/avg_10/avg_100`, `jitter`, `tt_last`, окна (unacked_bytes, max_inflight, optimal_inflight). |
||||
- **Таймеры:** `retrans_timer`, `ack_resp_timer`. |
||||
- **Callback-цепочки:** `ready_cbks`, `up_cbks`, `down_cbks`, `bgp_ready_cbk`. |
||||
- **Счётчики:** ACK hit/miss, дубликаты, ретрансмиссии, `debug[8]` для live watch. |
||||
- **Метрики качества:** `metrics` (гистограммы RTT/потерь с 10-мин снимками, тотальные счётчики). |
||||
- **Идентификация:** `log_name` (формат `XXXX→YYYY [name]`), `name` (из конфига), `session_id`. |
||||
|
||||
#### `struct INFLIGHT_PACKET` (`etcp.h:108`) |
||||
|
||||
Пакет в состоянии inflight (отправлен или ждёт отправки). Поля: |
||||
- `seq` — sequence number (ID по протоколу). |
||||
- `state` — `WAIT_SEND` (в input_send_q) или `WAIT_ACK` (в input_wait_ack). |
||||
- `last_link` — последний линк, через который отправлен (для retrans/loss detection). |
||||
- `last_timestamp` — время последней отправки (для retrans timeout). |
||||
- `send_count` — количество попыток отправки. |
||||
- `retrans_req_count` — количество запросов ретрансмиссии. |
||||
- `send_hist[8]` — номера линков через которые передавался (кольцевой буфер, send_count — head). |
||||
- `delivered_at_send`, `inflight_at_send`, `is_app_limited` — снэпшоты для BBR rate estimation. |
||||
|
||||
#### `struct ETCP_FRAGMENT` (`etcp.h:123`) |
||||
|
||||
Фрагмент данных в очередях ввода/вывода. Поля: |
||||
- `seq` — sequence number. |
||||
- `timestamp` — timestamp пакета (от удалённой стороны при приёме). |
||||
- `ll` — `ll_entry` с dgram (указатель на data_pool) и len. |
||||
|
||||
#### `struct ACK_PACKET` (`etcp.h:129`) |
||||
|
||||
Неотправленное подтверждение приёма. Поля: |
||||
- `seq` — sequence number подтверждаемого пакета. |
||||
- `pkt_timestamp` — timestamp подтверждаемого пакета (часы удалённой стороны). |
||||
- `recv_timestamp` — локальное время приёма (для вычисления задержки ACK). |
||||
|
||||
#### `struct etcp_metrics` (`etcp.h:70`) |
||||
|
||||
Гистограммы и счётчики качества соединения: |
||||
- **Рабочие гистограммы:** `rtt_hist[12]`, `loss_hist[11]` — обновляются в реальном времени. |
||||
- **Финальные снимки:** `rtt_hist_final[12]`, `loss_hist_final[11]` — копируются раз в 10 мин. |
||||
- **Счётчики окна:** `work_samples/sent/rcvd/bytes_sent/bytes_rcvd/lost` — сбрасываются при снимке. |
||||
- **Тотальные:** `total_sent/rcvd/bytes_sent/bytes_rcvd/lost` — кумулятивные счётчики. |
||||
- **RTT-бакеты:** 0-1, 1-2, 2-3, 3-5, 5-8, 8-13, 13-21, 21-34, 34-55, 55-90, 90-150, 150+ ms (в 0.1ms). |
||||
- **Loss-бакеты:** 0%, 1%, ..., 10%+. |
||||
- Интервал снимка: 10 мин. Минимум пакетов для обновления: 200. |
||||
|
||||
### 3.2. Основные функции |
||||
|
||||
#### Жизненный цикл |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `etcp_connection_create(instance, name)` | Создаёт ETCP_CONN, очереди, пулы, нормалайзер, помещает в `instance->connections` с key=0 (pending). | |
||||
| `etcp_connection_close(etcp)` | Phase 1: detach (линки, таймеры, роутинг). Phase 2: deferred free когда ref_count=0. | |
||||
| `etcp_conn_ref_take(conn)` | Увеличивает ref_count. Возвращает -1 если conn удалён. | |
||||
| `etcp_conn_ref_free(conn)` | Уменьшает ref_count. Если 0 и state==2 — планирует Phase 2. | |
||||
| `etcp_conn_reset(etcp)` | Сброс состояния после reinit: seq, метрики, очистка очередей, возврат данных в нормалайзер. | |
||||
| `etcp_conn_reinit(etcp)` | Полный reinit: сброс флагов, сброс NAT, сброс роутера, вызов `etcp_conn_reset`. | |
||||
| `etcp_links_reset(etcp)` | Сброс флага initialized у всех линков (после сбоя). | |
||||
| `etcp_conn_ready(conn)` | Вызывается когда первый линк проинициализирован. Устанавливает `initialized=1`, `reset_done=1`, запускает `conn_queue_set_ready`. | |
||||
| `etcp_conn_queue_set_ready(conn)` | Переиндексирует соединение в `instance->connections` с реальным `peer_node_id`, state=1, запускает ready/up callback'и, стартует таймер метрик. | |
||||
| `etcp_conn_set_peer_node_id(conn, id)` | Устанавливает peer_node_id. Если state=1 — переиндексирует в очереди. | |
||||
| `etcp_conn_on_inflight_lim_changed(etcp)` | Пересчитывает `optimal_inflight` как сумму `inflight_lim_bytes` всех линков. | |
||||
| `etcp_update_mtu(etcp)` | Пересчитывает MTU соединения как минимум MTU всех линков. Обновляет `frag_size` нормалайзера. | |
||||
|
||||
#### Отправка |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `etcp_int_send(etcp, data, len)` | Отправка данных через ETCP: аллокация из data_pool, создание ETCP_FRAGMENT, помещение в input_queue. | |
||||
| `etcp_request_pkt(etcp)` | Формирует ETCP_DGRAM для отправки: выбирает линк через load balancer, берёт INFLIGHT_PACKET из input_send_q, добавляет ACK-секцию, опциональные секции (MEAS_TS, MEAS_RESP, TIMESTAMP, FILLER), payload. Перемещает inflight в wait_ack. | |
||||
| `etcp_encrypt_send(dgram)` | (etcp_connections.c) Шифрует ETCP_DGRAM (AES-CCM), добавляет nonce/tag/crc32/pubkey, отправляет через udp_send. | |
||||
|
||||
#### Приём |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `etcp_conn_input(pkt)` | Обработка расшифрованного пакета: разбор секций (ACK, TIMESTAMP, PAYLOAD, MEAS_TS, MEAS_RESP, FILLER, METRICS), вызов `etcp_ack_recv`, добавление данных в `recv_q`/`ack_q`. | |
||||
| `etcp_ack_recv(etcp, seq, ts, dts)` | Обработка ACK: поиск пакета в `input_wait_ack`/`input_send_q`, вычитание inflight с линка, BBR rate estimation, освобождение памяти. | |
||||
| `etcp_output_try_assembly(etcp)` | Сборка выходной очереди: ищет непрерывную последовательность от `last_delivered_id+1` в `recv_q`, переносит в `output_queue`. | |
||||
| `etcp_stats(etcp)` | Вывод статистики в debug-лог: размеры очередей, RTT, счётчики, ID. | |
||||
|
||||
#### Callback'и / События |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `etcp_on_link_down(etcp)` | Вызывается при падении линка. Проверяет все линки, если все down — вызывает `etcp_on_down`. | |
||||
| `etcp_on_up(etcp)` | Запуск цепочки `up_cbks`. | |
||||
| `etcp_on_down(etcp)` | Запуск цепочки `down_cbks`. | |
||||
|
||||
#### Метрики качества |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `etcp_metrics_init(m)` | Обнуление структуры метрик. | |
||||
| `etcp_metrics_add_rtt(etcp, rtt_tb)` | Добавление RTT-замера в гистограмму. | |
||||
| `etcp_metrics_add_sent(etcp, len)` | Учёт отправленного пакета (working + total). | |
||||
| `etcp_metrics_add_rcvd(etcp, len)` | Учёт принятого пакета. | |
||||
| `etcp_metrics_add_loss(etcp, count)` | Учёт потерь. | |
||||
| `etcp_metrics_start_timer(etcp)` | Запуск 10-минутного таймера снимков. | |
||||
| `etcp_metrics_stop_timer(etcp)` | Остановка таймера метрик. | |
||||
|
||||
### 3.3. Константы |
||||
|
||||
| Константа | Значение | Описание | |
||||
|-----------|----------|----------| |
||||
| `MAX_INFLIGHT_BYTES` | 65536 | Начальное окно | |
||||
| `RETRANS_K1` | 32 | Множитель RTT для retrans timeout | |
||||
| `RETRANS_K2` | 32 | Множитель jitter для retrans timeout | |
||||
| `ACK_DELAY_TB` | 20 | Задержка отправки ACK (2ms) | |
||||
| `RTT_HISTORY_SIZE` | 10 | Размер истории RTT для jitter | |
||||
| `INFLIGHT_INITIAL_HASH_SIZE` | 1024 | Начальный размер хеш-таблиц | |
||||
| `MAX_INFLIGHT_SIZE` | 16384 | Максимум элементов в recv_q (защита от атак) | |
||||
| `ASM_BUF_MAX_SIZE` | 65536 | Максимальный размер буфера сборки | |
||||
| `BURST_PACKET_COUNT` | 12 | Пакетов в burst | |
||||
| `BURST_SKIP_COUNT` | 3 | Пропускаемых пакетов при замере gap | |
||||
| `MIN_BURST_INTERVAL_TB` | 5000 | Мин. интервал между burst (500ms) | |
||||
| `BURST_RESP_TIMEOUT_TB` | 20000 | Таймаут ответа на burst (2s) | |
||||
| `ETCP_METRICS_RTT_BUCKETS` | 12 | Число RTT-бакетов в гистограмме | |
||||
| `ETCP_METRICS_LOSS_BUCKETS` | 11 | Число loss-бакетов | |
||||
| `ETCP_METRICS_INTERVAL_TB` | 6000000 | Интервал снимков (10 мин) | |
||||
| `ETCP_METRICS_MIN_PACKETS` | 200 | Мин. пакетов для снимка | |
||||
|
||||
### 3.4. Секции пакета |
||||
|
||||
| Секция | Type | Размер | Описание | |
||||
|--------|------|--------|----------| |
||||
| `PAYLOAD` | 0x00 | 5 + data | Данные: type(1) + seq(4) + payload | |
||||
| `ACK` | 0x01 | 8 + N*8 | ACK: type(1) + count(1) + last_delivered_id(4) + rx_dup_count(2) + N×{seq(4) + recv_ts(2) + delay(2)} | |
||||
| `TIMESTAMP` | 0x06 | 5 | Channel timestamp: type(1) + ret_ts(2) + dt(2) | |
||||
| `MEAS_TS` | 0x07 | 9 | Burst measurement: type(1) + burst_id(2) + flags(1) + seq(1) + ts_us(2) + pkt_sz(2) | |
||||
| `MEAS_RESP` | 0x08 | 13 | Burst response: type(1) + burst_id(2) + valid(1) + gap_avg(4) + gap_min(4) + pkt_count(1) | |
||||
| `FILLER` | 0x09 | 3 + N | Filler padding: type(1) + len(2) + zeros(N) | |
||||
| `METRICS` | 0x0A | 2 + 64 + N | Metrics CSV: type(1) + csv_len(2) + ed25519_sig(64) + csv_data(N) | |
||||
|
||||
### 3.5. Зависимости |
||||
|
||||
| Модуль | Как используется | |
||||
|--------|-----------------| |
||||
| `etcp_connections.h` | ETCP_LINK, ETCP_DGRAM, ETCP_SOCKET, etcp_link_new/close, etcp_encrypt_send | |
||||
| `etcp_loadbalancer.h` | etcp_loadbalancer_select_link, etcp_loadbalancer_send | |
||||
| `etcp_router.h` | etcp_router_transit_queues_destroy, etcp_router_pause_retrans_for_node | |
||||
| `etcp_debug.h` | etcp_dump_pkt_sections | |
||||
| `etcp_connect.h` | etcp_connect_cancel_for_conn | |
||||
| `pkt_normalizer.h` | PKTNORM (pn_init, pn_deinit, pn_reset) | |
||||
| `secure_channel.h` | sc_context_t для шифрования | |
||||
| `routing.h` | routing_del_conn | |
||||
| `topo_group.h` | route_ping_cancel_for_conn | |
||||
| `../lib/ll_queue.h` | Все очереди | |
||||
| `../lib/u_async.h` | uasync_set_timeout, uasync_call_soon | |
||||
| `../lib/memory_pool.h` | memory_pool_alloc/free/init/destroy | |
||||
| `../lib/mem.h` | u_malloc, u_calloc, u_realloc, u_free, u_strdup | |
||||
| `../lib/debug_config.h` | DEBUG_ERROR/WARN/INFO/DEBUG/TRACE | |
||||
@ -1,51 +0,0 @@
|
||||
# ETCP Load Balancer (etcp_loadbalancer) |
||||
|
||||
## 1. Назначение |
||||
Балансировка исходящего трафика ETCP между несколькими линками (UDP-сокетами) внутри одного ETCP_CONN. Выбирает оптимальный линк по минимальному `inflight_bytes`, при равенстве — round-robin. Для каждого линка работает traffic shaper — ограничение полосы пропускания (bandwidth pacing) с таймерной задержкой и burst-режимом. |
||||
|
||||
## 2. Как пользоваться |
||||
Типовой сценарий: перед отправкой пакета вызывается `etcp_loadbalancer_send(dgram)`. Функция сама выбирает линк (если он ещё не задан), отправляет и обновляет shaper-нагрузку линка. |
||||
|
||||
```c |
||||
// Отправка пакета через loadbalancer |
||||
struct ETCP_DGRAM* dgram = memory_pool_alloc(inst->pkt_pool); |
||||
// ... заполнение dgram ... |
||||
etcp_loadbalancer_send(dgram); // сам выберет линк, зашифрует, отправит, освободит dgram |
||||
``` |
||||
|
||||
Если все линки заняты (shaper-таймер или inflight превышает лимит), пакет дропается. При освобождении линка shaper-таймер вызывает `loadbalancer_link_ready()`, который нотифицирует `ETCP_CONN->link_ready_for_send_fn`. |
||||
|
||||
```c |
||||
// Проверка статуса связи |
||||
if (etcp_loadbalancer_get_link_status(etcp)) { |
||||
// есть хотя бы один живой линк |
||||
} |
||||
``` |
||||
|
||||
**Нюансы:** |
||||
- Алгоритм выбора: min `inflight_bytes`, среди равных — round-robin (поле `last_rr_link` в ETCP_CONN) |
||||
- Shaper работает через `shaper_load_time_tb` / `shaper_sub_nanotime` — виртуальное время передачи, сравнивается с `now_tb + SHAPER_BURST_DELAY_TB` (1ms) |
||||
- Burst-пакеты (`link->burst_active`) обходят shaper |
||||
- При безлимитном bandwidth (`link->bandwidth == 0`) shaper пропускается |
||||
- `etcp_loadbalancer_send()` освобождает dgram через `memory_pool_free()` — после вызова dgram недействителен |
||||
|
||||
## 3. API |
||||
|
||||
### Структуры |
||||
Поля ETCP_LINK, используемые балансировщиком: |
||||
- `initialized` — линк готов к работе |
||||
- `link_status` — 1 = линк жив |
||||
- `inflight_bytes` — байт в полёте (минимизируемый критерий) |
||||
- `burst_active` — 1 = burst bypass всех ограничений |
||||
- `shaper_timer` — активный таймер задержки shaper'а |
||||
- `bandwidth` — Kbps, 0 = безлимит |
||||
- `shaper_load_time_tb`, `shaper_sub_nanotime` — аккумулированное время передачи (0.1ms timebase) |
||||
|
||||
### Функции |
||||
| Функция | Описание | |
||||
|---|---| |
||||
| `etcp_loadbalancer_select_link(etcp)` | Выбрать линк по min inflight_bytes с round-robin. NULL если все заняты | |
||||
| `etcp_loadbalancer_send(dgram)` | Выбрать линк → зашифровать/отправить → обновить shaper → освободить dgram | |
||||
| `loadbalancer_link_can_send(link)` | 1 если линк не заблокирован shaper'ом и burst не активен | |
||||
| `loadbalancer_link_ready(link)` | Уведомить ETCP_CONN о готовности линка (вызывает `link_ready_for_send_fn`) | |
||||
| `etcp_loadbalancer_get_link_status(etcp)` | 1 = есть живой линк, 0 = все недоступны | |
||||
@ -1,32 +0,0 @@
|
||||
сделай тест проверки отправкив в udp (etcp_loadbalancer) |
||||
|
||||
|
||||
1. инициализируешь etcp без линков, и один сокет для работы |
||||
2. вписываешь в link_ready_for_send_fn свою функцию |
||||
3. в тесте открываешь ответный сокет |
||||
4. добавляешь линк к etcp |
||||
5. |
||||
|
||||
|
||||
|
||||
|
||||
link initialization: |
||||
|
||||
client: |
||||
|
||||
1. init timer: |
||||
- шлём init with reset |
||||
|
||||
Получили response: |
||||
- отменяем init, переходим в обычный режим + keepalive |
||||
|
||||
link не отвечает: |
||||
- шлём init without reset |
||||
|
||||
server: |
||||
- просто ждёт init request. |
||||
как получил init, делает линк инициализированным. |
||||
|
||||
общая логика keepalive (client+server): |
||||
далее на init линках шлём keepalive со статусом remote keepalive. |
||||
если remote keepalive =0 - значит на другой стороне всё плохо и не шлём данные (только keepalive) |
||||
@ -1,52 +0,0 @@
|
||||
# STCP (Secure TCP) — общая часть соединения |
||||
|
||||
## 1. Назначение |
||||
|
||||
STCP — потоковый TCP-протокол с X25519-ключеобменом и потоковым AES-CTR шифрованием (stream cipher). Обеспечивает безопасное TCP-соединение: клиент подключается, сервер принимает, выполняется ECDH-рукопожатие с обфускацией публичных ключей, после чего трафик шифруется AES-CTR с контрольными суммами CRC32. |
||||
|
||||
Модуль `stcp.c/stcp.h` содержит общие структуры, константы и базовый жизненный цикл соединения `struct stcp_conn`, используемый как клиентской (`stcp_client`), так и серверной (`stcp_server`) сторонами. |
||||
|
||||
## 2. Как пользоваться |
||||
|
||||
Типовая схема: вышестоящий код создаёт `struct stcp_conn` (сервер через `stcp_server_create`, клиент — через `stcp_client_connect`), устанавливает очереди приёма/передачи (`stcp_conn_set_tx_queue`, `stcp_conn_set_rx_queue`), коллбэк закрытия (`stcp_conn_set_on_close`). После рукопожатия (`STCP_STATE_DATA`) сообщения передаются через `rx_queue`/`tx_queue`: отправка — через ll_queue с коллбэком `tx_cb`, приём — данные раскладываются в `rx_queue`. |
||||
|
||||
Потоковое шифрование (`sc_stream_state`) не требует буферизации целых сообщений — XOR применяется побайтово к потоку, поэтому порядок отправки/приёма критичен. Для каждого направления создаётся отдельный stream (`stream_send`, `stream_recv`). |
||||
|
||||
Ключевые нюансы: |
||||
- Размер сообщения — `uint16_t` в префиксе, макс. 65535 байт. |
||||
- Каждое сообщение шифруется: 2 байта длины + данные + 4 байта CRC32. |
||||
- CRC32 проверяется после расшифровки для детектирования повреждений. |
||||
- При разрыве соединения или ошибке вызывается `on_close`. |
||||
|
||||
## 3. API |
||||
|
||||
### Состояния |
||||
- `STCP_STATE_INIT` — начальное (на клиенте) |
||||
- `STCP_STATE_HS_SERVER_WAIT` — сервер ждёт рукопожатия от клиента |
||||
- `STCP_STATE_DATA` — активный обмен данными (шифрованный) |
||||
- `STCP_STATE_CLOSED` / `STCP_STATE_ERROR` — завершение |
||||
|
||||
### stcp_conn — структура соединения |
||||
| Поле | Описание | |
||||
|------|----------| |
||||
| `sock` | TCP-сокет | |
||||
| `ua` | UASYNC event loop | |
||||
| `state` | Текущее состояние (enum stcp_state) | |
||||
| `is_server` | 1 = серверная сторона | |
||||
| `session_key[SC_SESSION_KEY_SIZE]` | Ключ X25519 ECDH (AES-128) | |
||||
| `stream_send` / `stream_recv` | Состояния потокового AES-CTR для отправки и приёма | |
||||
| `my_keys` | Локальные ключи X25519 | |
||||
| `peer_pubkey` | Публичный ключ пира (после рукопожатия) | |
||||
| `rx_queue` / `tx_queue` | ll_queue для приёма/отправки сообщений | |
||||
| `tx_cb` | Коллбэк очереди tx_queue | |
||||
| `recv_buf` | Буфер приёма TCP (динамический, до 128KB) | |
||||
| `send_buf` | Буфер отправки (для досыла при EAGAIN) | |
||||
| `hs_expected_len` / `hs_key_processed` | Состояние рукопожатия | |
||||
| `on_ready` | Вызывается после завершения рукопожатия | |
||||
| `on_close` | Вызывается при закрытии соединения | |
||||
|
||||
### Функции |
||||
- `stcp_conn_set_tx_queue(c, q)` — установить очередь отправки, привязывает `tx_cb` как коллбэк |
||||
- `stcp_conn_set_rx_queue(c, q)` — установить очередь приёма (в неё кладутся расшифрованные сообщения) |
||||
- `stcp_conn_set_on_close(c, cb, arg)` — установить коллбэк закрытия (err=0 — норма, иначе код ошибки) |
||||
- `stcp_conn_free(c)` — освободить сокет, буферы, очистить stream-ы. Если `allocated=1` — освободить и саму структуру |
||||
@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3 |
||||
""" |
||||
chat_invite_test.py — headless test of the inviter-side invite flow. |
||||
|
||||
Сценарий: A (owner) говорит B (серверу) «добавь себе мою группу» |
||||
(CS_MSG_CHANNEL_INVITE), B авто-принимает (join_policy=0) и join+member_sync |
||||
завершается. Проверяем, что канал и мемберы появились на обеих сторонах. |
||||
|
||||
Usage: |
||||
python3 tools/chat_invite_test.py |
||||
""" |
||||
|
||||
import asyncio |
||||
import json |
||||
import os |
||||
import socket |
||||
import sys |
||||
import tempfile |
||||
import time |
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__)) |
||||
from chat_client import ChatClient, ChatClientError |
||||
|
||||
UTUN_BIN = os.path.join(os.path.dirname(__file__), "..", "src", "utun") |
||||
READY_TIMEOUT = 3.0 # max wait for utun ready |
||||
SYNC_TIMEOUT = 10.0 # max wait for join + sync |
||||
REQUEST_TIMEOUT = 5.0 # per-request timeout |
||||
TOTAL_TIMEOUT = 30.0 # overall test timeout |
||||
|
||||
|
||||
def find_free_port(): |
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
||||
s.bind(("127.0.0.1", 0)) |
||||
port = s.getsockname()[1] |
||||
s.close() |
||||
return port |
||||
|
||||
|
||||
def write_config(path, etcp_port, ctrl_port, db_subdir, join_policy=None): |
||||
chat_section = "" |
||||
if join_policy is not None: |
||||
chat_section = f"\n[chat]\njoin_policy={join_policy}\n" |
||||
content = f"""[global] |
||||
db_path={db_subdir} |
||||
|
||||
[server: srv] |
||||
addr=127.0.0.1:{etcp_port} |
||||
type=public |
||||
|
||||
[chatserver] |
||||
db_path={db_subdir} |
||||
headless_control_bind=127.0.0.1:{ctrl_port} |
||||
{chat_section} |
||||
[allowed_keys] |
||||
allow_all=1 |
||||
""" |
||||
with open(path, "w") as f: |
||||
f.write(content) |
||||
|
||||
|
||||
def read_config_key(path, key): |
||||
try: |
||||
with open(path, "r") as f: |
||||
for line in f: |
||||
line = line.strip() |
||||
if line.startswith(key + "="): |
||||
return line[len(key) + 1:] |
||||
except OSError: |
||||
return None |
||||
return None |
||||
|
||||
|
||||
async def wait_config_key(path, key, timeout=READY_TIMEOUT): |
||||
deadline = time.monotonic() + timeout |
||||
while time.monotonic() < deadline: |
||||
val = read_config_key(path, key) |
||||
if val: |
||||
return val |
||||
await asyncio.sleep(0.1) |
||||
return None |
||||
|
||||
|
||||
async def kill_proc(proc, label): |
||||
if proc is None or proc.returncode is not None: |
||||
return |
||||
try: |
||||
proc.terminate() |
||||
try: |
||||
await asyncio.wait_for(proc.wait(), timeout=3.0) |
||||
except asyncio.TimeoutError: |
||||
proc.kill() |
||||
await proc.wait() |
||||
except ProcessLookupError: |
||||
pass |
||||
|
||||
|
||||
async def wait_ready(cli, timeout=READY_TIMEOUT): |
||||
deadline = time.monotonic() + timeout |
||||
while time.monotonic() < deadline: |
||||
try: |
||||
await cli.ping() |
||||
return True |
||||
except ChatClientError: |
||||
await asyncio.sleep(0.1) |
||||
return False |
||||
|
||||
|
||||
def check(name, expr, detail=""): |
||||
if not expr: |
||||
detail = f" ({detail})" if detail else "" |
||||
raise AssertionError(f"FAIL: {name}{detail}") |
||||
print(f" OK: {name}") |
||||
|
||||
|
||||
async def main(): |
||||
etcp_a = find_free_port() |
||||
etcp_b = find_free_port() |
||||
ctrl_a = find_free_port() |
||||
ctrl_b = find_free_port() |
||||
|
||||
print(f"ports: etcp={etcp_a},{etcp_b} ctrl={ctrl_a},{ctrl_b}") |
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="utun_invite_test_") |
||||
db_a = os.path.join(tmpdir, "db_a") |
||||
db_b = os.path.join(tmpdir, "db_b") |
||||
os.makedirs(db_a, exist_ok=True) |
||||
os.makedirs(db_b, exist_ok=True) |
||||
|
||||
config_a = os.path.join(tmpdir, "a.conf") |
||||
config_b = os.path.join(tmpdir, "b.conf") |
||||
write_config(config_a, etcp_a, ctrl_a, db_a) # A: owner |
||||
write_config(config_b, etcp_b, ctrl_b, db_b, join_policy=0) # B: server, autojoin |
||||
|
||||
proc_a = None |
||||
proc_b = None |
||||
|
||||
try: |
||||
print("\n--- Starting utun ---") |
||||
log_a = os.path.join(tmpdir, "utun_a.log") |
||||
log_b = os.path.join(tmpdir, "utun_b.log") |
||||
pid_a = os.path.join(tmpdir, "utun_a.pid") |
||||
pid_b = os.path.join(tmpdir, "utun_b.pid") |
||||
|
||||
proc_a = await asyncio.create_subprocess_exec( |
||||
UTUN_BIN, "-f", "-p", pid_a, "-l", log_a, "-c", config_a, |
||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, |
||||
) |
||||
proc_b = await asyncio.create_subprocess_exec( |
||||
UTUN_BIN, "-f", "-p", pid_b, "-l", log_b, "-c", config_b, |
||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, |
||||
) |
||||
|
||||
print(f" proc_a pid={proc_a.pid} proc_b pid={proc_b.pid}") |
||||
await asyncio.sleep(0.5) |
||||
|
||||
print("\n--- Connecting to headless control ---") |
||||
async with ChatClient(port=ctrl_a, timeout=REQUEST_TIMEOUT) as cli_a: |
||||
if not await wait_ready(cli_a): |
||||
raise RuntimeError(f"node A not ready after {READY_TIMEOUT}s") |
||||
print(f" node A ready") |
||||
|
||||
async with ChatClient(port=ctrl_b, timeout=REQUEST_TIMEOUT) as cli_b: |
||||
if not await wait_ready(cli_b): |
||||
raise RuntimeError(f"node B not ready after {READY_TIMEOUT}s") |
||||
print(f" node B ready") |
||||
|
||||
# ── Create channel on A ── |
||||
print("\n--- Create channel ---") |
||||
await cli_a.create_channel("TestGroup") |
||||
channels = await cli_a.channels() |
||||
check("channel created", isinstance(channels, list) and len(channels) == 1, |
||||
f"channels={json.dumps(channels)}") |
||||
ch_id = str(channels[0]["id"]) |
||||
print(f" channel_id={ch_id} name={channels[0]['name']}") |
||||
|
||||
# ── Read B's node_id + pubkey from config (written back by utun) ── |
||||
print("\n--- Read B identity ---") |
||||
b_node_id = await wait_config_key(config_b, "my_node_id") |
||||
b_pubkey = await wait_config_key(config_b, "my_public_key") |
||||
if not b_node_id or not b_pubkey: |
||||
raise RuntimeError("failed to read B's node_id/pubkey from config") |
||||
b_node_id = "0x" + b_node_id |
||||
print(f" B node_id={b_node_id} pubkey={b_pubkey[:16]}...") |
||||
|
||||
# ── A invites B to add the channel (inviter side) ── |
||||
print("\n--- Invite B (inviter → CHANNEL_INVITE) ---") |
||||
resp = await cli_a.invite_to(ch_id, b_node_id, b_pubkey, f"127.0.0.1:{etcp_b}", proto=1) |
||||
check("invite_to accepted", isinstance(resp, dict) and resp.get("inviting"), |
||||
f"resp={json.dumps(resp)}") |
||||
|
||||
# ── Wait for B to join + member_sync ── |
||||
print(f"\n--- Wait join + sync (max {SYNC_TIMEOUT}s) ---") |
||||
deadline = time.monotonic() + SYNC_TIMEOUT |
||||
member_count_b = 0 |
||||
members_b = [] |
||||
while time.monotonic() < deadline: |
||||
try: |
||||
members_b = await cli_b.members(ch_id) |
||||
member_count_b = len(members_b) if isinstance(members_b, list) else 0 |
||||
if member_count_b >= 2: |
||||
break |
||||
except ChatClientError: |
||||
pass |
||||
await asyncio.sleep(0.15) |
||||
|
||||
print(f"\n--- Members on B ({member_count_b}) ---") |
||||
for m in (members_b if isinstance(members_b, list) else []): |
||||
print(f" {m['node_id']} name={m.get('name','?')} online={m.get('online')} connected={m.get('connected')}") |
||||
check("2 members on B (B joined A)", member_count_b >= 2, |
||||
f"got {member_count_b} after {SYNC_TIMEOUT}s") |
||||
|
||||
await asyncio.sleep(0.5) |
||||
|
||||
members_a = await cli_a.members(ch_id) |
||||
print(f"\n--- Members on A ({len(members_a) if isinstance(members_a, list) else '?'}) ---") |
||||
for m in members_a: |
||||
print(f" {m['node_id']} name={m.get('name','?')} online={m.get('online')} connected={m.get('connected')}") |
||||
check("2 members on A (B visible to A)", isinstance(members_a, list) and len(members_a) >= 2, |
||||
f"got {len(members_a) if isinstance(members_a, list) else '?'}") |
||||
|
||||
channels_b = await cli_b.channels() |
||||
check("channel present on B", isinstance(channels_b, list) and len(channels_b) >= 1, |
||||
f"channels_b={json.dumps(channels_b)}") |
||||
|
||||
print("\n=== TEST PASSED ===") |
||||
return 0 |
||||
|
||||
except Exception as e: |
||||
print(f"\n=== TEST FAILED: {e} ===", file=sys.stderr) |
||||
import traceback |
||||
traceback.print_exc() |
||||
print(f"\n--- logs ---", file=sys.stderr) |
||||
for lp in ["utun_a.log", "utun_b.log"]: |
||||
lp = os.path.join(tmpdir, lp) |
||||
if os.path.exists(lp): |
||||
print(f"\n===== {lp} (tail) =====", file=sys.stderr) |
||||
try: |
||||
with open(lp, "r") as f: |
||||
lines = f.readlines() |
||||
for ln in lines[-80:]: |
||||
sys.stderr.write(ln) |
||||
except OSError: |
||||
pass |
||||
return 1 |
||||
|
||||
finally: |
||||
print("\n--- Cleanup ---") |
||||
await kill_proc(proc_a, "proc_a") |
||||
await kill_proc(proc_b, "proc_b") |
||||
|
||||
for f in [config_a, config_b]: |
||||
try: os.unlink(f) |
||||
except OSError: pass |
||||
try: os.rmdir(db_a) |
||||
except OSError: pass |
||||
try: os.rmdir(db_b) |
||||
except OSError: pass |
||||
try: os.rmdir(tmpdir) |
||||
except OSError: pass |
||||
print(f" temp dir cleaned: {tmpdir}") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
async def _run(): |
||||
try: |
||||
return await asyncio.wait_for(main(), timeout=TOTAL_TIMEOUT) |
||||
except asyncio.TimeoutError: |
||||
print(f"\n=== TEST FAILED: total timeout {TOTAL_TIMEOUT}s ===", file=sys.stderr) |
||||
return 1 |
||||
sys.exit(asyncio.run(_run())) |
||||
Loading…
Reference in new issue