26 changed files with 1122 additions and 1005 deletions
@ -1,215 +1,114 @@ |
|||||||
# conn_mgr — Менеджер установки соединений |
# Connection Manager |
||||||
|
|
||||||
## 1. Назначение |
`conn_mgr` подбирает путь для медиа, файлов и звонков. Один менеджер принадлежит |
||||||
Трёхфазный асинхронный менеджер подключения к удалённым нодам. Отвечает за установку ETCP-соединения с выбором оптимального способа: |
одной группе. Несколько сервисов могут удерживать свои handles к одному узлу: |
||||||
|
транспорт и пробы общие, callbacks и отмена принадлежат каждому владельцу. |
||||||
1. **DIRECT** — прямое INIT-рукопожатие со всеми известными адресами цели (IPv4/IPv6, TCP/UDP, с NAT-фильтрацией через `sock_match`) |
Все операции выполняются в uasync-потоке. |
||||||
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 | |
||||||
|
|---|---|---|---|---| |
||||||
## 2. Как пользоваться |
| Мы доступны, пир нет | Сразу | В составе DIRECT | Сразу | 1000 мс | |
||||||
|
| Оба доступны | Сразу | В составе DIRECT | 500 мс | 1000 мс | |
||||||
### Инициализация |
| Мы недоступны, пир доступен | Сразу | В составе DIRECT | 500 мс | 1000 мс | |
||||||
```c |
| Оба недоступны | Сразу | Сразу | 500 мс | Сразу | |
||||||
struct CONN_MGR* mgr = conn_mgr_init(group); |
|
||||||
// Регистрируется в etcp_router через conn_mgr_router_recv_handler (ETCP_RT_ID_CONN_MGR) |
Доступность определяется по опубликованным адресам и общей классификации |
||||||
// Запускает фоновые таймеры: bg_ping, candidate_ping |
`sock_match`: публичный IPv4, EIM/прямой NAT, пригодный глобальный IPv6. |
||||||
``` |
Приватные адреса EIM-интерфейса не считаются внешними. |
||||||
|
|
||||||
### Подключение (handle-based) |
DIRECT использует NCD, который создаёт UDP/TCP-линки по известной записи узла, |
||||||
```c |
включая IPv4/IPv6, LAN и Reality-параметры. LOCAL — те же локальные handshake-пробы: |
||||||
static void my_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, |
отдельные ping-контексты и повторные линки не создаются. |
||||||
enum conn_mgr_event event, void* arg) { |
REVERSE просит получателя открыть/дополнить NCD к отправителю по уже известному |
||||||
if (event == CONN_EVENT_UP) { /* связь есть */ } |
подписанному снимку. Запрос обрабатывается и при CONNECTING, и при работе через |
||||||
else if (event == CONN_EVENT_DOWN) { /* временный обрыв, сам восстановится */ } |
посредника. Повторы запроса идут раз в секунду в течение трёх секунд. |
||||||
else if (event == CONN_EVENT_TIMEOUT) { /* не удалось, закройте handle */ } |
|
||||||
} |
Запуск нового способа не закрывает NCD и не удаляет предыдущие линки. NCD TIMEOUT |
||||||
|
по умолчанию через две секунды завершает только ожидание первой прямой попытки; |
||||||
struct CONN_MGR_HANDLE* h = NULL; |
удерживаемый handle продолжает получать поздний UP. |
||||||
conn_mgr_open(inst, group_id, node_id, my_cb, arg, &h); |
|
||||||
``` |
## Рабочий путь |
||||||
|
|
||||||
### Отправка данных |
Состояние доставки (`state`, `conn_type`) отделено от состояния проб. |
||||||
```c |
Посредник используется сразу после успешного согласования, пока прямой путь |
||||||
struct ll_entry* e = queue_entry_new(0); |
ещё устанавливается. Прямой транспорт становится рабочим после READY сессии |
||||||
e->dgram = data; e->len = len; |
нужной группы. Тогда он имеет приоритет независимо от порядка ответов INDIRECT. |
||||||
conn_mgr_send(h, e); // забирает владение e, освобождает внутри |
|
||||||
struct ETCP_CONN* conn = conn_mgr_get_conn(h); // NULL для INDIRECT |
Переходы между посредником и прямым путём не сбрасывают состояние `ETCP_ROUTER_CONN`, |
||||||
``` |
его очереди, seq и неподтверждённые пакеты. Список подтверждённых посредников |
||||||
|
сохраняется как резерв. Для выбора посредника проверяются READY первого плеча |
||||||
### Закрытие |
и наличие объявления маршрута к получателю именно от этого посредника. |
||||||
```c |
|
||||||
conn_mgr_close(h); // убирает ОДИН handle; последний рвёт соединение |
DIRECT/REVERSE определяются по текущим живым линкам: наличие исходящего означает |
||||||
``` |
DIRECT, только входящие — REVERSE. Метка первого UP не используется как постоянный тип. |
||||||
|
Окно звонка показывает фактический путь router, включая BGP fallback до готовности CM: |
||||||
### Кандидаты-посредники |
DIRECT/REVERSE/INDIRECT/NONE. Событие `CHAT_EVT_CALL_CONNECTION` содержит call_id, |
||||||
`cm_candidate_ping_timer_cb` (раз в ~2с) пополняет `best_candidates` (топ-3 по RTT) из узлов группы с активным соединением и пингует/probe'ит их. Список используется фазой INDIRECT. Внешний код (`route_connectivity`) лишь заполняет `nq->connectivity`, откуда берутся свежие RTT. |
тип и next hop; публикуется при изменении. Проверка идёт раз в секунду общим таймером |
||||||
|
звонка, сразу при начале/принятии и при событиях CM. Завершение сбрасывает статус |
||||||
### Нюансы |
и отменяет таймер. Отображение одинаково в Qt и Android. |
||||||
- Несколько `conn_mgr_open` на один узел → несколько handle на одном entry; все получают события |
|
||||||
- Повторный `open` для уже подключённой ноды вернёт handle сразу, UP доставляется отложенно (`uasync_call_soon`) |
Один timer на entry обслуживает ближайшее запланированное действие, повторы и |
||||||
- Для `open` в состоянии CONNECTING новый handle добавляется в список ожидания |
общий предел ожидания. READY и изменение маршрутов перепроверяют пути через |
||||||
- Для INDIRECT-соединений `conn_mgr_get_conn` возвращает `NULL`, `conn_mgr_send` идёт через `etcp_router` (первый посредник с живым conn) |
`conn_mgr_routes_changed()`, без периодического опроса работающих соединений. |
||||||
- Пока есть активный ETCP-линк к ноде — соединение считается DIRECT |
Начальное ожидание ограничено 15 секундами от срока запуска INDIRECT, даже если |
||||||
|
нет кандидатов/RTT/ответа. Получение ответа не отменяет этот предел. |
||||||
## 3. API |
Входящие записи без сервисных handles удаляются по этому сроку; групповые владельцы |
||||||
|
сохраняют общий транспорт. |
||||||
### События (enum conn_mgr_event) |
|
||||||
|
После готовности прямого пути новые дополнительные пробы не запускаются. |
||||||
| Событие | Значение | Смысл | |
Уже начатое согласование INDIRECT может завершиться и сохранить резерв. |
||||||
|---------|----------|-------| |
После начального бюджета поздние NCD/READY всё ещё могут улучшить путь. Бесконечные |
||||||
| `CONN_EVENT_UP` | 0 | Связь есть, можно отправлять данные | |
повторные поиски посредников и гарантированное восстановление не предусмотрены. |
||||||
| `CONN_EVENT_DOWN` | 1 | Временный обрыв, handle жив, само восстановится | |
|
||||||
| `CONN_EVENT_TIMEOUT` | 2 | Подключение не удалось, handle жив, закройте сами | |
## INDIRECT |
||||||
|
|
||||||
### Коллбэк |
1. Инициатор отправляет EXCHANGE_REQ со своими кандидатами. |
||||||
```c |
2. Получатель отвечает EXCHANGE_RESP: свои кандидаты и RTT до кандидатов инициатора. |
||||||
typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h, |
3. Инициатор объединяет кандидатов без дублей, исключает себя/получателя, |
||||||
uint64_t node_id, uint64_t group_id, |
неизвестные RTT и узлы без прямой READY-сессии. Выбирает до трёх по сумме RTT |
||||||
enum conn_mgr_event event, void* arg); |
обоих плеч; при равных RTT — по node_id. |
||||||
``` |
4. INTERM_SELECTED отправляется через выбранный next hop. Пока нет подтверждения, |
||||||
`h` может быть `NULL` (ошибки до создания handle). `node_id`/`group_id` всегда валидны. |
сервис ещё не получает UP. |
||||||
|
5. Получатель проверяет своё плечо и возвращает INTERM_ACK через выбранного |
||||||
### Корневые структуры |
посредника. Инициатор принимает только узлы из собственного выбора. |
||||||
|
|
||||||
| Структура | Описание | |
Пустой список и RTT=0/UINT16_MAX не дают успеха. Реальный нулевой текущий RTT |
||||||
|-----------|----------| |
после уже выполненного замера округляется до 1, чтобы отличать его от отсутствия |
||||||
| `CONN_MGR` | Корневая структура менеджера. `instance`, `group`, очередь `entries`, топ-3 `best_candidates`, таймеры (`bg_ping_timer`, `candidate_ping_timer`), `bg_ping_cursor`, списки `reverse_pending`/`exchange_pending`, `next_request_id` | |
замера. При отсутствии пригодных кандидатов EXCHANGE повторяется раз в секунду |
||||||
| `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_open()`. |
||||||
| `CONN_MGR_CANDIDATE` | Пара `{node_id, rtt}` — запись о кандидате-посреднике (packed) | |
Ответы привязаны одновременно к отправителю и request_id. Повторы идемпотентны; |
||||||
| `cm_reverse_pending` | Запись об ожидании REVERSE-подключения (связный список) | |
поздний ответ не может вытеснить рабочий прямой путь. |
||||||
| `cm_exchange_pending` | Запись об ожидании INDIRECT-обмена: кешированный `EXCHANGE_RESP`, флаги `received`/`probed`, таймер probe-повторов | |
|
||||||
|
Wire-структуры описаны в `conn_mgr_priv.h`. DIRECT_REQ содержит только request_id. |
||||||
### Жизненный цикл |
INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{node_id, rtt}`. |
||||||
|
Совместимость с прежним адресным DIRECT_REQ не поддерживается. |
||||||
| Функция | Описание | |
|
||||||
|---------|----------| |
## API и владение |
||||||
| `conn_mgr_init(group)` | Выделяет CONN_MGR, создаёт очередь entries, запускает bg_ping и candidate_ping таймеры | |
|
||||||
| `conn_mgr_destroy(mgr)` | Отменяет таймеры, освобождает все entries, exchange_pending, reverse_pending | |
- `conn_mgr_open()` создаёт отдельный handle. Если попытка уже идёт, добавляет |
||||||
| `conn_mgr_router_recv_handler(conn, entry)` | Обработчик входящих пакетов `ETCP_RT_ID_CONN_MGR` из etcp_router; диспетчеризация по subcmd | |
ожидающего; если путь готов, доставляет UP отложенно. |
||||||
|
Первый сервисный handle входящего пути запускает своё расписание проб, |
||||||
### Подключение / отключение |
сохраняя уже работающий путь через посредника. |
||||||
|
- UP означает появление пригодного пути. Улучшение пути не повторяет UP. |
||||||
| Функция | Описание | |
- DOWN означает потерю рабочих путей; при живом резерве DOWN не доставляется. |
||||||
|---------|----------| |
- TIMEOUT означает, что начальное подключение не получило ни одного рабочего пути. |
||||||
| `conn_mgr_open(inst, group_id, node_id, cb, cb_arg, &h)` | Единая точка входа. Резолвит группу, запускает 3-фазное подключение. Если уже подключена — сразу отложенный UP. Если в процессе — добавляет handle в список | |
Ошибка отдельной пробы при живом посреднике не становится TIMEOUT для сервиса. |
||||||
| `conn_mgr_close(h)` | Убирает один handle. Последний handle при DIRECT/REVERSE шлёт DISCONNECT и чистит entry | |
- DOWN/TIMEOUT не освобождают handle. Владелец обязан вызвать `conn_mgr_close()`. |
||||||
| `conn_mgr_send(h, entry)` | Отправляет данные через `etcp_router`; для INDIRECT — через первого посредника с живым conn; обновляет `last_traffic_tb` | |
- Закрытие последнего handle отменяет CM timer и callbacks и закрывает только |
||||||
| `conn_mgr_get_conn(h)` | ETCP_CONN для DIRECT/REVERSE, NULL для INDIRECT | |
NCD-владение CM. Группа и другие NCD-владельцы сохраняют транспорт. |
||||||
|
- `conn_mgr_get_conn()` возвращает заимствованный прямой транспорт либо NULL |
||||||
### Кандидаты-посредники |
при INDIRECT/отсоединённом handle. Данные следует отправлять через router. |
||||||
|
|
||||||
| Функция | Описание | |
Диагностика CM — категория `general`: INFO показывает расписание, выбор и смену |
||||||
|---------|----------| |
пути с временем от старта; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR — |
||||||
| `conn_mgr_update_best_candidates(mgr, node_id, rtt)` | Вставляет/обновляет кандидата в отсортированном топ-3 по RTT. Худшие кандидаты вытесняются | |
ошибки, недоступные пути и некорректные пакеты. Создание адресных линков NCD — |
||||||
|
категория `connection`. |
||||||
## 4. Алгоритм подключения |
|
||||||
|
Проверки: `test_conn_mgr`, `test_conn_mgr_handles`, `test_conn_mgr_already_connected`, |
||||||
> Нумерация фаз в коде ведётся через `entry->main.phase`: DIRECT = 0, REVERSE = 2, INDIRECT = 3 (значение 1 не используется). |
`test_conn_mgr_phases`. Последний проверяет расписание, LAN, односторонний INDIRECT, |
||||||
|
поздний DIRECT после NCD TIMEOUT, резерв, реальные данные и обработку кандидатов. |
||||||
### `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()` | |
|
||||||
|
|||||||
@ -1,206 +1,212 @@ |
|||||||
/* conn_mgr_indirect.c — INDIRECT фаза + exchange-обработчики */ |
/* conn_mgr_indirect.c — согласование и проверка пути через посредников. */ |
||||||
#include "conn_mgr_priv.h" |
#include "conn_mgr_priv.h" |
||||||
#include "etcp.h" |
#include "etcp.h" |
||||||
#include <string.h> |
#include <string.h> |
||||||
#include "../lib/debug_config.h" |
#include "../lib/debug_config.h" |
||||||
#include "../lib/mem.h" |
|
||||||
#include "../lib/u_async.h" |
#include "../lib/u_async.h" |
||||||
#include "etcp_router.h" |
|
||||||
#include "utun_instance.h" |
#include "utun_instance.h" |
||||||
#include "topo_group.h" |
#include "topo_group.h" |
||||||
#include "route_connectivity.h" |
|
||||||
|
|
||||||
/* INDIRECT фаза: отправляет EXCHANGE_REQ с нашими лучшими кандидатами (best_candidates)
|
/* Оба плеча посредника должны быть прямыми READY-сессиями. RTT=0/FFFF — нет замера. */ |
||||||
* цели через BGP-маршрут. Ожидаем EXCHANGE_RESP — по RTT-замерам с обеих сторон |
static int cm_valid_candidate(struct CONN_MGR* mgr, uint64_t target, uint64_t nid, uint16_t remote_rtt) { |
||||||
* выберем общего посредника с минимальной суммой RTT (cm_compute_intermediaries). |
return nid && nid != mgr->instance->node_id && nid != target && remote_rtt && remote_rtt != UINT16_MAX && |
||||||
* Периодически ретрансмитим EXCHANGE_REQ (1с) — надёжность на случай потери пакета. */ |
cm_candidate_rtt(mgr, nid) != UINT16_MAX; |
||||||
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); |
|
||||||
} |
} |
||||||
|
|
||||||
/* Периодический тик INDIRECT: ретрансмит EXCHANGE_REQ, по дедлайну — таймаут обмена. */ |
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry) { |
||||||
void cm_exchange_tick_cb(void* arg) { |
struct CONN_MGR* mgr = entry->mgr; |
||||||
struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)arg; |
if (!entry->indirect_started) { |
||||||
entry->main.timer = NULL; |
entry->indirect_started = 1; entry->exchange_request_id = ++mgr->next_request_id; |
||||||
if (!entry->indirect_active) return; |
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT start peer=%016llx elapsed=%llums candidates=%u", |
||||||
if (get_time_tb() >= entry->indirect_deadline_tb) { |
(unsigned long long)entry->node_id, (unsigned long long)((get_time_tb() - entry->start_tb) / 10), |
||||||
entry->indirect_active = 0; |
mgr->best_candidate_count); |
||||||
{ 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;} } |
entry->indirect_next_tb = get_time_tb() + CONN_MGR_EXCHANGE_RETRANSMIT_MS * 10; |
||||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: exchange timeout for 0x%016llx", (unsigned long long)entry->node_id); |
if (entry->selection.count) { |
||||||
if (!entry->reverse_active) cm_deliver_event(entry, CONN_EVENT_TIMEOUT); |
cm_send_control(mgr, entry->node_id, &entry->selection, sizeof(entry->selection)); |
||||||
return; |
return; |
||||||
} |
} |
||||||
for (struct cm_exchange_pending* x = entry->mgr->exchange_pending; x; x = x->next) { |
struct CM_EXCHANGE_REQ req = {0}; |
||||||
if (x->entry == entry && x->pkt) { |
req.cmd = ETCP_RT_ID_CONN_MGR; req.subcmd = CM_SUBCMD_INTERM_EXCHANGE_REQ; req.request_id = entry->exchange_request_id; |
||||||
struct ll_entry* qe = queue_entry_new(0); |
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) { |
||||||
if (qe) { |
uint64_t nid = mgr->best_candidates[i].node_id; |
||||||
qe->dgram = u_malloc(x->pkt_len); |
uint16_t rtt = cm_candidate_rtt(mgr, nid); |
||||||
if (qe->dgram) { memcpy(qe->dgram, x->pkt, x->pkt_len); qe->len = (uint16_t)x->pkt_len; |
if (nid == entry->node_id || rtt == UINT16_MAX) continue; |
||||||
etcp_route_send(entry->mgr->instance, entry->mgr->group->group_id, entry->node_id, qe, 1, 0); } |
req.candidates[req.candidate_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt}; |
||||||
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; |
|
||||||
} |
|
||||||
} |
} |
||||||
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) { |
void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry) { |
||||||
entry->conn_type = CONN_TYPE_INDIRECT; |
if (!entry->intermediariy_count) { |
||||||
entry->state = CONN_MGR_STATE_CONNECTED; |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: empty intermediary selection peer=%016llx", (unsigned long long)entry->node_id); |
||||||
entry->indirect_active = 0; entry->indirect_ready = 0; |
return; |
||||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT CONNECTED 0x%016llx via %u intermediaries", |
} |
||||||
(unsigned long long)entry->node_id, entry->intermediariy_count); |
entry->indirect_confirmed = 1; |
||||||
cm_update_nodeinfo(entry); |
uint8_t old_type = entry->conn_type; |
||||||
cm_deliver_event(entry, CONN_EVENT_UP); |
cm_refresh_path(entry); // Готовый прямой путь имеет приоритет; повторного UP при улучшении нет.
|
||||||
|
if (entry->state != CONN_MGR_STATE_DISCONNECTED && entry->conn_type == old_type) cm_update_nodeinfo(entry); |
||||||
} |
} |
||||||
|
|
||||||
/* Вычисляет лучших общих посредников по данным из EXCHANGE_RESP:
|
/* Объединение кандидатов обеих сторон без дублей, выбор по сумме двух действительных RTT. */ |
||||||
* 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) { |
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; |
struct scored_candidate { uint64_t nid; uint32_t rtt; }; |
||||||
uint8_t your_n = resp->your_count > 4 ? 4 : resp->your_count; |
struct scored_candidate all[2 * CONN_MGR_MAX_CANDIDATES]; uint8_t count = 0; |
||||||
uint8_t my_n = resp->my_count > 4 ? 4 : resp->my_count; |
for (int side = 0; side < 2; side++) { |
||||||
uint64_t self = entry->mgr->instance->node_id, target = entry->node_id; |
uint8_t n = side ? resp->your_count : resp->my_count; |
||||||
for (uint8_t i=0;i<your_n&&cnt<8;i++) { |
struct CONN_MGR_CANDIDATE* candidates = side ? resp->your_candidates : resp->my_candidates; |
||||||
uint64_t nid=resp->your_candidates[i].node_id; |
for (uint8_t i = 0; i < n; i++) { |
||||||
if (nid==self||nid==target) continue; /* сам/цель не может быть посредником */ |
uint64_t nid = candidates[i].node_id; |
||||||
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;} |
if (!cm_valid_candidate(entry->mgr, entry->node_id, nid, candidates[i].rtt)) continue; |
||||||
all[cnt]=(typeof(all[0])){nid,(uint64_t)our+resp->your_candidates[i].rtt}; cnt++; |
int duplicate = 0; |
||||||
} |
for (uint8_t j = 0; j < count; j++) if (all[j].nid == nid) duplicate = 1; |
||||||
for (uint8_t i=0;i<my_n&&cnt<8;i++) { |
if (duplicate) continue; |
||||||
uint64_t nid=resp->my_candidates[i].node_id; |
all[count].nid = nid; all[count++].rtt = (uint32_t)cm_candidate_rtt(entry->mgr, nid) + candidates[i].rtt; |
||||||
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; |
for (uint8_t i = 0; i < count; i++) for (uint8_t j = i + 1; j < count; j++) { |
||||||
all[cnt]=(typeof(all[0])){nid,(uint64_t)our+resp->my_candidates[i].rtt}; cnt++; |
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; |
||||||
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; |
if (!count) { |
||||||
entry->intermediariy_count=sel; entry->rr_idx=0; |
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: no common measured READY intermediary peer=%016llx; retry pending", |
||||||
struct CM_INTERM_SEL pkt; memset(&pkt,0,sizeof(pkt)); |
(unsigned long long)entry->node_id); |
||||||
pkt.cmd=ETCP_RT_ID_CONN_MGR; pkt.subcmd=CM_SUBCMD_INTERM_SELECTED; pkt.count=sel; |
return; |
||||||
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 CM_INTERM_SEL* sel = &entry->selection; memset(sel, 0, sizeof(*sel)); |
||||||
struct ll_entry* qe=queue_entry_new(0); |
sel->cmd = ETCP_RT_ID_CONN_MGR; sel->subcmd = CM_SUBCMD_INTERM_SELECTED; sel->request_id = entry->exchange_request_id; |
||||||
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);} |
sel->count = count > CONN_MGR_MAX_INTERMEDIARIES ? CONN_MGR_MAX_INTERMEDIARIES : count; |
||||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: indirect OK for 0x%016llx via %u intermediaries",(unsigned long long)entry->node_id,sel); |
for (uint8_t i = 0; i < sel->count; i++) { |
||||||
/* reverse приоритетнее: если он ещё идёт — откладываем финализацию (indirect_ready),
|
sel->selected[i].node_id = all[i].nid; sel->selected[i].rtt = cm_candidate_rtt(entry->mgr, all[i].nid); |
||||||
* иначе финализируем INDIRECT сразу. */ |
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: intermediary peer=%016llx via=%016llx sum_rtt=%u", |
||||||
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; } |
(unsigned long long)entry->node_id, (unsigned long long)all[i].nid, all[i].rtt); |
||||||
cm_finalize_indirect(entry); |
} |
||||||
|
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) { |
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) { |
||||||
struct cm_exchange_pending* ep=(struct cm_exchange_pending*)arg; ep->timer=NULL; |
if (len != sizeof(struct CM_EXCHANGE_RESP)) { |
||||||
uint8_t need=0; uint64_t now=get_time_tb(); |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed EXCHANGE_RESP peer=%016llx len=%zu", (unsigned long long)src, len); |
||||||
for(uint8_t i=0;i<ep->cached.my_count&&i<4;i++){ |
return; |
||||||
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 / не в группе — не посредник, пропускаем */ |
struct CM_EXCHANGE_RESP resp; memcpy(&resp, data, sizeof(resp)); |
||||||
if(!cm_is_rtt_fresh(nq,now)){need++;break;} |
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src); |
||||||
} |
if (!entry || !entry->indirect_started || entry->indirect_confirmed || entry->selection.count || |
||||||
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);} |
entry->exchange_request_id != resp.request_id || get_time_tb() >= entry->deadline_tb) { |
||||||
else ep->timer=uasync_set_timeout(ep->entry->mgr->instance->ua,2000,ep,cm_exchange_probe_retry_cb,"cm_exch_probe"); |
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
|
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) { |
||||||
* до СВОИХ кандидатов (тех, что в my_candidates ответа). Если протухли — |
if (len != sizeof(struct CM_INTERM_SEL)) { |
||||||
* probe + retry-таймер 200мс. Если свежие — cm_compute_intermediaries. */ |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed INTERM_SELECTED peer=%016llx len=%zu", (unsigned long long)src, len); |
||||||
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len) { |
return; |
||||||
const struct CM_EXCHANGE_RESP* resp=(const struct CM_EXCHANGE_RESP*)data; |
} |
||||||
if(len<offsetof(struct CM_EXCHANGE_RESP,your_candidates))return; |
struct CM_INTERM_SEL sel; memcpy(&sel, data, sizeof(sel)); |
||||||
if(resp->my_count>4||resp->your_count>4)return; |
if (!sel.count || sel.count > CONN_MGR_MAX_INTERMEDIARIES) { |
||||||
if(len<offsetof(struct CM_EXCHANGE_RESP,your_candidates)+(size_t)resp->your_count*sizeof(struct CONN_MGR_CANDIDATE))return; |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid intermediary count=%u peer=%016llx", sel.count, (unsigned long long)src); return; |
||||||
struct cm_exchange_pending* ep=mgr->exchange_pending; |
} |
||||||
while(ep){if(ep->request_id==resp->request_id)break;ep=ep->next;} |
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src); |
||||||
if(!ep){DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: EXCHANGE_RESP req=%u no pending",resp->request_id);return;} |
if (!entry) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selection without exchange peer=%016llx", (unsigned long long)src); return; } |
||||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: EXCHANGE_RESP req=%u my=%u your=%u", |
if (entry->peer_exchange_request_id != sel.request_id) { |
||||||
resp->request_id, resp->my_count, resp->your_count); |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale selection peer=%016llx request=%u", (unsigned long long)src, sel.request_id); |
||||||
if(ep->entry->main.timer){uasync_cancel_timeout(mgr->instance->ua,ep->entry->main.timer);ep->entry->main.timer=NULL;} |
return; |
||||||
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(); |
if (get_time_tb() >= entry->deadline_tb && !entry->indirect_confirmed) { |
||||||
for(uint8_t i=0;i<ep->cached.my_count&&i<4;i++){ |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selection after deadline peer=%016llx", (unsigned long long)src); |
||||||
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(mgr->group,ep->cached.my_candidates[i].node_id); |
return; |
||||||
if(!nq) continue; /* self / не в группе — не посредник, пропускаем */ |
} |
||||||
if(!cm_is_rtt_fresh(nq,now)){need++;route_connectivity_probe_node(mgr->instance,mgr->group,nq);} |
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++) { |
||||||
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;} |
uint64_t nid = sel.selected[i].node_id; |
||||||
ep->timer=uasync_set_timeout(mgr->instance->ua,2000,ep,cm_exchange_probe_retry_cb,"cm_exch_probe"); |
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-
|
void cm_handle_interm_ack(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) { |
||||||
* обмена). Сохраняет выбранные intermediaries в entry, помечает CONNECTED. */ |
if (len != sizeof(struct CM_INTERM_SEL)) { |
||||||
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) { |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed INTERM_ACK peer=%016llx len=%zu", (unsigned long long)src, len); return; |
||||||
const struct CM_INTERM_SEL* sel=(const struct CM_INTERM_SEL*)data; |
} |
||||||
if(len<offsetof(struct CM_INTERM_SEL,selected))return; |
struct CM_INTERM_SEL ack; memcpy(&ack, data, sizeof(ack)); |
||||||
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); |
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src); |
||||||
if(!entry)return; |
if (!entry || entry->exchange_request_id != ack.request_id || !entry->selection.count || entry->indirect_confirmed || |
||||||
if(entry->state==CONN_MGR_STATE_CONNECTED)return; /* симметричный случай — UP уже доставлен инициаторским путём */ |
get_time_tb() >= entry->deadline_tb) { |
||||||
if(!entry->indirect_active && !entry->reverse_active)return; /* не INDIRECT/REVERSE фаза */ |
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale INTERM_ACK peer=%016llx request=%u", (unsigned long long)src, ack.request_id); |
||||||
for(uint8_t i=0;i<c;i++)entry->intermediaries[i]=sel->selected[i].node_id; |
return; |
||||||
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 (ack.count > CONN_MGR_MAX_INTERMEDIARIES) { |
||||||
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_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); |
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) { |
void cm_handle_interm_exchange_req(uint64_t src, struct CONN_MGR* mgr, struct CM_EXCHANGE_REQ* req) { |
||||||
if (!mgr) return; |
if (!mgr) return; |
||||||
if (req->candidate_count > 4) return; |
if (req->candidate_count > CONN_MGR_MAX_CANDIDATES || !topo_node_registry_find(mgr->instance->topo_groups, src)) { |
||||||
uint64_t now=get_time_tb(); |
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid EXCHANGE_REQ peer=%016llx count=%u", (unsigned long long)src, req->candidate_count); |
||||||
for(uint8_t i=0;i<req->candidate_count&&i<4;i++){ |
return; |
||||||
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 CONN_MGR_ENTRY* entry = cm_ensure_entry(mgr, src); |
||||||
} |
if (!entry) return; |
||||||
struct CM_EXCHANGE_RESP resp; memset(&resp,0,sizeof(resp)); |
entry->peer_exchange_request_id = req->request_id; |
||||||
resp.cmd=ETCP_RT_ID_CONN_MGR; resp.subcmd=CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id=req->request_id; |
if (entry->state == CONN_MGR_STATE_DISCONNECTED) { |
||||||
resp.my_count=mgr->best_candidate_count>4?4:mgr->best_candidate_count; |
entry->state = CONN_MGR_STATE_CONNECTING; entry->start_tb = get_time_tb(); |
||||||
for(uint8_t i=0;i<resp.my_count;i++)resp.my_candidates[i]=mgr->best_candidates[i]; |
entry->deadline_tb = entry->start_tb + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS * 10; |
||||||
resp.your_count=req->candidate_count>4?4:req->candidate_count; |
cm_arm_timer(entry); |
||||||
for(uint8_t i=0;i<resp.your_count;i++){ |
} |
||||||
resp.your_candidates[i].node_id=req->candidates[i].node_id; |
struct CM_EXCHANGE_RESP resp = {0}; |
||||||
struct TOPO_GROUP_NODE* nq2=topo_node_find_by_id(mgr->group,req->candidates[i].node_id); |
resp.cmd = ETCP_RT_ID_CONN_MGR; resp.subcmd = CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id = req->request_id; |
||||||
resp.your_candidates[i].rtt=nq2?topo_get_chain_rtt(nq2):0; |
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); |
||||||
size_t sz=offsetof(struct CM_EXCHANGE_RESP,your_candidates)+(size_t)resp.your_count*sizeof(struct CONN_MGR_CANDIDATE); |
if (nid == src || rtt == UINT16_MAX) continue; |
||||||
struct ll_entry* qe=queue_entry_new(0); |
resp.my_candidates[resp.my_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt}; |
||||||
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); |
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