conn_mgr: race connection probes and report call path with correct RTT

This commit is contained in:
evgeny committed 2026-10-01 00:42:47 +03:00
1 parent 1c51df039f
commit 111432aa64
26 files changed
+1112 -995

No files matched your search

+14 -4
View File
@@ -8,9 +8,9 @@
* - не является NCD handle и не привязан временем жизни к одному ETCP_CONN.
*
* NCD обеспечивает общий прямой мультиадресный транспорт для BGP/topo_group.
* NCD handle попытки CM после установления связи должен перейти группе либо быть закрыт;
* CM удерживает свой NCD handle для позднего UP и мониторинга прямого пути;
* группа приобретает независимое NCD-владение при согласовании своей сессии.
* CM handle остаётся у медиа-сервиса. Закрытие CM handle отменяет его запрос, а не владение группы.
* Это контракт разделения ролей; механизм передачи транспорта группе относится к NCD/topo_group.
* Групповые сессии открывают через topo_group_peer_open(). Все операции — в потоке uasync. */
#ifndef CONN_MGR_H
#define CONN_MGR_H
@@ -58,7 +58,8 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
/* ═══════ единая точка входа ═══════ */
/**
* Единственная публичная функция подключения — 3-фазное (DIRECT→REVERSE→INDIRECT).
* Параллельные пробы: DIRECT сразу, REVERSE через 0/500мс, INDIRECT через 0/1000мс от общего старта.
* Посредник используется сразу после проверки; поздний прямой путь заменяет его без повторного UP.
* Возврат 0 — запрос принят, -1 — ошибка запуска. cb обязателен.
* Ошибка до запуска может возвращаться без callback. В callback h может быть NULL.
* Успешно выданный handle закрывает вызывающий, в том числе после TIMEOUT.
@@ -77,7 +78,7 @@ void conn_mgr_close(struct CONN_MGR_HANDLE* h);
/* ═══════ доступ к соединению ═══════ */
/** Заимствованный ETCP_CONN от NCD, если он есть; NULL для INDIRECT или отсоединённого handle. */
/** Заимствованный READY прямой ETCP_CONN; NULL для INDIRECT, ожидания или отсоединённого handle. */
struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h);
/** Передать e->dgram через маршрутизатор CM. Освобождает entry при ненулевом h;
@@ -89,6 +90,15 @@ int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e);
/** Обновить RTT кандидата-посредника (≤3, сортировка по возрастанию). */
void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uint16_t rtt);
/** Запланировать перепроверку путей после изменения маршрутов или READY групповых сессий. */
void conn_mgr_routes_changed(struct CONN_MGR* mgr);
/** Заимствованный next hop: READY прямой путь/подтверждённый посредник; allow_probe — только для управляющего обмена CM. */
struct ETCP_CONN* conn_mgr_route_conn(struct CONN_MGR* mgr, uint64_t node_id, int allow_probe);
/** Тип живого транспорта к node_id: исходящий DIRECT, только входящие REVERSE, другой next hop INDIRECT. */
uint8_t conn_mgr_connection_type(const struct ETCP_CONN* conn, uint64_t node_id);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large. Load diff
+93 -194
View File
@@ -1,215 +1,114 @@
# conn_mgr — Менеджер установки соединений
# Connection Manager
## 1. Назначение
Трёхфазный асинхронный менеджер подключения к удалённым нодам. Отвечает за установку ETCP-соединения с выбором оптимального способа:
`conn_mgr` подбирает путь для медиа, файлов и звонков. Один менеджер принадлежит
одной группе. Несколько сервисов могут удерживать свои handles к одному узлу:
транспорт и пробы общие, callbacks и отмена принадлежат каждому владельцу.
Все операции выполняются в uasync-потоке.
1. **DIRECT** — прямое INIT-рукопожатие со всеми известными адресами цели (IPv4/IPv6, TCP/UDP, с NAT-фильтрацией через `sock_match`)
2. **REVERSE** — если у нас прямой IP, а цель за NAT: отправляем наши адреса через BGP (`DIRECT_REQ`), цель подключается к нам
3. **INDIRECT** — обмениваемся через BGP списками кандидатов-посредников, выбираем общих по минимальной сумме RTT, трафик идёт через `etcp_router`
## Расписание подключения
Также выполняет фоновые задачи: периодический ping всех BGP-нод, поддержание списка лучших посредников, idle-таймаут для неактивных соединений, локальное сканирование сети (LAN broadcast ping).
Все сроки отсчитываются от одного `start_tb`, а не от таймаута предыдущей пробы.
Один `CONN_MGR` обслуживает одну `TOPO_GROUP` (создаётся в `conn_mgr_init(group)`). Внутри — кеш `entries` (по одной записи на целевой узел) и список `handle`'ов (по одному на каждый вызов `conn_mgr_open`).
| Доступность извне | DIRECT | LOCAL | REVERSE | INDIRECT |
|---|---|---|---|---|
| Мы доступны, пир нет | Сразу | В составе DIRECT | Сразу | 1000 мс |
| Оба доступны | Сразу | В составе DIRECT | 500 мс | 1000 мс |
| Мы недоступны, пир доступен | Сразу | В составе DIRECT | 500 мс | 1000 мс |
| Оба недоступны | Сразу | Сразу | 500 мс | Сразу |
## 2. Как пользоваться
Доступность определяется по опубликованным адресам и общей классификации
`sock_match`: публичный IPv4, EIM/прямой NAT, пригодный глобальный IPv6.
Приватные адреса EIM-интерфейса не считаются внешними.
### Инициализация
```c
struct CONN_MGR* mgr = conn_mgr_init(group);
// Регистрируется в etcp_router через conn_mgr_router_recv_handler (ETCP_RT_ID_CONN_MGR)
// Запускает фоновые таймеры: bg_ping, candidate_ping
```
DIRECT использует NCD, который создаёт UDP/TCP-линки по известной записи узла,
включая IPv4/IPv6, LAN и Reality-параметры. LOCAL — те же локальные handshake-пробы:
отдельные ping-контексты и повторные линки не создаются.
REVERSE просит получателя открыть/дополнить NCD к отправителю по уже известному
подписанному снимку. Запрос обрабатывается и при CONNECTING, и при работе через
посредника. Повторы запроса идут раз в секунду в течение трёх секунд.
### Подключение (handle-based)
```c
static void my_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id,
enum conn_mgr_event event, void* arg) {
if (event == CONN_EVENT_UP) { /* связь есть */ }
else if (event == CONN_EVENT_DOWN) { /* временный обрыв, сам восстановится */ }
else if (event == CONN_EVENT_TIMEOUT) { /* не удалось, закройте handle */ }
}
Запуск нового способа не закрывает NCD и не удаляет предыдущие линки. NCD TIMEOUT
по умолчанию через две секунды завершает только ожидание первой прямой попытки;
удерживаемый handle продолжает получать поздний UP.
struct CONN_MGR_HANDLE* h = NULL;
conn_mgr_open(inst, group_id, node_id, my_cb, arg, &h);
```
## Рабочий путь
### Отправка данных
```c
struct ll_entry* e = queue_entry_new(0);
e->dgram = data; e->len = len;
conn_mgr_send(h, e); // забирает владение e, освобождает внутри
struct ETCP_CONN* conn = conn_mgr_get_conn(h); // NULL для INDIRECT
```
Состояние доставки (`state`, `conn_type`) отделено от состояния проб.
Посредник используется сразу после успешного согласования, пока прямой путь
ещё устанавливается. Прямой транспорт становится рабочим после READY сессии
нужной группы. Тогда он имеет приоритет независимо от порядка ответов INDIRECT.
### Закрытие
```c
conn_mgr_close(h); // убирает ОДИН handle; последний рвёт соединение
```
Переходы между посредником и прямым путём не сбрасывают состояние `ETCP_ROUTER_CONN`,
его очереди, seq и неподтверждённые пакеты. Список подтверждённых посредников
сохраняется как резерв. Для выбора посредника проверяются READY первого плеча
и наличие объявления маршрута к получателю именно от этого посредника.
### Кандидаты-посредники
`cm_candidate_ping_timer_cb` (раз в ~2с) пополняет `best_candidates` (топ-3 по RTT) из узлов группы с активным соединением и пингует/probe'ит их. Список используется фазой INDIRECT. Внешний код (`route_connectivity`) лишь заполняет `nq->connectivity`, откуда берутся свежие RTT.
DIRECT/REVERSE определяются по текущим живым линкам: наличие исходящего означает
DIRECT, только входящие — REVERSE. Метка первого UP не используется как постоянный тип.
Окно звонка показывает фактический путь router, включая BGP fallback до готовности CM:
DIRECT/REVERSE/INDIRECT/NONE. Событие `CHAT_EVT_CALL_CONNECTION` содержит call_id,
тип и next hop; публикуется при изменении. Проверка идёт раз в секунду общим таймером
звонка, сразу при начале/принятии и при событиях CM. Завершение сбрасывает статус
и отменяет таймер. Отображение одинаково в Qt и Android.
### Нюансы
- Несколько `conn_mgr_open` на один узел → несколько handle на одном entry; все получают события
- Повторный `open` для уже подключённой ноды вернёт handle сразу, UP доставляется отложенно (`uasync_call_soon`)
- Для `open` в состоянии CONNECTING новый handle добавляется в список ожидания
- Для INDIRECT-соединений `conn_mgr_get_conn` возвращает `NULL`, `conn_mgr_send` идёт через `etcp_router` (первый посредник с живым conn)
- Пока есть активный ETCP-линк к ноде — соединение считается DIRECT
Один timer на entry обслуживает ближайшее запланированное действие, повторы и
общий предел ожидания. READY и изменение маршрутов перепроверяют пути через
`conn_mgr_routes_changed()`, без периодического опроса работающих соединений.
Начальное ожидание ограничено 15 секундами от срока запуска INDIRECT, даже если
нет кандидатов/RTT/ответа. Получение ответа не отменяет этот предел.
Входящие записи без сервисных handles удаляются по этому сроку; групповые владельцы
сохраняют общий транспорт.
## 3. API
После готовности прямого пути новые дополнительные пробы не запускаются.
Уже начатое согласование INDIRECT может завершиться и сохранить резерв.
После начального бюджета поздние NCD/READY всё ещё могут улучшить путь. Бесконечные
повторные поиски посредников и гарантированное восстановление не предусмотрены.
### События (enum conn_mgr_event)
## INDIRECT
| Событие | Значение | Смысл |
|---------|----------|-------|
| `CONN_EVENT_UP` | 0 | Связь есть, можно отправлять данные |
| `CONN_EVENT_DOWN` | 1 | Временный обрыв, handle жив, само восстановится |
| `CONN_EVENT_TIMEOUT` | 2 | Подключение не удалось, handle жив, закройте сами |
1. Инициатор отправляет EXCHANGE_REQ со своими кандидатами.
2. Получатель отвечает EXCHANGE_RESP: свои кандидаты и RTT до кандидатов инициатора.
3. Инициатор объединяет кандидатов без дублей, исключает себя/получателя,
неизвестные RTT и узлы без прямой READY-сессии. Выбирает до трёх по сумме RTT
обоих плеч; при равных RTT — по node_id.
4. INTERM_SELECTED отправляется через выбранный next hop. Пока нет подтверждения,
сервис ещё не получает UP.
5. Получатель проверяет своё плечо и возвращает INTERM_ACK через выбранного
посредника. Инициатор принимает только узлы из собственного выбора.
### Коллбэк
```c
typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h,
uint64_t node_id, uint64_t group_id,
enum conn_mgr_event event, void* arg);
```
`h` может быть `NULL` (ошибки до создания handle). `node_id`/`group_id` всегда валидны.
Пустой список и RTT=0/UINT16_MAX не дают успеха. Реальный нулевой текущий RTT
после уже выполненного замера округляется до 1, чтобы отличать его от отсутствия
замера. При отсутствии пригодных кандидатов EXCHANGE повторяется раз в секунду
до общего дедлайна; новые кандидаты могут появиться благодаря фоновым проверкам.
Запрос работает и при одностороннем `conn_mgr_open()`.
Ответы привязаны одновременно к отправителю и request_id. Повторы идемпотентны;
поздний ответ не может вытеснить рабочий прямой путь.
### Корневые структуры
Wire-структуры описаны в `conn_mgr_priv.h`. DIRECT_REQ содержит только request_id.
INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{node_id, rtt}`.
Совместимость с прежним адресным DIRECT_REQ не поддерживается.
| Структура | Описание |
|-----------|----------|
| `CONN_MGR` | Корневая структура менеджера. `instance`, `group`, очередь `entries`, топ-3 `best_candidates`, таймеры (`bg_ping_timer`, `candidate_ping_timer`), `bg_ping_cursor`, списки `reverse_pending`/`exchange_pending`, `next_request_id` |
| `CONN_MGR_ENTRY` | Запись о подключении к одной ноде: `state`, `conn_type`, `last_traffic_tb`, список `handles`, `ncd_handle`, `intermediaries[3]`/`rr_idx`, `local_scan_state`/`main_connect_state`, `db_loaded`, `main.phase` |
| `CONN_MGR_HANDLE` | Handle для одного вызова `conn_mgr_open`: `node_id`, `group_id`, `cb`, `cb_arg`, ссылка на `entry`, `next`, `deliver_up_token` (отложенный UP) |
| `CONN_MGR_CANDIDATE` | Пара `{node_id, rtt}` — запись о кандидате-посреднике (packed) |
| `cm_reverse_pending` | Запись об ожидании REVERSE-подключения (связный список) |
| `cm_exchange_pending` | Запись об ожидании INDIRECT-обмена: кешированный `EXCHANGE_RESP`, флаги `received`/`probed`, таймер probe-повторов |
## API и владение
### Жизненный цикл
- `conn_mgr_open()` создаёт отдельный handle. Если попытка уже идёт, добавляет
ожидающего; если путь готов, доставляет UP отложенно.
Первый сервисный handle входящего пути запускает своё расписание проб,
сохраняя уже работающий путь через посредника.
- UP означает появление пригодного пути. Улучшение пути не повторяет UP.
- DOWN означает потерю рабочих путей; при живом резерве DOWN не доставляется.
- TIMEOUT означает, что начальное подключение не получило ни одного рабочего пути.
Ошибка отдельной пробы при живом посреднике не становится TIMEOUT для сервиса.
- DOWN/TIMEOUT не освобождают handle. Владелец обязан вызвать `conn_mgr_close()`.
- Закрытие последнего handle отменяет CM timer и callbacks и закрывает только
NCD-владение CM. Группа и другие NCD-владельцы сохраняют транспорт.
- `conn_mgr_get_conn()` возвращает заимствованный прямой транспорт либо NULL
при INDIRECT/отсоединённом handle. Данные следует отправлять через router.
| Функция | Описание |
|---------|----------|
| `conn_mgr_init(group)` | Выделяет CONN_MGR, создаёт очередь entries, запускает bg_ping и candidate_ping таймеры |
| `conn_mgr_destroy(mgr)` | Отменяет таймеры, освобождает все entries, exchange_pending, reverse_pending |
| `conn_mgr_router_recv_handler(conn, entry)` | Обработчик входящих пакетов `ETCP_RT_ID_CONN_MGR` из etcp_router; диспетчеризация по subcmd |
Диагностика CM — категория `general`: INFO показывает расписание, выбор и смену
пути с временем от старта; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR —
ошибки, недоступные пути и некорректные пакеты. Создание адресных линков NCD —
категория `connection`.
### Подключение / отключение
| Функция | Описание |
|---------|----------|
| `conn_mgr_open(inst, group_id, node_id, cb, cb_arg, &h)` | Единая точка входа. Резолвит группу, запускает 3-фазное подключение. Если уже подключена — сразу отложенный UP. Если в процессе — добавляет handle в список |
| `conn_mgr_close(h)` | Убирает один handle. Последний handle при DIRECT/REVERSE шлёт DISCONNECT и чистит entry |
| `conn_mgr_send(h, entry)` | Отправляет данные через `etcp_router`; для INDIRECT — через первого посредника с живым conn; обновляет `last_traffic_tb` |
| `conn_mgr_get_conn(h)` | ETCP_CONN для DIRECT/REVERSE, NULL для INDIRECT |
### Кандидаты-посредники
| Функция | Описание |
|---------|----------|
| `conn_mgr_update_best_candidates(mgr, node_id, rtt)` | Вставляет/обновляет кандидата в отсортированном топ-3 по RTT. Худшие кандидаты вытесняются |
## 4. Алгоритм подключения
> Нумерация фаз в коде ведётся через `entry->main.phase`: DIRECT = 0, REVERSE = 2, INDIRECT = 3 (значение 1 не используется).
### `conn_mgr_open` / `cm_open` (core)
1. Отклоняет `node_id == 0`.
2. Ищет целевой `TOPO_GROUP_NODE`; если нет — подгружает из SQLite (`db_loaded = 1`).
3. Уже `CONNECTED` → новый handle + отложенный UP.
4. Уже `CONNECTING` → добавляет handle в список ожидания.
5. Есть живой ETCP-линк → помечает `CONNECTED` (DIRECT), открывает NCD, отложенный UP.
6. Иначе: новый entry → `CONNECTING`. При локальных адресах у обеих сторон — запускает локальный скан параллельно, затем фазу DIRECT.
### Фаза 1 — DIRECT (`cm_start_phase_direct`)
- `node_conn_direct_open_node` открывает NCD-соединение (пустой `ni`, коллбэк `cm_ncd_callback`).
- `cm_direct_add_links` вручную добавляет линки: перебор адресов в приоритете `NAT > INTERFACE`, v4+v6, TCP+UDP, сопоставление сокетов через `sock_match`.
- Хоть один линк → ждём INIT handshake. Ноль линков → закрыть NCD, перейти к REVERSE/INDIRECT (для `db_loaded` — cleanup + TIMEOUT).
- NCD держит таймаут 2с. Результаты через `cm_ncd_callback`:
- `NCD_EVENT_UP` → `conn_type = (main.phase==2) ? REVERSE : DIRECT`, `CONNECTED`, регистрация conn в группе (`topo_group_new_conn`) и доставка UP.
- `NCD_EVENT_TIMEOUT` → выбор следующей фазы.
- `NCD_EVENT_DOWN` → доставка DOWN (handle жив).
### Локальный скан (`cm_start_local_scan`)
Параллельно DIRECT. Если у обеих сторон приватные/локальные адреса — LAN ping (`etcp_send_ping_to_socket`, до 3 попыток по 100мс). Успех только обновляет RTT (`topo_node_ping_update_rtt`) и помечает узел достижимым локально (`local_scan_state`); соединение при этом устанавливает DIRECT-фаза обычным путём.
### Фаза 2 — REVERSE (`cm_start_phase_reverse`)
Запускается при `NCD_EVENT_TIMEOUT`, если у нас прямой IP, а у цели нет (`cm_has_direct_ip(local) && !cm_has_direct_ip(target)`):
1. Собираем наши публичные/EIM/direct адреса.
2. Шлём `DIRECT_REQ` (наши адреса) цели через BGP-маршрут `etcp_route_send`, таймер 15с. При таймауте NCD-соединение фазы DIRECT **не закрывается** — оно остаётся в `instance->connections`, чтобы обратный INIT цели переиспользовал его. (В пути «ноль линков» из `cm_direct_add_links` NCD закрывается — линков для INIT-matching всё равно нет.)
3. Цель (`cm_handle_direct_req`) открывает NCD к нам и добавляет линки к нашим адресам с NAT-фильтрацией (`sock_match`) — линк создаётся от любого совместимого сокета (цель может быть за NAT).
4. Цель дозванивается → её INIT матчится с нашим conn по `peer_node_id` → conn поднимается → `cm_ncd_callback(UP)` с `main.phase==2` → `conn_type=REVERSE`, `CONNECTED`, UP.
5. Таймаут → `cm_reverse_timeout_cb` (закрывает NCD) → INDIRECT.
### Фаза 3 — INDIRECT (indirect.c)
1. `cm_start_phase_indirect`: пустой `best_candidates` → TIMEOUT. Иначе шлёт `INTERM_EXCHANGE_REQ` со своим топ-4 кандидатов, таймер 15с.
2. Цель (`cm_handle_interm_exchange_req`) пробегает RTT до кандидатов инициатора (probe при протухших), шлёт `EXCHANGE_RESP` со своими кандидатами + замерами RTT.
3. Цель (`cm_handle_interm_exchange_resp`) кеширует ответ, проверяет свежесть RTT до своих кандидатов; протухшие — probe + ретрай 200мс; свежие → `cm_compute_intermediaries`.
4. `cm_compute_intermediaries` суммирует RTT по общим узлам, сортирует, берёт топ-3, шлёт `INTERM_SELECTED`, помечает `conn_type=INDIRECT`, `CONNECTED`, UP.
5. Цель (`cm_handle_interm_selected`) находит свой entry по `src`, сохраняет посредников, помечает `CONNECTED`, `cm_update_nodeinfo` + UP (если ещё не было).
Трафик идёт через `etcp_router` (первый посредник с живым conn; `rr_idx` зарезервирован, не используется).
## 5. Протокол (ETCP_RT_ID_CONN_MGR)
### Подкоманды
| Константа | Значение | Описание |
|-----------|----------|----------|
| `CM_SUBCMD_DIRECT_REQ` | 0x01 | Фаза 2: "я за NAT, подключись ко мне" — наши адреса |
| `CM_SUBCMD_DIRECT_RESP` | 0x02 | Ответ на DIRECT_REQ (зарезервирован) |
| `CM_SUBCMD_INTERM_EXCHANGE_REQ` | 0x03 | Фаза 3 инициатор: наш топ-4 кандидатов |
| `CM_SUBCMD_INTERM_EXCHANGE_RESP` | 0x04 | Фаза 3 ответ: свои + чужие кандидаты с RTT обеих сторон |
| `CM_SUBCMD_INTERM_SELECTED` | 0x05 | Фаза 3 финал: выбранные посредники |
| `CM_SUBCMD_DISCONNECT` | 0x06 | Уведомление о разрыве |
### Пакеты протокола (все packed)
| Структура | Поля |
|-----------|------|
| `CM_DIRECT_REQ` | `cmd, subcmd, request_id, addr_count` + массив адресов `{type, ip[4], port, sock_id}` |
| `CM_DIRECT_RESP` | `cmd, subcmd, request_id, accepted` |
| `CM_EXCHANGE_REQ` | `cmd, subcmd, request_id, candidate_count, candidates[4]` |
| `CM_EXCHANGE_RESP` | `cmd, subcmd, request_id, my_count, your_count, my_candidates[4], your_candidates[4]` |
| `CM_INTERM_SEL` | `cmd, subcmd, request_id, count, selected[3]` |
| `CM_DISCONNECT` | `cmd, subcmd, node_id` |
## 6. Фоновые процессы (monitor.c)
| Таймер | Интервал | Описание |
|--------|----------|----------|
| `bg_ping` | ~100мс (один узел за тик, цикл ≥10с) | Пинг всех BGP-нод без активного соединения, поддерживает свежие RTT для best_candidates |
| `candidate_ping` | ~2с | Удаляет протухших кандидатов (>30с), обновляет `best_candidates`, probe'ит неактивных |
## 7. Ключевые константы
| Константа | Значение | Смысл |
|-----------|----------|-------|
| `CONN_MGR_MAX_CANDIDATES` | 3 | Макс. число лучших посредников в списке |
| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Макс. число выбранных посредников для одного соединения |
| `CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS` | 2000 | Таймаут фазы DIRECT |
| `CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS` | 15000 | Таймаут фазы REVERSE |
| `CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS` | 15000 | Таймаут фазы INDIRECT |
| `CONN_MGR_CANDIDATE_STALE_TB` | 300000 | Порог устаревания кандидата (~30с) |
| `CONN_MGR_CANDIDATE_PING_TB` | 20000 | Интервал ping кандидатов (~2с) |
| `CONN_MGR_BG_PING_INTERVAL_TB` | 1000 | Интервал фонового ping нод (~100мс) |
| `CONN_MGR_BG_PING_CYCLE_MIN_TB` | 100000 | Мин. длительность цикла обхода всех нод (~10с) |
| `CONN_MGR_LOCAL_SCAN_ATTEMPTS` | 3 | Попыток локального сканирования |
| `CONN_MGR_LOCAL_SCAN_TIMEOUT_MS` | 100 | Таймаут одной попытки локального скана |
| `CACHE_MS` | 20000 | Срок свежести RTT-замера (~20с) |
## 8. Типы соединений (итоговый результат)
| Тип | Значение | Описание |
|-----|----------|----------|
| `CONN_TYPE_NONE` | 0 | Нет соединения |
| `CONN_TYPE_DIRECT` | 1 | Прямое соединение |
| `CONN_TYPE_REVERSE` | 2 | Цель подключилась к нам |
| `CONN_TYPE_INDIRECT` | 3 | Через посредника |
## 9. Тесты
| Тест | Покрытие |
|------|----------|
| `tests/test_conn_mgr.c` | DIRECT (существующее соединение + узел из БД), TIMEOUT |
| `tests/test_conn_mgr_already_connected.c` | DIRECT к уже подключённому узлу |
| `tests/test_conn_mgr_phases.c` | Все три фазы на loopback-топологии C(relay)↔A/B: DIRECT, REVERSE (A public + B nat), INDIRECT (A/B nat через C). Адреса цели «портятся» на неверный порт, чтобы DIRECT честно провалился; таймаут probe уменьшен до 10мс через `route_connectivity_set_probe_timeout_ms()` |
Проверки: `test_conn_mgr`, `test_conn_mgr_handles`, `test_conn_mgr_already_connected`,
`test_conn_mgr_phases`. Последний проверяет расписание, LAN, односторонний INDIRECT,
поздний DIRECT после NCD TIMEOUT, резерв, реальные данные и обработку кандидатов.
+188 -182
View File
@@ -1,206 +1,212 @@
/* conn_mgr_indirect.c — INDIRECT фаза + exchange-обработчики */
/* conn_mgr_indirect.c — согласование и проверка пути через посредников. */
#include "conn_mgr_priv.h"
#include "etcp.h"
#include <string.h>
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/u_async.h"
#include "etcp_router.h"
#include "utun_instance.h"
#include "topo_group.h"
#include "route_connectivity.h"
/* INDIRECT фаза: отправляет EXCHANGE_REQ с нашими лучшими кандидатами (best_candidates)
* цели через BGP-маршрут. Ожидаем EXCHANGE_RESP — по RTT-замерам с обеих сторон
* выберем общего посредника с минимальной суммой RTT (cm_compute_intermediaries).
* Периодически ретрансмитим EXCHANGE_REQ (1с) — надёжность на случай потери пакета. */
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry) {
struct CONN_MGR* mgr = entry->mgr;
if (!mgr->best_candidate_count) return;
entry->main.request_id = ++mgr->next_request_id;
struct CM_EXCHANGE_REQ req; memset(&req,0,sizeof(req));
req.cmd=ETCP_RT_ID_CONN_MGR; req.subcmd=CM_SUBCMD_INTERM_EXCHANGE_REQ; req.request_id=entry->main.request_id;
req.candidate_count = mgr->best_candidate_count > 4 ? 4 : mgr->best_candidate_count;
for (uint8_t i=0;i<req.candidate_count;i++) req.candidates[i]=mgr->best_candidates[i];
struct ll_entry* qe = queue_entry_new(0); if (!qe) return;
qe->dgram=u_malloc(1+sizeof(req)); qe->dgram[0]=ETCP_RT_ID_CONN_MGR; memcpy(qe->dgram+1,&req,sizeof(req)); qe->len=(uint16_t)(1+sizeof(req));
/* копия для ретрансмита (etcp_route_send забирает владение qe) */
struct cm_exchange_pending* ep = u_calloc(1,sizeof(*ep));
if (ep) {
ep->request_id=entry->main.request_id; ep->entry=entry;
ep->pkt=u_malloc(1+sizeof(req));
if (ep->pkt) { memcpy(ep->pkt, qe->dgram, 1+sizeof(req)); ep->pkt_len=1+sizeof(req); }
ep->next=mgr->exchange_pending; mgr->exchange_pending=ep;
}
etcp_route_send(mgr->instance, mgr->group->group_id, entry->node_id, qe, 1, 0);
entry->indirect_active = 1;
entry->indirect_deadline_tb = get_time_tb() + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS*10;
if (entry->main.timer) { uasync_cancel_timeout(mgr->instance->ua, entry->main.timer); entry->main.timer = NULL; }
entry->main.timer = uasync_set_timeout(mgr->instance->ua, CONN_MGR_EXCHANGE_RETRANSMIT_MS*10, entry, cm_exchange_tick_cb, "conn_mgr_interm_exch");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: phase 3 sent EXCHANGE_REQ to 0x%016llx with %u candidates",
(unsigned long long)entry->node_id, req.candidate_count);
/* Оба плеча посредника должны быть прямыми READY-сессиями. RTT=0/FFFF — нет замера. */
static int cm_valid_candidate(struct CONN_MGR* mgr, uint64_t target, uint64_t nid, uint16_t remote_rtt) {
return nid && nid != mgr->instance->node_id && nid != target && remote_rtt && remote_rtt != UINT16_MAX &&
cm_candidate_rtt(mgr, nid) != UINT16_MAX;
}
/* Периодический тик INDIRECT: ретрансмит EXCHANGE_REQ, по дедлайну — таймаут обмена. */
void cm_exchange_tick_cb(void* arg) {
struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)arg;
entry->main.timer = NULL;
if (!entry->indirect_active) return;
if (get_time_tb() >= entry->indirect_deadline_tb) {
entry->indirect_active = 0;
{ struct cm_exchange_pending** pp=&entry->mgr->exchange_pending;
while(*pp){if((*pp)->entry==entry){struct cm_exchange_pending* ep=*pp; *pp=ep->next; if(ep->pkt)u_free(ep->pkt); u_free(ep); break;} pp=&(*pp)->next;} }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: exchange timeout for 0x%016llx", (unsigned long long)entry->node_id);
if (!entry->reverse_active) cm_deliver_event(entry, CONN_EVENT_TIMEOUT);
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry) {
struct CONN_MGR* mgr = entry->mgr;
if (!entry->indirect_started) {
entry->indirect_started = 1; entry->exchange_request_id = ++mgr->next_request_id;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT start peer=%016llx elapsed=%llums candidates=%u",
(unsigned long long)entry->node_id, (unsigned long long)((get_time_tb() - entry->start_tb) / 10),
mgr->best_candidate_count);
}
entry->indirect_next_tb = get_time_tb() + CONN_MGR_EXCHANGE_RETRANSMIT_MS * 10;
if (entry->selection.count) {
cm_send_control(mgr, entry->node_id, &entry->selection, sizeof(entry->selection));
return;
}
for (struct cm_exchange_pending* x = entry->mgr->exchange_pending; x; x = x->next) {
if (x->entry == entry && x->pkt) {
struct ll_entry* qe = queue_entry_new(0);
if (qe) {
qe->dgram = u_malloc(x->pkt_len);
if (qe->dgram) { memcpy(qe->dgram, x->pkt, x->pkt_len); qe->len = (uint16_t)x->pkt_len;
etcp_route_send(entry->mgr->instance, entry->mgr->group->group_id, entry->node_id, qe, 1, 0); }
else queue_entry_free(qe);
}
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: exchange retransmit EXCHANGE_REQ 0x%016llx", (unsigned long long)entry->node_id);
struct CM_EXCHANGE_REQ req = {0};
req.cmd = ETCP_RT_ID_CONN_MGR; req.subcmd = CM_SUBCMD_INTERM_EXCHANGE_REQ; req.request_id = entry->exchange_request_id;
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) {
uint64_t nid = mgr->best_candidates[i].node_id;
uint16_t rtt = cm_candidate_rtt(mgr, nid);
if (nid == entry->node_id || rtt == UINT16_MAX) continue;
req.candidates[req.candidate_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt};
}
// Даже пустой запрос позволяет получить кандидатов другой стороны. Повторы подхватят новые замеры.
cm_send_control(mgr, entry->node_id, &req, sizeof(req));
}
void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry) {
if (!entry->intermediariy_count) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: empty intermediary selection peer=%016llx", (unsigned long long)entry->node_id);
return;
}
entry->indirect_confirmed = 1;
uint8_t old_type = entry->conn_type;
cm_refresh_path(entry); // Готовый прямой путь имеет приоритет; повторного UP при улучшении нет.
if (entry->state != CONN_MGR_STATE_DISCONNECTED && entry->conn_type == old_type) cm_update_nodeinfo(entry);
}
/* Объединение кандидатов обеих сторон без дублей, выбор по сумме двух действительных RTT. */
void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp) {
struct scored_candidate { uint64_t nid; uint32_t rtt; };
struct scored_candidate all[2 * CONN_MGR_MAX_CANDIDATES]; uint8_t count = 0;
for (int side = 0; side < 2; side++) {
uint8_t n = side ? resp->your_count : resp->my_count;
struct CONN_MGR_CANDIDATE* candidates = side ? resp->your_candidates : resp->my_candidates;
for (uint8_t i = 0; i < n; i++) {
uint64_t nid = candidates[i].node_id;
if (!cm_valid_candidate(entry->mgr, entry->node_id, nid, candidates[i].rtt)) continue;
int duplicate = 0;
for (uint8_t j = 0; j < count; j++) if (all[j].nid == nid) duplicate = 1;
if (duplicate) continue;
all[count].nid = nid; all[count++].rtt = (uint32_t)cm_candidate_rtt(entry->mgr, nid) + candidates[i].rtt;
}
}
for (uint8_t i = 0; i < count; i++) for (uint8_t j = i + 1; j < count; j++) {
if (all[j].rtt < all[i].rtt || (all[j].rtt == all[i].rtt && all[j].nid < all[i].nid)) {
struct scored_candidate tmp = all[i]; all[i] = all[j]; all[j] = tmp;
}
}
if (!count) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: no common measured READY intermediary peer=%016llx; retry pending",
(unsigned long long)entry->node_id);
return;
}
struct CM_INTERM_SEL* sel = &entry->selection; memset(sel, 0, sizeof(*sel));
sel->cmd = ETCP_RT_ID_CONN_MGR; sel->subcmd = CM_SUBCMD_INTERM_SELECTED; sel->request_id = entry->exchange_request_id;
sel->count = count > CONN_MGR_MAX_INTERMEDIARIES ? CONN_MGR_MAX_INTERMEDIARIES : count;
for (uint8_t i = 0; i < sel->count; i++) {
sel->selected[i].node_id = all[i].nid; sel->selected[i].rtt = cm_candidate_rtt(entry->mgr, all[i].nid);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: intermediary peer=%016llx via=%016llx sum_rtt=%u",
(unsigned long long)entry->node_id, (unsigned long long)all[i].nid, all[i].rtt);
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT selected peer=%016llx count=%u; verifying delivery",
(unsigned long long)entry->node_id, sel->count);
cm_send_control(entry->mgr, entry->node_id, sel, sizeof(*sel));
}
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
if (len != sizeof(struct CM_EXCHANGE_RESP)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed EXCHANGE_RESP peer=%016llx len=%zu", (unsigned long long)src, len);
return;
}
struct CM_EXCHANGE_RESP resp; memcpy(&resp, data, sizeof(resp));
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if (!entry || !entry->indirect_started || entry->indirect_confirmed || entry->selection.count ||
entry->exchange_request_id != resp.request_id || get_time_tb() >= entry->deadline_tb) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale EXCHANGE_RESP peer=%016llx request=%u", (unsigned long long)src, resp.request_id);
return;
}
if (resp.my_count > CONN_MGR_MAX_CANDIDATES || resp.your_count > CONN_MGR_MAX_CANDIDATES) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid candidate counts peer=%016llx", (unsigned long long)src); return;
}
cm_compute_intermediaries(entry, &resp);
}
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
if (len != sizeof(struct CM_INTERM_SEL)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed INTERM_SELECTED peer=%016llx len=%zu", (unsigned long long)src, len);
return;
}
struct CM_INTERM_SEL sel; memcpy(&sel, data, sizeof(sel));
if (!sel.count || sel.count > CONN_MGR_MAX_INTERMEDIARIES) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid intermediary count=%u peer=%016llx", sel.count, (unsigned long long)src); return;
}
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if (!entry) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selection without exchange peer=%016llx", (unsigned long long)src); return; }
if (entry->peer_exchange_request_id != sel.request_id) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale selection peer=%016llx request=%u", (unsigned long long)src, sel.request_id);
return;
}
if (get_time_tb() >= entry->deadline_tb && !entry->indirect_confirmed) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selection after deadline peer=%016llx", (unsigned long long)src);
return;
}
struct CM_INTERM_SEL ack = {0}; ack.cmd = ETCP_RT_ID_CONN_MGR; ack.subcmd = CM_SUBCMD_INTERM_ACK; ack.request_id = sel.request_id;
for (uint8_t i = 0; i < sel.count; i++) {
uint64_t nid = sel.selected[i].node_id;
if (!cm_valid_candidate(mgr, src, nid, sel.selected[i].rtt)) continue;
int duplicate = 0;
for (uint8_t j = 0; j < ack.count; j++) if (ack.selected[j].node_id == nid) duplicate = 1;
if (!duplicate) ack.selected[ack.count++] = (struct CONN_MGR_CANDIDATE){nid, cm_candidate_rtt(mgr, nid)};
}
if (!ack.count) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selected intermediaries unavailable peer=%016llx", (unsigned long long)src);
cm_send_control(mgr, src, &ack, sizeof(ack)); return;
}
for (uint8_t i = 0; i < ack.count; i++) entry->intermediaries[i] = ack.selected[i].node_id;
entry->intermediariy_count = ack.count;
// Публикуем путь до ACK, чтобы ответ прошёл через выбранного посредника.
cm_finalize_indirect(entry);
cm_send_control(mgr, src, &ack, sizeof(ack));
}
void cm_handle_interm_ack(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
if (len != sizeof(struct CM_INTERM_SEL)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed INTERM_ACK peer=%016llx len=%zu", (unsigned long long)src, len); return;
}
struct CM_INTERM_SEL ack; memcpy(&ack, data, sizeof(ack));
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if (!entry || entry->exchange_request_id != ack.request_id || !entry->selection.count || entry->indirect_confirmed ||
get_time_tb() >= entry->deadline_tb) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale INTERM_ACK peer=%016llx request=%u", (unsigned long long)src, ack.request_id);
return;
}
if (ack.count > CONN_MGR_MAX_INTERMEDIARIES) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid ACK count=%u peer=%016llx", ack.count, (unsigned long long)src); return;
}
entry->intermediariy_count = 0;
for (uint8_t i = 0; i < ack.count; i++) {
uint64_t nid = ack.selected[i].node_id;
if (!cm_valid_candidate(mgr, src, nid, ack.selected[i].rtt)) continue;
for (uint8_t j = 0; j < entry->selection.count; j++) {
if (entry->selection.selected[j].node_id != nid) continue;
int duplicate = 0;
for (uint8_t k = 0; k < entry->intermediariy_count; k++) if (entry->intermediaries[k] == nid) duplicate = 1;
if (!duplicate) entry->intermediaries[entry->intermediariy_count++] = nid;
break;
}
}
entry->main.timer = uasync_set_timeout(entry->mgr->instance->ua, CONN_MGR_EXCHANGE_RETRANSMIT_MS*10,
entry, cm_exchange_tick_cb, "conn_mgr_interm_exch");
}
/* Финализация INDIRECT: пометить CONNECTED и доставить UP. Вызывается либо сразу
* (reverse не активен), либо из cm_reverse_tick_cb после reverse-таймаута. */
void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry) {
entry->conn_type = CONN_TYPE_INDIRECT;
entry->state = CONN_MGR_STATE_CONNECTED;
entry->indirect_active = 0; entry->indirect_ready = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT CONNECTED 0x%016llx via %u intermediaries",
(unsigned long long)entry->node_id, entry->intermediariy_count);
cm_update_nodeinfo(entry);
cm_deliver_event(entry, CONN_EVENT_UP);
}
/* Вычисляет лучших общих посредников по данным из EXCHANGE_RESP:
* your_candidates — кандидаты инициатора + НАШ RTT до них
* my_candidates — НАШИ кандидаты + RTT цели до них
* Суммирует RTT для каждого общего узла, сортирует, выбирает топ-3.
* Отправляет INTERM_SELECTED инициатору и доставляет CONN_EVENT_UP. */
void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp) {
struct { uint64_t nid; uint64_t rtt; } all[8]; uint8_t cnt=0;
uint8_t your_n = resp->your_count > 4 ? 4 : resp->your_count;
uint8_t my_n = resp->my_count > 4 ? 4 : resp->my_count;
uint64_t self = entry->mgr->instance->node_id, target = entry->node_id;
for (uint8_t i=0;i<your_n&&cnt<8;i++) {
uint64_t nid=resp->your_candidates[i].node_id;
if (nid==self||nid==target) continue; /* сам/цель не может быть посредником */
uint16_t our=0; for(uint8_t j=0;j<CONN_MGR_MAX_CANDIDATES;j++) if(entry->mgr->best_candidates[j].node_id==nid){our=entry->mgr->best_candidates[j].rtt;break;}
all[cnt]=(typeof(all[0])){nid,(uint64_t)our+resp->your_candidates[i].rtt}; cnt++;
if (!entry->intermediariy_count) {
memset(&entry->selection, 0, sizeof(entry->selection));
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: intermediary verification failed peer=%016llx; repeating exchange", (unsigned long long)src);
return;
}
for (uint8_t i=0;i<my_n&&cnt<8;i++) {
uint64_t nid=resp->my_candidates[i].node_id;
if (nid==self||nid==target) continue; /* сам/цель не может быть посредником */
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(entry->mgr->group,nid);
uint16_t our=nq?topo_get_chain_rtt(nq):0; if(our==0xFFFF)our=0;
all[cnt]=(typeof(all[0])){nid,(uint64_t)our+resp->my_candidates[i].rtt}; cnt++;
}
for (uint8_t i=0;i<cnt;i++) for(uint8_t j=i+1;j<cnt;j++) if(all[j].rtt<all[i].rtt){typeof(all[0])t=all[i];all[i]=all[j];all[j]=t;}
uint8_t sel=cnt>CONN_MGR_MAX_INTERMEDIARIES?CONN_MGR_MAX_INTERMEDIARIES:cnt;
for(uint8_t i=0;i<sel;i++)entry->intermediaries[i]=all[i].nid;
entry->intermediariy_count=sel; entry->rr_idx=0;
struct CM_INTERM_SEL pkt; memset(&pkt,0,sizeof(pkt));
pkt.cmd=ETCP_RT_ID_CONN_MGR; pkt.subcmd=CM_SUBCMD_INTERM_SELECTED; pkt.count=sel;
for(uint8_t i=0;i<sel;i++){pkt.selected[i].node_id=all[i].nid;pkt.selected[i].rtt=0;
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(entry->mgr->group,all[i].nid); if(nq)pkt.selected[i].rtt=topo_get_chain_rtt(nq);}
struct ll_entry* qe=queue_entry_new(0);
if(qe){qe->dgram=u_malloc(1+sizeof(pkt));qe->dgram[0]=ETCP_RT_ID_CONN_MGR;memcpy(qe->dgram+1,&pkt,sizeof(pkt));qe->len=(uint16_t)(1+sizeof(pkt));etcp_route_send(entry->mgr->instance,entry->mgr->group->group_id,entry->node_id,qe,1,0);}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: indirect OK for 0x%016llx via %u intermediaries",(unsigned long long)entry->node_id,sel);
/* reverse приоритетнее: если он ещё идёт — откладываем финализацию (indirect_ready),
* иначе финализируем INDIRECT сразу. */
if (entry->reverse_active) { entry->indirect_ready = 1; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: indirect deferred 0x%016llx (reverse active)", (unsigned long long)entry->node_id); return; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT confirmed peer=%016llx count=%u elapsed=%llums",
(unsigned long long)src, entry->intermediariy_count, (unsigned long long)((get_time_tb() - entry->start_tb) / 10));
cm_finalize_indirect(entry);
}
void cm_exchange_probe_retry_cb(void* arg) {
struct cm_exchange_pending* ep=(struct cm_exchange_pending*)arg; ep->timer=NULL;
uint8_t need=0; uint64_t now=get_time_tb();
for(uint8_t i=0;i<ep->cached.my_count&&i<4;i++){
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(ep->entry->mgr->group,ep->cached.my_candidates[i].node_id);
if(!nq) continue; /* self / не в группе — не посредник, пропускаем */
if(!cm_is_rtt_fresh(nq,now)){need++;break;}
}
if(!need){cm_compute_intermediaries(ep->entry,&ep->cached);struct cm_exchange_pending**pp=&ep->entry->mgr->exchange_pending;while(*pp){if(*pp==ep){*pp=ep->next;break;}pp=&(*pp)->next;}if(ep->pkt)u_free(ep->pkt);u_free(ep);}
else ep->timer=uasync_set_timeout(ep->entry->mgr->instance->ua,2000,ep,cm_exchange_probe_retry_cb,"cm_exch_probe");
}
/* Инициатор получил EXCHANGE_RESP: кеширует ответ, проверяет свежесть RTT
* до СВОИХ кандидатов (тех, что в my_candidates ответа). Если протухли —
* probe + retry-таймер 200мс. Если свежие — cm_compute_intermediaries. */
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len) {
const struct CM_EXCHANGE_RESP* resp=(const struct CM_EXCHANGE_RESP*)data;
if(len<offsetof(struct CM_EXCHANGE_RESP,your_candidates))return;
if(resp->my_count>4||resp->your_count>4)return;
if(len<offsetof(struct CM_EXCHANGE_RESP,your_candidates)+(size_t)resp->your_count*sizeof(struct CONN_MGR_CANDIDATE))return;
struct cm_exchange_pending* ep=mgr->exchange_pending;
while(ep){if(ep->request_id==resp->request_id)break;ep=ep->next;}
if(!ep){DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: EXCHANGE_RESP req=%u no pending",resp->request_id);return;}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: EXCHANGE_RESP req=%u my=%u your=%u",
resp->request_id, resp->my_count, resp->your_count);
if(ep->entry->main.timer){uasync_cancel_timeout(mgr->instance->ua,ep->entry->main.timer);ep->entry->main.timer=NULL;}
memcpy(&ep->cached,resp,len<sizeof(ep->cached)?len:sizeof(ep->cached)); ep->received=1; ep->probed=0;
uint8_t need=0; uint64_t now=get_time_tb();
for(uint8_t i=0;i<ep->cached.my_count&&i<4;i++){
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(mgr->group,ep->cached.my_candidates[i].node_id);
if(!nq) continue; /* self / не в группе — не посредник, пропускаем */
if(!cm_is_rtt_fresh(nq,now)){need++;route_connectivity_probe_node(mgr->instance,mgr->group,nq);}
}
if(!need){ep->probed=1;cm_compute_intermediaries(ep->entry,&ep->cached);struct cm_exchange_pending**pp=&mgr->exchange_pending;while(*pp){if(*pp==ep){*pp=ep->next;break;}pp=&(*pp)->next;}if(ep->pkt)u_free(ep->pkt);u_free(ep);return;}
ep->timer=uasync_set_timeout(mgr->instance->ua,2000,ep,cm_exchange_probe_retry_cb,"cm_exch_probe");
}
/* Финальный выбор посредников от инициатора (приходит второй стороне INDIRECT-
* обмена). Сохраняет выбранные intermediaries в entry, помечает CONNECTED. */
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
const struct CM_INTERM_SEL* sel=(const struct CM_INTERM_SEL*)data;
if(len<offsetof(struct CM_INTERM_SEL,selected))return;
uint8_t c=sel->count>CONN_MGR_MAX_INTERMEDIARIES?CONN_MGR_MAX_INTERMEDIARIES:sel->count;
if(len<offsetof(struct CM_INTERM_SEL,selected)+(size_t)c*sizeof(struct CONN_MGR_CANDIDATE))return;
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if(!entry)return;
if(entry->state==CONN_MGR_STATE_CONNECTED)return; /* симметричный случай — UP уже доставлен инициаторским путём */
if(!entry->indirect_active && !entry->reverse_active)return; /* не INDIRECT/REVERSE фаза */
for(uint8_t i=0;i<c;i++)entry->intermediaries[i]=sel->selected[i].node_id;
entry->intermediariy_count=c; entry->rr_idx=0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: got INTERM_SELECTED from 0x%016llx count=%u",(unsigned long long)src,c);
if (entry->reverse_active) { entry->indirect_ready = 1; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: indirect deferred 0x%016llx (reverse active)", (unsigned long long)entry->node_id); return; }
cm_finalize_indirect(entry);
}
/* Цель получила EXCHANGE_REQ: измеряет RTT до кандидатов инициатора (если
* замеры протухли — запускает probe), отправляет EXCHANGE_RESP со СВОИМИ
* кандидатами + замерами RTT до кандидатов инициатора. */
void cm_handle_interm_exchange_req(uint64_t src, struct CONN_MGR* mgr, struct CM_EXCHANGE_REQ* req) {
if (!mgr) return;
if (req->candidate_count > 4) return;
uint64_t now=get_time_tb();
for(uint8_t i=0;i<req->candidate_count&&i<4;i++){
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(mgr->group,req->candidates[i].node_id);
if(nq&&cm_is_rtt_fresh(nq,now))continue; route_connectivity_probe_node(mgr->instance,mgr->group,nq);
if (req->candidate_count > CONN_MGR_MAX_CANDIDATES || !topo_node_registry_find(mgr->instance->topo_groups, src)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid EXCHANGE_REQ peer=%016llx count=%u", (unsigned long long)src, req->candidate_count);
return;
}
struct CM_EXCHANGE_RESP resp; memset(&resp,0,sizeof(resp));
resp.cmd=ETCP_RT_ID_CONN_MGR; resp.subcmd=CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id=req->request_id;
resp.my_count=mgr->best_candidate_count>4?4:mgr->best_candidate_count;
for(uint8_t i=0;i<resp.my_count;i++)resp.my_candidates[i]=mgr->best_candidates[i];
resp.your_count=req->candidate_count>4?4:req->candidate_count;
for(uint8_t i=0;i<resp.your_count;i++){
resp.your_candidates[i].node_id=req->candidates[i].node_id;
struct TOPO_GROUP_NODE* nq2=topo_node_find_by_id(mgr->group,req->candidates[i].node_id);
resp.your_candidates[i].rtt=nq2?topo_get_chain_rtt(nq2):0;
struct CONN_MGR_ENTRY* entry = cm_ensure_entry(mgr, src);
if (!entry) return;
entry->peer_exchange_request_id = req->request_id;
if (entry->state == CONN_MGR_STATE_DISCONNECTED) {
entry->state = CONN_MGR_STATE_CONNECTING; entry->start_tb = get_time_tb();
entry->deadline_tb = entry->start_tb + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS * 10;
cm_arm_timer(entry);
}
size_t sz=offsetof(struct CM_EXCHANGE_RESP,your_candidates)+(size_t)resp.your_count*sizeof(struct CONN_MGR_CANDIDATE);
struct ll_entry* qe=queue_entry_new(0);
if(qe){qe->dgram=u_malloc(1+sz);qe->dgram[0]=ETCP_RT_ID_CONN_MGR;memcpy(qe->dgram+1,&resp,sz);qe->len=(uint16_t)(1+sz);etcp_route_send(mgr->instance,mgr->group->group_id,src,qe,1,0);}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: sent EXCHANGE_RESP to 0x%016llx my=%u your=%u",(unsigned long long)src,resp.my_count,resp.your_count);
struct CM_EXCHANGE_RESP resp = {0};
resp.cmd = ETCP_RT_ID_CONN_MGR; resp.subcmd = CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id = req->request_id;
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) {
uint64_t nid = mgr->best_candidates[i].node_id; uint16_t rtt = cm_candidate_rtt(mgr, nid);
if (nid == src || rtt == UINT16_MAX) continue;
resp.my_candidates[resp.my_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt};
}
for (uint8_t i = 0; i < req->candidate_count; i++) {
uint64_t nid = req->candidates[i].node_id; uint16_t rtt = cm_candidate_rtt(mgr, nid);
if (nid == src || nid == mgr->instance->node_id || rtt == UINT16_MAX) continue;
resp.your_candidates[resp.your_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt};
}
cm_send_control(mgr, src, &resp, sizeof(resp));
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: EXCHANGE_RESP peer=%016llx request=%u my=%u your=%u",
(unsigned long long)src, req->request_id, resp.my_count, resp.your_count);
}
+21 -34
View File
@@ -26,9 +26,8 @@ struct stcp_client;
#define CONN_MGR_CANDIDATE_PING_TB 20000
#define CONN_MGR_BG_PING_INTERVAL_TB 1000
#define CONN_MGR_BG_PING_CYCLE_MIN_TB 600000
#define CONN_MGR_LOCAL_SCAN_ATTEMPTS 3
#define CONN_MGR_LOCAL_SCAN_TIMEOUT_MS 100
#define CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS 2000
#define CONN_MGR_REVERSE_DELAY_MS 500
#define CONN_MGR_INDIRECT_DELAY_MS 1000
#define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 3000
#define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000
#define CONN_MGR_REVERSE_RETRANSMIT_MS 1000
@@ -61,15 +60,16 @@ enum {
CM_SUBCMD_INTERM_EXCHANGE_RESP= 0x04,
CM_SUBCMD_INTERM_SELECTED = 0x05,
CM_SUBCMD_DISCONNECT = 0x06,
CM_SUBCMD_INTERM_ACK = 0x07,
};
struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed));
#pragma pack(push, 1)
struct CM_DIRECT_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t addr_count; };
struct CM_DIRECT_REQ { uint8_t cmd, subcmd; uint32_t request_id; };
struct CM_DIRECT_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t accepted; };
struct CM_EXCHANGE_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t candidate_count; struct CONN_MGR_CANDIDATE candidates[4]; };
struct CM_EXCHANGE_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t my_count, your_count; struct CONN_MGR_CANDIDATE my_candidates[4], your_candidates[4]; };
struct CM_EXCHANGE_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t candidate_count; struct CONN_MGR_CANDIDATE candidates[CONN_MGR_MAX_CANDIDATES]; };
struct CM_EXCHANGE_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t my_count, your_count; struct CONN_MGR_CANDIDATE my_candidates[CONN_MGR_MAX_CANDIDATES], your_candidates[CONN_MGR_MAX_CANDIDATES]; };
struct CM_INTERM_SEL { uint8_t cmd, subcmd; uint32_t request_id; uint8_t count; struct CONN_MGR_CANDIDATE selected[3]; };
struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; };
#pragma pack(pop)
@@ -83,15 +83,6 @@ struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; };
/* ═══════ внутренние структуры ═══════ */
struct cm_reverse_pending { struct cm_reverse_pending* next; uint32_t request_id; struct CONN_MGR_ENTRY* entry; uint8_t* pkt; size_t pkt_len; };
struct cm_exchange_pending {
struct cm_exchange_pending* next; uint32_t request_id;
struct CONN_MGR_ENTRY* entry; void* timer;
struct CM_EXCHANGE_RESP cached; uint8_t received, probed;
uint8_t* pkt; size_t pkt_len;
};
struct CONN_MGR_HANDLE {
uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg;
struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next;
@@ -106,13 +97,14 @@ struct CONN_MGR_ENTRY {
uint8_t state, conn_type;
uint64_t last_traffic_tb;
struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle;
uint8_t ncd_down_notified;
uint64_t intermediaries[3]; uint8_t intermediariy_count, rr_idx;
uint64_t intermediaries[3]; uint8_t intermediariy_count;
uint8_t local_scan_state, main_connect_state, db_loaded;
uint8_t reverse_active, indirect_active, indirect_ready;
void* reverse_timer;
uint64_t reverse_deadline_tb, indirect_deadline_tb;
struct { void* timer; uint32_t request_id; void* conn_ctx; } main;
uint8_t reverse_started, indirect_started, indirect_confirmed, direct_up, timeout_notified, ever_up;
void* timer; void* refresh_token;
uint64_t start_tb, reverse_start_tb, indirect_start_tb, deadline_tb;
uint64_t reverse_next_tb, reverse_deadline_tb, indirect_next_tb;
uint32_t reverse_request_id, exchange_request_id, peer_exchange_request_id;
struct CM_INTERM_SEL selection; /* Исходящий выбор до подтверждения пира. */
struct CONN_MGR* mgr;
};
@@ -124,10 +116,7 @@ struct CONN_MGR {
size_t bg_ping_cursor; uint64_t bg_ping_cycle_start_tb;
struct CONN_MGR_CANDIDATE best_candidates[CONN_MGR_MAX_CANDIDATES]; uint8_t best_candidate_count;
struct stcp_client* cand_tcp_pings[CONN_MGR_MAX_CANDIDATES]; /* влётные candidate TCP-пинги (для отмены при destroy) */
uint32_t next_request_id, direct_timeout_ms; uint8_t initialized;
struct cm_reverse_pending* reverse_pending;
struct cm_exchange_pending* exchange_pending;
uint8_t _unused[0];
uint32_t next_request_id; uint8_t initialized;
};
/* ═══════ декларации внутренних функций ═══════ */
@@ -151,28 +140,26 @@ void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry);
void cm_start_local_scan(struct CONN_MGR_ENTRY* entry);
void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* data, size_t len);
void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id);
void cm_reverse_tick_cb(void* arg);
void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry);
void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s);
void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s);
void cm_refresh_path(struct CONN_MGR_ENTRY* entry);
void cm_arm_timer(struct CONN_MGR_ENTRY* entry);
int cm_send_control(struct CONN_MGR* mgr, uint64_t node_id, const void* packet, size_t len);
uint16_t cm_candidate_rtt(struct CONN_MGR* mgr, uint64_t node_id);
struct ETCP_CONN* cm_direct_conn(struct CONN_MGR* mgr, uint64_t node_id);
int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq);
int cm_has_local_addr(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq);
uint8_t cm_classify_v6_addr(const uint8_t addr[16]);
uint8_t cm_sock_v6_classify(const struct ETCP_SOCKET* s);
#define CM_V6_LL 0
#define CM_V6_LOC 1
#define CM_V6_DIR 2
#define CM_V6_OTH 3
#define CM_V6_ANY 4
/* indirect */
void cm_exchange_tick_cb(void* arg);
void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp);
void cm_exchange_probe_retry_cb(void* arg);
void cm_handle_interm_exchange_req(uint64_t src, struct CONN_MGR* mgr, struct CM_EXCHANGE_REQ* req);
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len);
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len);
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len);
void cm_handle_interm_ack(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len);
/* monitor */
void cm_bg_ping_timer_cb(void* arg);
+16 -10
View File
@@ -22,6 +22,7 @@ in ---> {Q} -> [src, etcp] --> ... --> [dst,etcp] -> {asm_q} -> {buf q} ---> out
#include "etcp.h"
#include "utun_instance.h"
#include "topo_group.h"
#include "conn_mgr.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/ll_queue.h"
@@ -294,16 +295,11 @@ static struct ETCP_ROUTER_CONN* router_conn_find(struct UTUN_INSTANCE* inst,
// ====================================================================
// Найти ETCP_CONN для отправки (учитывая indirect-посредников). NULL = нет маршрута.
static struct ETCP_CONN* router_route_conn(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id) {
static struct ETCP_CONN* router_route_conn(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id) {
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
if (group) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, remote_node_id);
if (nq && nq->conn_mgr_type == CONN_TYPE_INDIRECT && nq->conn_mgr_intermediariy_count > 0) {
for (uint8_t i = 0; i < nq->conn_mgr_intermediariy_count; i++) {
struct ETCP_CONN* c = topo_group_find_conn_for_node(group, nq->conn_mgr_intermediaries[i]);
if (c) return c;
}
}
struct ETCP_CONN* preferred = conn_mgr_route_conn(group->conn_mgr, remote_node_id, svc_id == ETCP_RT_ID_CONN_MGR);
if (preferred) return preferred;
return topo_group_find_conn_for_node(group, remote_node_id);
}
/* group_id=0 (или группа не найдена) — глобальная маршрутизация: прямое соединение по node_id */
@@ -312,7 +308,7 @@ static struct ETCP_CONN* router_route_conn(struct UTUN_INSTANCE* inst, uint64_t
// Тонкая обёртка над router_route_conn для rconn (next_hop для отправки).
static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) {
return router_route_conn(rconn->inst, rconn->group_id, rconn->remote_node_id);
return router_route_conn(rconn->inst, rconn->group_id, rconn->remote_node_id, rconn->svc_id);
}
// Есть ли физический маршрут до узла (прямой/indirect/глобальный): 1 — есть, 0 — нет.
@@ -320,7 +316,17 @@ static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) {
int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id) {
if (!inst) return 0;
if (dst_node_id == inst->node_id) return 1;
return router_route_conn(inst, group_id, dst_node_id) != NULL;
return router_route_conn(inst, group_id, dst_node_id, 0) != NULL;
}
struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t dst_node_id, uint8_t svc_id) {
struct ETCP_ROUTER_PATH path = {0};
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "path snapshot without instance"); return path; }
struct ETCP_CONN* conn = router_route_conn(inst, group_id, dst_node_id, svc_id);
path.type = conn_mgr_connection_type(conn, dst_node_id);
if (path.type != CONN_TYPE_NONE) path.next_hop_node_id = conn->peer_node_id;
return path;
}
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.
+6
View File
@@ -244,6 +244,12 @@ struct ETCP_ROUTER_CONN* etcp_router_conn_get(struct UTUN_INSTANCE* inst,
// 1 = найден транспорт (для self всегда 1); это не гарантия UP/готовности логического канала.
int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id);
// Снимок текущего пути отправки сервиса, только uasync-поток; без создания канала/изменения очередей.
// type — CONN_TYPE_*; отсутствие живого пути: NONE и next_hop_node_id=0.
struct ETCP_ROUTER_PATH { uint8_t type; uint64_t next_hop_node_id; };
struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t dst_node_id, uint8_t svc_id);
// Отправить данные с авто-seq и контролем inflight
// data: payload без svc_id, mode: ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (0 = обычный)
// Копирует data; 0=принято в очередь, <0=ошибка. Доставка подтверждается отдельно протоколом.
+2
View File
@@ -217,6 +217,7 @@ static void topo_group_log_exchange(struct TOPO_GROUP* group, struct TOPO_GROUP_
(unsigned long long)group->group_id, (unsigned long long)peer->conn->peer_node_id,
(unsigned long long)peer->local_epoch, peer->table_sent, peer->table_received,
topo_group_peer_ready(group, peer->conn->peer_node_id));
conn_mgr_routes_changed(group->conn_mgr);
}
static struct TOPOMSG_HEADER topo_header(struct TOPO_GROUP_CONN_ITEM* peer, uint8_t cmd) {
@@ -1153,6 +1154,7 @@ static void topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROU
for (struct topo_node_cbk_entry* c = group->node_cbks; c; c = c->next) c->fn(group, id, event, c->arg);
topo_group_publish_route(group, id);
topo_recovery_changed(group);
conn_mgr_routes_changed(group->conn_mgr);
}
/* TABLE_COMPLETE подтверждает только NODEINFO текущего обмена. Сбой обмена