26 changed files with 1122 additions and 1005 deletions
@ -1,215 +1,114 @@
|
||||
# conn_mgr — Менеджер установки соединений |
||||
|
||||
## 1. Назначение |
||||
Трёхфазный асинхронный менеджер подключения к удалённым нодам. Отвечает за установку ETCP-соединения с выбором оптимального способа: |
||||
|
||||
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). |
||||
|
||||
Один `CONN_MGR` обслуживает одну `TOPO_GROUP` (создаётся в `conn_mgr_init(group)`). Внутри — кеш `entries` (по одной записи на целевой узел) и список `handle`'ов (по одному на каждый вызов `conn_mgr_open`). |
||||
|
||||
## 2. Как пользоваться |
||||
|
||||
### Инициализация |
||||
```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 |
||||
``` |
||||
|
||||
### Подключение (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 */ } |
||||
} |
||||
|
||||
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 |
||||
``` |
||||
|
||||
### Закрытие |
||||
```c |
||||
conn_mgr_close(h); // убирает ОДИН handle; последний рвёт соединение |
||||
``` |
||||
|
||||
### Кандидаты-посредники |
||||
`cm_candidate_ping_timer_cb` (раз в ~2с) пополняет `best_candidates` (топ-3 по RTT) из узлов группы с активным соединением и пингует/probe'ит их. Список используется фазой INDIRECT. Внешний код (`route_connectivity`) лишь заполняет `nq->connectivity`, откуда берутся свежие RTT. |
||||
|
||||
### Нюансы |
||||
- Несколько `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 |
||||
|
||||
## 3. API |
||||
|
||||
### События (enum conn_mgr_event) |
||||
|
||||
| Событие | Значение | Смысл | |
||||
|---------|----------|-------| |
||||
| `CONN_EVENT_UP` | 0 | Связь есть, можно отправлять данные | |
||||
| `CONN_EVENT_DOWN` | 1 | Временный обрыв, handle жив, само восстановится | |
||||
| `CONN_EVENT_TIMEOUT` | 2 | Подключение не удалось, handle жив, закройте сами | |
||||
|
||||
### Коллбэк |
||||
```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` всегда валидны. |
||||
|
||||
### Корневые структуры |
||||
|
||||
| Структура | Описание | |
||||
|-----------|----------| |
||||
| `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-повторов | |
||||
|
||||
### Жизненный цикл |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `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 | |
||||
|
||||
### Подключение / отключение |
||||
|
||||
| Функция | Описание | |
||||
|---------|----------| |
||||
| `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()` | |
||||
# Connection Manager |
||||
|
||||
`conn_mgr` подбирает путь для медиа, файлов и звонков. Один менеджер принадлежит |
||||
одной группе. Несколько сервисов могут удерживать свои handles к одному узлу: |
||||
транспорт и пробы общие, callbacks и отмена принадлежат каждому владельцу. |
||||
Все операции выполняются в uasync-потоке. |
||||
|
||||
## Расписание подключения |
||||
|
||||
Все сроки отсчитываются от одного `start_tb`, а не от таймаута предыдущей пробы. |
||||
|
||||
| Доступность извне | DIRECT | LOCAL | REVERSE | INDIRECT | |
||||
|---|---|---|---|---| |
||||
| Мы доступны, пир нет | Сразу | В составе DIRECT | Сразу | 1000 мс | |
||||
| Оба доступны | Сразу | В составе DIRECT | 500 мс | 1000 мс | |
||||
| Мы недоступны, пир доступен | Сразу | В составе DIRECT | 500 мс | 1000 мс | |
||||
| Оба недоступны | Сразу | Сразу | 500 мс | Сразу | |
||||
|
||||
Доступность определяется по опубликованным адресам и общей классификации |
||||
`sock_match`: публичный IPv4, EIM/прямой NAT, пригодный глобальный IPv6. |
||||
Приватные адреса EIM-интерфейса не считаются внешними. |
||||
|
||||
DIRECT использует NCD, который создаёт UDP/TCP-линки по известной записи узла, |
||||
включая IPv4/IPv6, LAN и Reality-параметры. LOCAL — те же локальные handshake-пробы: |
||||
отдельные ping-контексты и повторные линки не создаются. |
||||
REVERSE просит получателя открыть/дополнить NCD к отправителю по уже известному |
||||
подписанному снимку. Запрос обрабатывается и при CONNECTING, и при работе через |
||||
посредника. Повторы запроса идут раз в секунду в течение трёх секунд. |
||||
|
||||
Запуск нового способа не закрывает NCD и не удаляет предыдущие линки. NCD TIMEOUT |
||||
по умолчанию через две секунды завершает только ожидание первой прямой попытки; |
||||
удерживаемый handle продолжает получать поздний UP. |
||||
|
||||
## Рабочий путь |
||||
|
||||
Состояние доставки (`state`, `conn_type`) отделено от состояния проб. |
||||
Посредник используется сразу после успешного согласования, пока прямой путь |
||||
ещё устанавливается. Прямой транспорт становится рабочим после READY сессии |
||||
нужной группы. Тогда он имеет приоритет независимо от порядка ответов INDIRECT. |
||||
|
||||
Переходы между посредником и прямым путём не сбрасывают состояние `ETCP_ROUTER_CONN`, |
||||
его очереди, seq и неподтверждённые пакеты. Список подтверждённых посредников |
||||
сохраняется как резерв. Для выбора посредника проверяются READY первого плеча |
||||
и наличие объявления маршрута к получателю именно от этого посредника. |
||||
|
||||
DIRECT/REVERSE определяются по текущим живым линкам: наличие исходящего означает |
||||
DIRECT, только входящие — REVERSE. Метка первого UP не используется как постоянный тип. |
||||
Окно звонка показывает фактический путь router, включая BGP fallback до готовности CM: |
||||
DIRECT/REVERSE/INDIRECT/NONE. Событие `CHAT_EVT_CALL_CONNECTION` содержит call_id, |
||||
тип и next hop; публикуется при изменении. Проверка идёт раз в секунду общим таймером |
||||
звонка, сразу при начале/принятии и при событиях CM. Завершение сбрасывает статус |
||||
и отменяет таймер. Отображение одинаково в Qt и Android. |
||||
|
||||
Один timer на entry обслуживает ближайшее запланированное действие, повторы и |
||||
общий предел ожидания. READY и изменение маршрутов перепроверяют пути через |
||||
`conn_mgr_routes_changed()`, без периодического опроса работающих соединений. |
||||
Начальное ожидание ограничено 15 секундами от срока запуска INDIRECT, даже если |
||||
нет кандидатов/RTT/ответа. Получение ответа не отменяет этот предел. |
||||
Входящие записи без сервисных handles удаляются по этому сроку; групповые владельцы |
||||
сохраняют общий транспорт. |
||||
|
||||
После готовности прямого пути новые дополнительные пробы не запускаются. |
||||
Уже начатое согласование INDIRECT может завершиться и сохранить резерв. |
||||
После начального бюджета поздние NCD/READY всё ещё могут улучшить путь. Бесконечные |
||||
повторные поиски посредников и гарантированное восстановление не предусмотрены. |
||||
|
||||
## INDIRECT |
||||
|
||||
1. Инициатор отправляет EXCHANGE_REQ со своими кандидатами. |
||||
2. Получатель отвечает EXCHANGE_RESP: свои кандидаты и RTT до кандидатов инициатора. |
||||
3. Инициатор объединяет кандидатов без дублей, исключает себя/получателя, |
||||
неизвестные RTT и узлы без прямой READY-сессии. Выбирает до трёх по сумме RTT |
||||
обоих плеч; при равных RTT — по node_id. |
||||
4. INTERM_SELECTED отправляется через выбранный next hop. Пока нет подтверждения, |
||||
сервис ещё не получает UP. |
||||
5. Получатель проверяет своё плечо и возвращает INTERM_ACK через выбранного |
||||
посредника. Инициатор принимает только узлы из собственного выбора. |
||||
|
||||
Пустой список и 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 не поддерживается. |
||||
|
||||
## 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. |
||||
|
||||
Диагностика CM — категория `general`: INFO показывает расписание, выбор и смену |
||||
пути с временем от старта; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR — |
||||
ошибки, недоступные пути и некорректные пакеты. Создание адресных линков NCD — |
||||
категория `connection`. |
||||
|
||||
Проверки: `test_conn_mgr`, `test_conn_mgr_handles`, `test_conn_mgr_already_connected`, |
||||
`test_conn_mgr_phases`. Последний проверяет расписание, LAN, односторонний INDIRECT, |
||||
поздний DIRECT после NCD TIMEOUT, резерв, реальные данные и обработку кандидатов. |
||||
|
||||
@ -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); |
||||
break; |
||||
} |
||||
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}; |
||||
} |
||||
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"); |
||||
// Даже пустой запрос позволяет получить кандидатов другой стороны. Повторы подхватят новые замеры.
|
||||
cm_send_control(mgr, entry->node_id, &req, sizeof(req)); |
||||
} |
||||
|
||||
/* Финализация 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); |
||||
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); |
||||
} |
||||
|
||||
/* Вычисляет лучших общих посредников по данным из EXCHANGE_RESP:
|
||||
* your_candidates — кандидаты инициатора + НАШ RTT до них |
||||
* my_candidates — НАШИ кандидаты + RTT цели до них |
||||
* Суммирует RTT для каждого общего узла, сортирует, выбирает топ-3. |
||||
* Отправляет INTERM_SELECTED инициатору и доставляет CONN_EVENT_UP. */ |
||||
/* Объединение кандидатов обеих сторон без дублей, выбор по сумме двух действительных RTT. */ |
||||
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++; |
||||
} |
||||
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; } |
||||
cm_finalize_indirect(entry); |
||||
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_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"); |
||||
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); |
||||
} |
||||
|
||||
/* Инициатор получил 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"); |
||||
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)); |
||||
} |
||||
|
||||
/* Финальный выбор посредников от инициатора (приходит второй стороне 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; |
||||
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)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; } |
||||
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; |
||||
} |
||||
} |
||||
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; |
||||
} |
||||
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); |
||||
} |
||||
|
||||
/* Цель получила 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); |
||||
} |
||||
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; |
||||
} |
||||
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); |
||||
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 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); |
||||
} |
||||
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); |
||||
} |
||||
|
||||
Loading…
Reference in new issue