From 111432aa6405ee5fbaa44ba39c654fd3bde9d113 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 00:42:47 +0300 Subject: [PATCH] conn_mgr: race connection probes and report call path with correct RTT --- doc/tasks.md | 43 + src/call/call.c | 71 +- src/call/call.h | 2 +- src/chat/chat_event.c | 3 +- src/chat/chat_event.h | 1 + src/routing_layer/conn_mgr.h | 18 +- src/routing_layer/conn_mgr_core.c | 865 +++++++----------- src/routing_layer/conn_mgr_doc.md | 329 +++---- src/routing_layer/conn_mgr_indirect.c | 354 +++---- src/routing_layer/conn_mgr_priv.h | 55 +- src/routing_layer/etcp_router.c | 26 +- src/routing_layer/etcp_router.h | 6 + src/routing_layer/topo_group.c | 2 + tests/test_call.c | 24 + tests/test_conn_mgr_handles.c | 2 + tests/test_conn_mgr_phases.c | 258 +++++- .../java/com/utun/chat/ChatApplication.kt | 2 +- .../java/com/utun/chat/data/CallController.kt | 14 +- .../main/java/com/utun/chat/data/CallState.kt | 9 +- .../com/utun/chat/ui/screens/CallScreen.kt | 3 + .../chat/ui/screens/IncomingCallDialog.kt | 2 + tools/chatgui/src/callwindow.cpp | 13 + tools/chatgui/src/callwindow.h | 2 + tools/chatgui/src/mainwindow.cpp | 5 + tools/chatgui/transport/gui_bridge.h | 4 + tools/chatgui/transport/gui_bridge_impl.cpp | 14 + 26 files changed, 1122 insertions(+), 1005 deletions(-) diff --git a/doc/tasks.md b/doc/tasks.md index 7658891d..1c95c826 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -3,6 +3,49 @@ Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ. ## Текущая задача +[+] **Статус подключения звонка в Qt и Android** — фактический путь router: + прямое / reverse / indirect / нет подключения. Учитывается BGP fallback; + события только при изменении, общий таймер 1с, сброс при завершении. + Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check. + +[+] **Единицы RTT в окне звонка** — `call_post_stats()` переводит + `ETCP_CONN.rtt_last` из 0.1мс в миллисекунды до отправки CALL_STATS. + Исправление общее для Qt и Android. DEBUG-категория `debug` раз в секунду + показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио. + +[ ] **Восстановление путей conn_mgr на протяжении жизни handle** — план: + разделить начальный TIMEOUT и срок отдельного цикла проб. После потери прямого + пути сразу использовать живой резерв, запустить новый DIRECT/LOCAL/REVERSE; + при отсутствии пригодного посредника сразу повторить INDIRECT. Доставку через + рабочего посредника сохранять до READY прямого пути. Пока есть handles и нет + прямого READY, повторять ограниченные циклы с паузой 1→2→4→8→15с; новая версия + адресов или изменение локальных сокетов запускает пробу без ожидания этой паузы. + NCD удерживать, создавать заново только после CLOSED; существующие UDP/TCP-пробы + не дублировать и не отменять. Рабочие посредники и router-очереди не сбрасывать. + Новые request_id каждого обмена, идемпотентные повторы, отдельный срок обработки + входящего обмена; старые ответы не меняют новый выбор. UP/DOWN — только изменение + общей доступности, TIMEOUT — один раз при неудаче первого подключения. + Закрытие последнего handle отменяет восстановление. Проверить поздний обрыв, + несколько циклов, смену адресов, потерю посредника, устаревшие ответы и close. + +[ ] **Паузы аудио Qt при звонке с SM** — 01.10.2026, звонок + `939a3ea36f3eeac3`, прямой LAN TCP `192.168.40.250:44604 ↔ 192.168.40.247:44173`. + Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем + 100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно `CallAudio: cb` + падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill. + В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall; + причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов + и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить + сетевые задержки после подключения ПК к 5ГГц или кабелю. + +[ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только + существование файла, а `wf()` создаёт его до записи. Читатель иногда получает + пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно. + +[ ] **Некорректное время в выводе check-proxy** — успешные `basic_1mb` и + `stress_sessions` иногда печатают огромные положительные/отрицательные миллисекунды, + хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода. + [+] **SpeexDSP AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка. Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`, обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних diff --git a/src/call/call.c b/src/call/call.c index 12ea5e3a..2c5f4c4b 100644 --- a/src/call/call.c +++ b/src/call/call.c @@ -59,7 +59,9 @@ struct call_session { uint8_t no_route_strikes; /* caller: подряд ретрансмитов без маршрута (fail-fast) */ void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */ void* teardown_timer; /* отложенный разрыв conn_mgr (500мс после завершения) */ - void* stats_timer; /* ACTIVE: раз в 1с — CALL_STATS (rtt/buffer/tempo) */ + void* stats_timer; /* Раз в 1с — путь подключения; в ACTIVE также CALL_STATS. */ + uint8_t connection_reported, connection_type; + uint64_t connection_next_hop; uint64_t last_media_tb; /* время последнего CALL_MEDIA (0.1ms) */ uint16_t tx_media_seq; /* seq следующего исходящего CALL_MEDIA */ struct ll_queue* tx_q; /* FIFO закодированных CALL_MEDIA (backpressure) */ @@ -255,10 +257,12 @@ static void call_post_bgp_path(struct call_ctx* ctx, struct call_session* s) { char text[512]; int off = 0; - if (hc <= 1) { - off = snprintf(text, sizeof(text), "маршрут: прямой"); + if (!hc) { + off = snprintf(text, sizeof(text), "BGP: нет маршрута"); + } else if (hc == 1) { + off = snprintf(text, sizeof(text), "BGP: прямой"); } else { - off = snprintf(text, sizeof(text), "маршрут: через "); + off = snprintf(text, sizeof(text), "BGP: через "); for (int i = hc - 1; i >= 1 && off < (int)sizeof(text) - 48; i--) { char hop[48]; struct TOPO_NODE* ni = topo_node_registry_find(ctx->inst->topo_groups, hops[i]); @@ -283,6 +287,31 @@ static void call_post_bgp_path(struct call_ctx* ctx, struct call_session* s) { CALL_ID, (unsigned long long)s->call_id, hc, cr, text); } +/* Тот же выбор next hop, что для CALL_MEDIA, включая BGP fallback до готовности CM. */ +static void call_post_connection(struct call_session* s) { + struct ETCP_ROUTER_PATH path = {0}; + if (s->state != CALL_ST_ENDED) + path = etcp_router_get_path(s->ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL); + if (s->connection_reported && s->connection_type == path.type && s->connection_next_hop == path.next_hop_node_id) return; + s->connection_reported = 1; s->connection_type = path.type; s->connection_next_hop = path.next_hop_node_id; + uint8_t buf[17]; + memcpy(buf, &s->call_id, 8); buf[8] = path.type; memcpy(buf + 9, &path.next_hop_node_id, 8); + static const char* names[] = { "NONE", "DIRECT", "REVERSE", "INDIRECT" }; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: connection id=%016llx peer=%016llx type=%s via=%016llx", + CALL_ID, (unsigned long long)s->call_id, (unsigned long long)s->peer_node_id, names[path.type], + (unsigned long long)path.next_hop_node_id); + chat_event_post(s->ctx->inst, CHAT_EVT_CALL_CONNECTION, buf, sizeof(buf)); + if (s->state != CALL_ST_ENDED) call_post_bgp_path(s->ctx, s); +} + +static void call_watch_connection(struct call_session* s) { + call_post_connection(s); + if (s->state == CALL_ST_ENDED || s->stats_timer) return; + s->stats_timer = uasync_set_timeout(s->ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats"); + if (!s->stats_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: connection timer failed id=%016llx", + CALL_ID, (unsigned long long)s->call_id); +} + /* ─────────────────── завершение ─────────────────── */ /* Отложенный разрыв: единственная точка, где закрывается conn_mgr (после того, @@ -313,6 +342,7 @@ static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reaso s->role == CALL_ROLE_CALLER ? "caller" : "callee", s->c_rx_frames, s->c_tx_frames, s->c_tx_dropped); + call_post_connection(s); call_event_id(ctx, CHAT_EVT_CALL_ENDED, s->call_id, reason); /* Закрытие запроса CM откладываем на 500мс для отправки финального сигналинга. */ @@ -335,11 +365,11 @@ static void call_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g if (event == CONN_EVENT_UP) { DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: CM path ready id=%016llx peer=%016llx", CALL_ID, (unsigned long long)s->call_id, (unsigned long long)node_id); - if (s->state == CALL_ST_ACTIVE) call_post_bgp_path(s->ctx, s); } else { DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: CM event=%d id=%016llx peer=%016llx; delivery continues via router", CALL_ID, event, (unsigned long long)s->call_id, (unsigned long long)node_id); } + call_watch_connection(s); (void)h; (void)group_id; } @@ -357,9 +387,8 @@ static void call_activate(struct call_session* s) { DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: accepted id=%016llx -> ACTIVE (router delivery)", CALL_ID, (unsigned long long)s->call_id); call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0); - call_post_bgp_path(ctx, s); + call_watch_connection(s); s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic"); - s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats"); } /* ─────────────────── таймеры ─────────────────── */ @@ -446,12 +475,8 @@ static void call_traffic_timeout_cb(void* arg) { } /* Периодическое событие CALL_STATS (раз в 1с): rtt_ms, buffer_ms, tempo_x100. */ -static void call_stats_timer_cb(void* arg) { - struct call_session* s = (struct call_session*)arg; - if (!s || !s->ctx) return; +static void call_post_stats(struct call_session* s) { struct call_ctx* ctx = s->ctx; - s->stats_timer = NULL; - if (s->state != CALL_ST_ACTIVE) return; int buffer_ms = 0, tempo_x100 = 100; struct call_jitter_range range = {0, 0, 60}; @@ -459,10 +484,15 @@ static void call_stats_timer_cb(void* arg) { if (ctx->audio_ops && ctx->audio_ops->get_stats) ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns, &range); - uint16_t rtt = 0; + uint16_t rtt_tb = 0; struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL); - if (rconn && rconn->recv_conn) rtt = rconn->recv_conn->rtt_last; + if (rconn && rconn->recv_conn) rtt_tb = rconn->recv_conn->rtt_last; + uint16_t rtt = rtt_tb / 10; /* ETCP хранит RTT в 0.1мс; CALL_STATS передаёт миллисекунды. */ + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: stats id=%016llx recv_peer=%016llx rtt_tb=%u rtt_ms=%u buffer=%dms tempo=%d dropped=%u underruns=%u", + CALL_ID, (unsigned long long)s->call_id, + (unsigned long long)(rconn && rconn->recv_conn ? rconn->recv_conn->peer_node_id : 0), + rtt_tb, rtt, buffer_ms, tempo_x100, dropped, underruns); uint8_t buf[24]; size_t off = 0; @@ -482,8 +512,15 @@ static void call_stats_timer_cb(void* arg) { buf[off++] = (uint8_t)(value >> 8); buf[off++] = (uint8_t)value; } chat_event_post(ctx->inst, CHAT_EVT_CALL_STATS, buf, (int)off); +} - s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats"); +static void call_stats_timer_cb(void* arg) { + struct call_session* s = arg; + if (!s || !s->ctx) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid stats timer session", CALL_ID); return; } + s->stats_timer = NULL; + if (s->state == CALL_ST_ENDED) return; + if (s->state == CALL_ST_ACTIVE) call_post_stats(s); + call_watch_connection(s); } /* ─────────────────── обработчики приёма ─────────────────── */ @@ -546,7 +583,7 @@ static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t gro (unsigned long long)group, (unsigned long long)inv_group); call_event_incoming(ctx, group, from, call_id); - call_post_bgp_path(ctx, s); + call_watch_connection(s); /* watchdog: вызывающий шлёт кодограммы INVITE каждые 300мс; 2с тишины → вызов сорван */ s->ring_timer = uasync_set_timeout(ctx->inst->ua, CALL_RING_DEAD_TB, s, call_ring_dead_cb, "call_ring_dead"); @@ -884,7 +921,7 @@ void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_nod call_end(ctx, s, CALL_REASON_CONNECT_FAIL); return; } - call_post_bgp_path(ctx, s); + call_watch_connection(s); s->ring_timer = uasync_set_timeout(inst->ua, CALL_RING_TIMEOUT_TB, s, call_ring_timeout_cb, "call_ring"); s->invite_timer = uasync_set_timeout(inst->ua, CALL_INVITE_RETRY_TB, s, call_invite_retry_cb, "call_invite_retry"); } diff --git a/src/call/call.h b/src/call/call.h index fc4b2635..6a0f8ad4 100644 --- a/src/call/call.h +++ b/src/call/call.h @@ -3,7 +3,7 @@ // Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из // CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core). // Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение -// устанавливается через conn_mgr (3-фазное DIRECT→REVERSE→INDIRECT). +// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT). // // Все функции — в uasync-потоке. GUI/headless вызывают через trampoline // (gui_bridge_post_uasync_fn / uasync_post). diff --git a/src/chat/chat_event.c b/src/chat/chat_event.c index 22b3e3a6..4ecceabb 100644 --- a/src/chat/chat_event.c +++ b/src/chat/chat_event.c @@ -46,8 +46,9 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, [37]="CALL_ENDED", [38]="CALL_DECLINED", [39]="CALL_STATS", [40]="CALL_ERROR", [41]="DM_MESSAGES", [42]="CALL_PATH", [43]="RADIO_TALK", + [44]="CALL_CONNECTION", }; - const char* n = (type >= 1 && type <= 43) ? names[type] : "?"; + const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : "?"; DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len); if (data && len > 0) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len); } diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 96750916..163b7b06 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -64,6 +64,7 @@ extern "C" { #define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][count:2][msg:var]* — расшифрованные сообщения беседы */ #define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */ #define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */ +#define CHAT_EVT_CALL_CONNECTION 44 /* [call_id:8][CONN_TYPE:1][next_hop_node_id:8] — текущий путь отправки звонка */ typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); diff --git a/src/routing_layer/conn_mgr.h b/src/routing_layer/conn_mgr.h index dc5f2b8d..8f0e9b8a 100644 --- a/src/routing_layer/conn_mgr.h +++ b/src/routing_layer/conn_mgr.h @@ -8,9 +8,9 @@ * - не является NCD handle и не привязан временем жизни к одному ETCP_CONN. * * NCD обеспечивает общий прямой мультиадресный транспорт для BGP/topo_group. - * NCD handle попытки CM после установления связи должен перейти группе либо быть закрыт; + * CM удерживает свой NCD handle для позднего UP и мониторинга прямого пути; + * группа приобретает независимое NCD-владение при согласовании своей сессии. * CM handle остаётся у медиа-сервиса. Закрытие CM handle отменяет его запрос, а не владение группы. - * Это контракт разделения ролей; механизм передачи транспорта группе относится к NCD/topo_group. * Групповые сессии открывают через topo_group_peer_open(). Все операции — в потоке uasync. */ #ifndef CONN_MGR_H #define CONN_MGR_H @@ -58,7 +58,8 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry /* ═══════ единая точка входа ═══════ */ /** - * Единственная публичная функция подключения — 3-фазное (DIRECT→REVERSE→INDIRECT). + * Параллельные пробы: DIRECT сразу, REVERSE через 0/500мс, INDIRECT через 0/1000мс от общего старта. + * Посредник используется сразу после проверки; поздний прямой путь заменяет его без повторного UP. * Возврат 0 — запрос принят, -1 — ошибка запуска. cb обязателен. * Ошибка до запуска может возвращаться без callback. В callback h может быть NULL. * Успешно выданный handle закрывает вызывающий, в том числе после TIMEOUT. @@ -77,7 +78,7 @@ void conn_mgr_close(struct CONN_MGR_HANDLE* h); /* ═══════ доступ к соединению ═══════ */ -/** Заимствованный ETCP_CONN от NCD, если он есть; NULL для INDIRECT или отсоединённого handle. */ +/** Заимствованный READY прямой ETCP_CONN; NULL для INDIRECT, ожидания или отсоединённого handle. */ struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h); /** Передать e->dgram через маршрутизатор CM. Освобождает entry при ненулевом h; @@ -89,6 +90,15 @@ int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e); /** Обновить RTT кандидата-посредника (≤3, сортировка по возрастанию). */ void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uint16_t rtt); +/** Запланировать перепроверку путей после изменения маршрутов или READY групповых сессий. */ +void conn_mgr_routes_changed(struct CONN_MGR* mgr); + +/** Заимствованный next hop: READY прямой путь/подтверждённый посредник; allow_probe — только для управляющего обмена CM. */ +struct ETCP_CONN* conn_mgr_route_conn(struct CONN_MGR* mgr, uint64_t node_id, int allow_probe); + +/** Тип живого транспорта к node_id: исходящий DIRECT, только входящие REVERSE, другой next hop INDIRECT. */ +uint8_t conn_mgr_connection_type(const struct ETCP_CONN* conn, uint64_t node_id); + #ifdef __cplusplus } #endif diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index da7b3aa7..6af20d37 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/src/routing_layer/conn_mgr_core.c @@ -31,14 +31,19 @@ /* ═══════ forward-декларации ═══════ */ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); -static void cm_start_parallel(struct CONN_MGR_ENTRY* entry); +static void cm_attempt_tick(void* arg); + +static const char* cm_path_name(uint8_t type) { + static const char* names[] = { "NONE", "DIRECT", "REVERSE", "INDIRECT" }; + return type <= CONN_TYPE_INDIRECT ? names[type] : "INVALID"; +} /* ═══════ утилиты ═══════ */ static struct CONN_MGR_HANDLE* cm_handle_new(struct CONN_MGR_ENTRY* entry, uint64_t group_id, conn_mgr_cb_t cb, void* cb_arg) { struct CONN_MGR_HANDLE* h = u_calloc(1, sizeof(*h)); - if (!h) return NULL; + if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: handle allocation failed"); return NULL; } h->node_id = entry->node_id; h->group_id = group_id; h->cb = cb; h->cb_arg = cb_arg; h->ua = entry->mgr->instance->ua; h->entry = entry; h->next = entry->handles; entry->handles = h; @@ -90,79 +95,17 @@ uint8_t cm_classify_v6_addr(const uint8_t addr[16]) { return CM_V6_DIR; } -uint8_t cm_sock_v6_classify(const struct ETCP_SOCKET* s) { - if (s->local_addr.ss_family != AF_INET6) return CM_V6_OTH; - const uint8_t* a6 = ((const struct sockaddr_in6*)&s->local_addr)->sin6_addr.s6_addr; - static const uint8_t z[16]; - if (!memcmp(a6, z, 16)) return CM_V6_ANY; - return cm_classify_v6_addr(a6); -} - -static void cm_warn_addr_busy(struct ETCP_CONN* conn, struct ETCP_SOCKET* s, struct sockaddr_storage* sa) { - struct ETCP_LINK* st = etcp_link_find_by_addr(s, sa, 0); - if (st && st->etcp != conn) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: addr %s busy by [%s] state=%d init=%d up=%d (adding for [%s] state=%d)", - sockaddr_storage_to_str(sa).str, - st->etcp->log_name, st->etcp->state, st->etcp->initialized, st->etcp->links_up, - conn->log_name, conn->state); - } -} - -void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s) { - struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; - memcpy(&sin.sin_addr.s_addr, addr, 4); sin.sin_port = port; - struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin)); - cm_warn_addr_busy(conn, s, &sa); - etcp_link_new(conn, s, &sa, 0); -} - -void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s) { - struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; - memcpy(&sin6.sin6_addr, addr, 16); sin6.sin6_port = htons(port); - if (cm_classify_v6_addr(addr) == CM_V6_LL) sin6.sin6_scope_id = s->netif_index; - struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6)); - cm_warn_addr_busy(conn, s, &sa); - etcp_link_new(conn, s, &sa, 0); -} - -/* Проверяет есть ли у узла прямые IP (public/EIM/DIRECT на NAT/INTERFACE адресах) — - * можем ли мы достучаться до узла напрямую. Определяет, пойдём в REVERSE или INDIRECT. */ +/* Та же классификация, что при создании линков; EIM и прямой IPv6 доступны извне. */ int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq) { - if (!nq) return 0; - struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id); + struct TOPO_NODE* ni = nq ? topo_node_registry_find(mgr->instance->topo_groups, nq->node_id) : NULL; if (!ni) return 0; - const uint8_t pub_conf[2] = {CFG_SERVER_TYPE_PUBLIC, CFG_SERVER_TYPE_UNKNOWN}; for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { - if (a->type != TOPO_ADDR_NAT && a->type != TOPO_ADDR_INTERFACE) continue; + if (!(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) continue; for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) - if (m->id == a->socket_id && (m->config_type == pub_conf[0] || m->config_type == pub_conf[1] - || m->nat_type == NAT_VERIFIED_EIM || m->nat_type == NAT_VERIFIED_DIRECT)) return 1; - } - for (const struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) { - if ((a6->type != TOPO_ADDR_NAT && a6->type != TOPO_ADDR_INTERFACE) || cm_classify_v6_addr(a6->addr) != CM_V6_DIR) continue; - for (const struct TOPO_SOCKMETA6* m = ni->v6_sock_meta; m; m = m->next) - if (m->id == a6->socket_id && (m->config_type == pub_conf[0] || m->config_type == pub_conf[1] - || m->nat_type == NAT_VERIFIED_EIM || m->nat_type == NAT_VERIFIED_DIRECT)) return 1; - } - return 0; -} - -int cm_has_local_addr(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq) { - if (!nq) return 0; - struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id); - if (!ni) return 0; - for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { - if (a->type != TOPO_ADDR_INTERFACE) continue; - for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) - if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PRIV) return 1; - } - for (const struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) { - if (a6->type != TOPO_ADDR_INTERFACE) continue; - uint8_t cls = cm_classify_v6_addr(a6->addr); - if (cls != CM_V6_LL && cls != CM_V6_LOC) continue; - for (const struct TOPO_SOCKMETA6* m = ni->v6_sock_meta; m; m = m->next) - if (m->id == a6->socket_id && (m->config_type == CFG_SERVER_TYPE_PRIVATE || m->config_type == CFG_SERVER_TYPE_LOCAL)) return 1; + if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PUB) return 1; } + for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) + if ((a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP)) && cm_classify_v6_addr(a->addr) == CM_V6_DIR) return 1; return 0; } @@ -182,7 +125,10 @@ struct CONN_MGR_ENTRY* cm_ensure_entry(struct CONN_MGR* mgr, uint64_t node_id) { struct CONN_MGR_ENTRY* new_e = (struct CONN_MGR_ENTRY*)qe; memset((uint8_t*)new_e + sizeof(struct ll_entry), 0, sizeof(*new_e) - sizeof(struct ll_entry)); new_e->node_id = node_id; new_e->mgr = mgr; - queue_data_put_with_index(mgr->entries, qe); + if (queue_data_put_with_index(mgr->entries, qe) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: entry insertion failed peer=%016llx", (unsigned long long)node_id); + queue_entry_free(qe); return NULL; + } return new_e; } @@ -191,42 +137,23 @@ static void cm_handle_free_cb(void* arg) { u_free(arg); } void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) { if (!entry) return; - // Сначала исключаем из поиска и запрещаем NCD callbacks; освобождение памяти — вне текущего стека. struct CONN_MGR* mgr = entry->mgr; if (cm_find_entry(mgr, entry->node_id) != entry) return; entry->state = CONN_MGR_STATE_DISCONNECTED; queue_remove_data(mgr->entries, &entry->ll); + if (entry->timer) { uasync_cancel_timeout(mgr->instance->ua, entry->timer); entry->timer = NULL; } + if (entry->refresh_token) { uasync_call_soon_cancel(mgr->instance->ua, entry->refresh_token); entry->refresh_token = NULL; } for (struct CONN_MGR_HANDLE* h = entry->handles; h; h = h->next) { - if (h->deliver_up_token) { - uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL; - } - h->entry = NULL; // handle остаётся у владельца до close - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: detached handle=%p node=%016llx", (void*)h, - (unsigned long long)h->node_id); + if (h->deliver_up_token) { uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL; } + h->entry = NULL; // Владелец обязан закрыть отсоединённый handle. } entry->handles = NULL; - if (entry->main.timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->main.timer); entry->main.timer = NULL; } - if (entry->reverse_timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->reverse_timer); entry->reverse_timer = NULL; } - entry->reverse_active = 0; entry->indirect_active = 0; entry->indirect_ready = 0; - { struct cm_reverse_pending** pp = &entry->mgr->reverse_pending; - while (*pp) { - if ((*pp)->entry == entry) { struct cm_reverse_pending* rp = *pp; *pp = rp->next; if (rp->pkt) u_free(rp->pkt); u_free(rp); } - else pp = &(*pp)->next; - } - } - { struct cm_exchange_pending** pp = &entry->mgr->exchange_pending; - while (*pp) { - if ((*pp)->entry == entry) { - if ((*pp)->timer && (*pp)->timer != entry->main.timer) - { uasync_cancel_timeout(entry->mgr->instance->ua, (*pp)->timer); (*pp)->timer = NULL; } - struct cm_exchange_pending* ep = *pp; *pp = ep->next; if (ep->pkt) u_free(ep->pkt); u_free(ep); - } else pp = &(*pp)->next; - } - } struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL; if (ncd) node_conn_direct_force_close(ncd); - cm_clear_nodeinfo(entry->mgr, entry->node_id); - entry->state = CONN_MGR_STATE_DISCONNECTED; entry->conn_type = CONN_TYPE_NONE; + cm_clear_nodeinfo(mgr, entry->node_id); + cm_cleanup_db_node(entry); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: cleanup group=%016llx peer=%016llx", + (unsigned long long)mgr->group->group_id, (unsigned long long)entry->node_id); uasync_call_soon(mgr->instance->ua, entry, cm_entry_free_cb); } @@ -262,86 +189,195 @@ void cm_cleanup_db_node(struct CONN_MGR_ENTRY* entry) { topo_nodeq_remove_node(group, nq); } -/* ═══════ NCD коллбэки ═══════ */ - -/* NCD (node conn direct) события → conn_mgr для DIRECT/REVERSE фаз: - * UP — соединение поднялось, помечаем CONNECTED, доставляем UP всем handle'ам. - * TIMEOUT — NCD таймаут истёк, переходим к REVERSE или INDIRECT, либо фейлим db_node. - * DOWN — соединение упало, доставляем DOWN. */ -void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void* arg) { - struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)arg; - if (!entry || !entry->mgr || entry->state == CONN_MGR_STATE_DISCONNECTED) return; - switch (ncd_ev) { - case NCD_EVENT_UP: - entry->ncd_down_notified = 0; - if (entry->state == CONN_MGR_STATE_CONNECTED) return; - { int won = entry->reverse_active ? CONN_TYPE_REVERSE : CONN_TYPE_DIRECT; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: ETCP handshake OK 0x%016llx — %s (reverse_active=%d indirect_active=%d)", - (unsigned long long)entry->node_id, - won == CONN_TYPE_REVERSE ? "REVERSE WON (indirect cancelled)" : "DIRECT", - entry->reverse_active, entry->indirect_active); } - if (entry->main.timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->main.timer); entry->main.timer = NULL; } - if (entry->reverse_timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->reverse_timer); entry->reverse_timer = NULL; } - entry->main_connect_state = CM_TRY_OK; - entry->conn_type = entry->reverse_active ? CONN_TYPE_REVERSE : CONN_TYPE_DIRECT; - entry->state = CONN_MGR_STATE_CONNECTED; - entry->reverse_active = 0; entry->indirect_active = 0; entry->indirect_ready = 0; - { struct cm_reverse_pending** pp = &entry->mgr->reverse_pending; - while (*pp) { if ((*pp)->entry == entry) { struct cm_reverse_pending* rp = *pp; *pp = rp->next; if (rp->pkt) u_free(rp->pkt); u_free(rp); break; } pp = &(*pp)->next; } - } - { 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; } - } - cm_update_nodeinfo(entry); - { struct ETCP_CONN* c = node_conn_direct_get_conn(ncd_h); - if (c && entry->mgr && entry->mgr->group) topo_group_new_conn(entry->mgr->group, c); } - cm_deliver_event(entry, CONN_EVENT_UP); - break; - case NCD_EVENT_TIMEOUT: - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: ETCP connect TIMEOUT to 0x%016llx — entry=%p mgr=%p db_loaded=%d", - (unsigned long long)entry->node_id, entry, (void*)entry->mgr, entry->db_loaded); - if (entry->db_loaded) { - entry->main_connect_state = CM_TRY_FAILED; - { struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL; - if (ncd) node_conn_direct_force_close(ncd); } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: TIMEOUT after force_close, calling cleanup"); - cm_cleanup_db_node(entry); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: TIMEOUT after cleanup, calling deliver"); - cm_deliver_event(entry, CONN_EVENT_TIMEOUT); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: TIMEOUT done"); - return; +/* Прямой транспорт пригоден только вместе с READY-сессией этой группы. */ +struct ETCP_CONN* cm_direct_conn(struct CONN_MGR* mgr, uint64_t node_id) { + struct ETCP_CONN* conn = topo_group_find_conn_for_node(mgr->group, node_id); + return conn && conn->peer_node_id == node_id && conn->links_up && !conn->close_requested && + topo_group_peer_ready(mgr->group, node_id) ? conn : NULL; +} + +/* READY первого плеча недостаточно: посредник должен ещё объявлять маршрут к получателю. */ +static struct ETCP_CONN* cm_intermediary_conn(struct CONN_MGR_ENTRY* entry, uint64_t node_id) { + struct ETCP_CONN* conn = cm_direct_conn(entry->mgr, node_id); + struct TOPO_GROUP_NODE* target = topo_node_find_by_id(entry->mgr->group, entry->node_id); + for (struct ll_entry* e = conn && target && target->paths ? target->paths->head : NULL; e; e = e->next) + if (((struct TOPO_NODEPATH*)e)->conn == conn) return conn; + return NULL; +} + +uint16_t cm_candidate_rtt(struct CONN_MGR* mgr, uint64_t node_id) { + struct ETCP_CONN* conn = cm_direct_conn(mgr, node_id); + if (!conn) return UINT16_MAX; + uint16_t rtt = conn->rtt_last; + if (!rtt) { + struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(mgr->group, node_id); + // Нулевой текущий RTT на loopback допустим, если успешный замер уже был. + if (conn->last_rtt_cb_time) return 1; + if (nq && cm_is_rtt_fresh(nq, get_time_tb())) { + rtt = nq->connectivity.real_min_rtt; + if (!rtt) rtt = nq->connectivity.nat_min_rtt; + if (!rtt) rtt = nq->connectivity.interface_min_rtt; } + if (!rtt) DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: READY candidate lacks RTT peer=%016llx", + (unsigned long long)node_id); + } + return rtt && rtt != UINT16_MAX ? rtt : UINT16_MAX; +} + +struct ETCP_CONN* conn_mgr_route_conn(struct CONN_MGR* mgr, uint64_t node_id, int allow_probe) { + struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, node_id); + if (!entry || entry->state == CONN_MGR_STATE_DISCONNECTED) return NULL; + struct ETCP_CONN* direct = cm_direct_conn(mgr, node_id); + if (direct) return direct; + for (uint8_t i = 0; entry->indirect_confirmed && i < entry->intermediariy_count; i++) { + struct ETCP_CONN* conn = cm_intermediary_conn(entry, entry->intermediaries[i]); + if (conn) return conn; + } + // Пока нет ACK, через этот путь идёт проверка; сервис ещё не получил UP. + for (uint8_t i = 0; allow_probe && i < entry->selection.count; i++) { + struct ETCP_CONN* conn = cm_intermediary_conn(entry, entry->selection.selected[i].node_id); + if (conn) return conn; + } + return NULL; +} + +/* Исходящие и входящие линки могут сосуществовать; тип не зависит от порядка их создания. */ +uint8_t conn_mgr_connection_type(const struct ETCP_CONN* conn, uint64_t node_id) { + if (!conn || !conn->links_up || conn->close_requested || conn->state == 2) return CONN_TYPE_NONE; + if (conn->peer_node_id != node_id) return CONN_TYPE_INDIRECT; + int incoming = 0; + for (const struct ETCP_LINK* link = conn->links; link; link = link->next) { + if (!link->initialized || !link->link_status) continue; + if (!link->is_server) return CONN_TYPE_DIRECT; + incoming = 1; + } + return incoming ? CONN_TYPE_REVERSE : CONN_TYPE_NONE; +} + +/* Смена пути не сбрасывает router-очереди и не повторяет UP при улучшении. */ +void cm_refresh_path(struct CONN_MGR_ENTRY* entry) { + if (entry->state == CONN_MGR_STATE_DISCONNECTED) return; + uint8_t type = CONN_TYPE_NONE; + struct ETCP_CONN* direct = cm_direct_conn(entry->mgr, entry->node_id); + if (direct) type = conn_mgr_connection_type(direct, entry->node_id); + else if (entry->indirect_confirmed) { + for (uint8_t i = 0; i < entry->intermediariy_count; i++) + if (cm_intermediary_conn(entry, entry->intermediaries[i])) { type = CONN_TYPE_INDIRECT; break; } + } + int was_up = entry->state == CONN_MGR_STATE_CONNECTED; + if (type == entry->conn_type && was_up == (type != CONN_TYPE_NONE)) return; + struct ETCP_CONN* next = conn_mgr_route_conn(entry->mgr, entry->node_id, 0); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: path group=%016llx peer=%016llx %s->%s via=%016llx elapsed=%llums", + (unsigned long long)entry->mgr->group->group_id, (unsigned long long)entry->node_id, + cm_path_name(entry->conn_type), cm_path_name(type), (unsigned long long)(next ? next->peer_node_id : 0), + (unsigned long long)((get_time_tb() - entry->start_tb) / 10)); + entry->conn_type = type; + entry->state = type ? CONN_MGR_STATE_CONNECTED : CONN_MGR_STATE_CONNECTING; + cm_update_nodeinfo(entry); + if (entry->state == CONN_MGR_STATE_DISCONNECTED) return; + if (!was_up && type) { entry->ever_up = 1; cm_deliver_event(entry, CONN_EVENT_UP); } + else if (was_up && !type) cm_deliver_event(entry, CONN_EVENT_DOWN); +} + +static void cm_refresh_cb(void* arg) { + struct CONN_MGR_ENTRY* entry = arg; entry->refresh_token = NULL; + cm_refresh_path(entry); + if (entry->state != CONN_MGR_STATE_DISCONNECTED) cm_arm_timer(entry); +} + +void conn_mgr_routes_changed(struct CONN_MGR* mgr) { + if (!mgr || !mgr->initialized) return; + // Не вызываем сервисы внутри обхода entries/изменения таблицы: callback может закрыть соседний entry. + for (struct ll_entry* e = mgr->entries->head; e; e = e->next) { + struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)e; + if (entry->refresh_token) continue; + entry->refresh_token = uasync_call_soon(mgr->instance->ua, entry, cm_refresh_cb); + if (!entry->refresh_token) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: refresh scheduling failed peer=%016llx", + (unsigned long long)entry->node_id); + } +} + +/* TIMEOUT первого NCD не закрывает транспорт: поздний UP всё ещё может улучшить путь. */ +void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ev, void* arg) { + struct CONN_MGR_ENTRY* entry = arg; + if (!entry || entry->state == CONN_MGR_STATE_DISCONNECTED) return; + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: NCD event=%d peer=%016llx path=%u elapsed=%llums", + ev, (unsigned long long)entry->node_id, entry->conn_type, + (unsigned long long)((get_time_tb() - entry->start_tb) / 10)); + if (ev == NCD_EVENT_UP) { + entry->direct_up = 1; entry->main_connect_state = CM_TRY_OK; + struct ETCP_CONN* conn = node_conn_direct_get_conn(ncd_h); + if (conn && topo_group_new_conn(entry->mgr->group, conn) < 0) + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: group negotiation unavailable peer=%016llx", + (unsigned long long)entry->node_id); + } else if (ev == NCD_EVENT_TIMEOUT) { entry->main_connect_state = CM_TRY_FAILED; - /* Entry без ожидающих handle (цель REVERSE) — эскалация бессмысленна, просто чистим */ - if (!entry->handles) { - { struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL; - if (ncd) node_conn_direct_force_close(ncd); } - cm_entry_cleanup(entry); - return; - } - /* Инициатор с handle'ами: параллельный запуск REVERSE + INDIRECT. Повторные - * NCD-таймауты (шаримый NCD оживляется другими модулями) игнорируем — иначе - * протекут таймеры и выстрелят в освобождённый entry. */ - if (entry->reverse_active || entry->indirect_active) return; - cm_start_parallel(entry); - break; - case NCD_EVENT_CLOSED: - if (entry->ncd_handle == ncd_h) entry->ncd_handle = NULL; - node_conn_direct_close(ncd_h); - if (entry->state != CONN_MGR_STATE_CONNECTED) { - cm_ncd_callback(NULL, NCD_EVENT_TIMEOUT, entry); - return; + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: direct probe timeout peer=%016llx; retaining transport path=%u", + (unsigned long long)entry->node_id, entry->conn_type); + } else { + entry->direct_up = 0; + if (ev == NCD_EVENT_CLOSED) { entry->ncd_handle = NULL; node_conn_direct_close(ncd_h); } + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: direct transport lost peer=%016llx event=%d", + (unsigned long long)entry->node_id, ev); + } + cm_refresh_path(entry); + if (entry->state != CONN_MGR_STATE_DISCONNECTED) cm_arm_timer(entry); +} + +int cm_send_control(struct CONN_MGR* mgr, uint64_t node_id, const void* packet, size_t len) { + struct ll_entry* qe = ll_alloc_lldgram(1 + len); + if (!qe) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: control allocation failed peer=%016llx", (unsigned long long)node_id); return -1; } + qe->dgram[0] = ETCP_RT_ID_CONN_MGR; memcpy(qe->dgram + 1, packet, len); qe->len = (uint16_t)(1 + len); + int ret = etcp_route_send(mgr->instance, mgr->group->group_id, node_id, qe, 1, 0); + if (ret < 0) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: control send failed peer=%016llx ret=%d", + (unsigned long long)node_id, ret); + return ret; +} + +void cm_arm_timer(struct CONN_MGR_ENTRY* entry) { + if (entry->state == CONN_MGR_STATE_DISCONNECTED || entry->timer) return; + uint64_t now = get_time_tb(), when = entry->deadline_tb; + int direct_ready = entry->conn_type == CONN_TYPE_DIRECT || entry->conn_type == CONN_TYPE_REVERSE; + if (entry->timeout_notified || (entry->handles && direct_ready && (!entry->indirect_started || entry->indirect_confirmed))) return; + if (entry->handles && !direct_ready) { + uint64_t reverse = entry->reverse_started ? entry->reverse_next_tb : entry->reverse_start_tb; + if (reverse < when) when = reverse; + } + if (entry->handles && !entry->indirect_confirmed && (!direct_ready || entry->indirect_started)) { + uint64_t indirect = entry->indirect_started ? entry->indirect_next_tb : entry->indirect_start_tb; + if (indirect < when) when = indirect; + } + entry->timer = uasync_set_timeout(entry->mgr->instance->ua, when > now ? (int)(when - now) : 1, + entry, cm_attempt_tick, "conn_mgr_attempt"); + if (!entry->timer) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: timer allocation failed peer=%016llx", + (unsigned long long)entry->node_id); +} + +static void cm_attempt_tick(void* arg) { + struct CONN_MGR_ENTRY* entry = arg; entry->timer = NULL; + cm_refresh_path(entry); + if (entry->state == CONN_MGR_STATE_DISCONNECTED) return; + uint64_t now = get_time_tb(); + int direct_ready = entry->conn_type == CONN_TYPE_DIRECT || entry->conn_type == CONN_TYPE_REVERSE; + if (entry->handles && now < entry->deadline_tb) { + if (!entry->ncd_handle && !direct_ready) cm_start_phase_direct(entry); + if (!direct_ready && now >= entry->reverse_start_tb && now >= entry->reverse_next_tb) cm_start_phase_reverse(entry); + if ((!direct_ready || entry->indirect_started) && !entry->indirect_confirmed && + now >= entry->indirect_start_tb && now >= entry->indirect_next_tb) + cm_start_phase_indirect(entry); + } + if (now >= entry->deadline_tb && !entry->timeout_notified) { + entry->timeout_notified = 1; + if (!entry->handles) { cm_entry_cleanup(entry); return; } + if (!entry->ever_up && entry->state != CONN_MGR_STATE_CONNECTED) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: all paths unavailable peer=%016llx elapsed=%llums", + (unsigned long long)entry->node_id, (unsigned long long)((now - entry->start_tb) / 10)); + cm_deliver_event(entry, CONN_EVENT_TIMEOUT); } - entry->state = CONN_MGR_STATE_FAILED; entry->conn_type = CONN_TYPE_NONE; - if (!entry->ncd_down_notified) cm_deliver_event(entry, CONN_EVENT_DOWN); - return; - case NCD_EVENT_DOWN: - entry->ncd_down_notified = 1; - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "link: ETCP link DROPPED to 0x%016llx", - (unsigned long long)entry->node_id); - cm_deliver_event(entry, CONN_EVENT_DOWN); - break; } + if (entry->state == CONN_MGR_STATE_DISCONNECTED) return; + // После начального бюджета ждём поздний NCD/READY и изменения путей через callbacks. + if (now < entry->deadline_tb) cm_arm_timer(entry); } /* ═══════ init / destroy ═══════ */ @@ -351,7 +387,6 @@ struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) { struct CONN_MGR* mgr = u_calloc(1, sizeof(struct CONN_MGR)); if (!mgr) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "alloc failed"); return NULL; } mgr->instance = group->instance; mgr->group = group; - mgr->direct_timeout_ms = CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS; mgr->entries = queue_new(mgr->instance->ua, 256, offsetof(struct CONN_MGR_ENTRY, node_id) - sizeof(struct ll_entry), 8, "conn_mgr_entries"); @@ -365,35 +400,18 @@ struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) { void conn_mgr_destroy(struct CONN_MGR* mgr) { if (!mgr) return; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 0 enter mgr=%p entries=%p exch=%p rev=%p", - mgr, mgr->entries, mgr->exchange_pending, mgr->reverse_pending); - if (mgr->bg_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer); mgr->bg_ping_timer = NULL; } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 1 bg_ping done"); - if (mgr->candidate_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer); mgr->candidate_ping_timer = NULL; } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 2 cand_ping done"); + mgr->initialized = 0; + if (mgr->bg_ping_timer) uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer); + if (mgr->candidate_ping_timer) uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer); #ifdef UTUN_HAVE_STANDBY - if (mgr->bg_ping_wait) { standby_wait_cancel(mgr->bg_ping_wait); mgr->bg_ping_wait = NULL; } - if (mgr->candidate_ping_wait) { standby_wait_cancel(mgr->candidate_ping_wait); mgr->candidate_ping_wait = NULL; } + if (mgr->bg_ping_wait) standby_wait_cancel(mgr->bg_ping_wait); + if (mgr->candidate_ping_wait) standby_wait_cancel(mgr->candidate_ping_wait); #endif - /* Досрочно закрыть влётные candidate TCP-пинги — иначе их stcp_client утечёт до uasync_destroy. */ for (int i = 0; i < CONN_MGR_MAX_CANDIDATES; i++) - if (mgr->cand_tcp_pings[i]) { stcp_client_destroy(mgr->cand_tcp_pings[i]); mgr->cand_tcp_pings[i] = NULL; } - { size_t ec = queue_entry_count(mgr->entries); - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries count=%zu", ec); - struct ll_entry* e = mgr->entries->head; - int clean = 0; - while (e) { struct ll_entry* next = e->next; struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)e; cm_entry_cleanup(entry); clean++; e = next; } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries cleaned (%d)", clean); - while (mgr->exchange_pending) { - struct cm_exchange_pending* ep = mgr->exchange_pending; mgr->exchange_pending = ep->next; - if (ep->timer) uasync_cancel_timeout(mgr->instance->ua, ep->timer); - u_free(ep); - } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 5 exchange_pending done"); - while (mgr->reverse_pending) { struct cm_reverse_pending* rp = mgr->reverse_pending; mgr->reverse_pending = rp->next; u_free(rp); } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 6 reverse_pending done"); - queue_free(mgr->entries); mgr->initialized = 0; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 7 queue_free done (was %zu)", ec); } + if (mgr->cand_tcp_pings[i]) stcp_client_destroy(mgr->cand_tcp_pings[i]); + while (mgr->entries->head) cm_entry_cleanup((struct CONN_MGR_ENTRY*)mgr->entries->head); + queue_free(mgr->entries); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: destroyed group=%016llx", (unsigned long long)mgr->group->group_id); u_free(mgr); } @@ -419,7 +437,7 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id, struct TOPO_NODE* ni = topo_node_sqlite_node_load(mgr->instance->topo_sqlite_db, mgr->instance->topo_groups, node_id); if (ni) { ni = topo_node_registry_store(mgr->instance->topo_groups, ni); - struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE)); + struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE) - sizeof(struct ll_entry)); if (qe) { target = (struct TOPO_GROUP_NODE*)qe; memset((uint8_t*)target + sizeof(struct ll_entry), 0, sizeof(*target) - sizeof(struct ll_entry)); @@ -433,48 +451,45 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id, struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, node_id); - if (entry && entry->state == CONN_MGR_STATE_CONNECTED) { + if (entry && entry->state == CONN_MGR_STATE_CONNECTED && entry->handles) { struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg); if (!h) return -1; if (out_handle) *out_handle = h; h->deliver_up_token = uasync_call_soon(mgr->instance->ua, h, cm_deliver_up_cb); return 0; } - if (entry && entry->state == CONN_MGR_STATE_CONNECTING) { + if (entry && entry->state == CONN_MGR_STATE_CONNECTING && entry->handles && !entry->timeout_notified) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "node 0x%016llx already connecting, adding handle", (unsigned long long)node_id); struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg); if (!h) return -1; if (out_handle) *out_handle = h; return 0; } - struct ETCP_CONN* existing = topo_group_find_conn_for_node(group, node_id); - if (!existing) existing = instance_find_conn(mgr->instance, node_id); - if (existing && existing->peer_node_id == node_id && existing->links) { - struct ETCP_LINK* l = existing->links; - while (l) { if (l->link_state == 3 && l->initialized) break; l = l->next; } - if (l) { - entry = cm_ensure_entry(mgr, node_id); - if (!entry) return -1; - entry->state = CONN_MGR_STATE_CONNECTED; entry->conn_type = CONN_TYPE_DIRECT; - entry->last_traffic_tb = get_time_tb(); - { struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, node_id); - struct TOPO_NODE ni_e; memset(&ni_e, 0, sizeof(ni_e)); ni_e.node_id = node_id; - if (ni) memcpy(ni_e.public_key, ni->public_key, SC_PUBKEY_SIZE); - node_conn_direct_open_node(mgr->instance, node_id, cm_ncd_callback, entry, &entry->ncd_handle, &ni_e, NULL); } - struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg); - if (!h) { cm_entry_cleanup(entry); return -1; } if (out_handle) *out_handle = h; - cm_update_nodeinfo(entry); h->deliver_up_token = uasync_call_soon(mgr->instance->ua, h, cm_deliver_up_cb); return 0; - } - } - entry = cm_ensure_entry(mgr, node_id); if (!entry) return -1; - entry->state = CONN_MGR_STATE_CONNECTING; + int already_up = entry->state == CONN_MGR_STATE_CONNECTED; + if (!already_up) entry->state = CONN_MGR_STATE_CONNECTING; struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg); if (!h) { cm_entry_cleanup(entry); return -1; } if (out_handle) *out_handle = h; - entry->last_traffic_tb = get_time_tb(); - entry->local_scan_state = CM_TRY_NONE; entry->main_connect_state = CM_TRY_NONE; - entry->db_loaded = (uint8_t)loaded_from_db; - if (cm_has_local_addr(mgr, group->local_node) && cm_has_local_addr(mgr, target)) - { entry->local_scan_state = CM_TRY_PENDING; cm_start_local_scan(entry); } - entry->main_connect_state = CM_TRY_PENDING; cm_start_phase_direct(entry); + // Первый владелец входящего пути начинает свои пробы, сохраняя рабочую доставку. + if (entry->timer) { uasync_cancel_timeout(mgr->instance->ua, entry->timer); entry->timer = NULL; } + if (already_up) h->deliver_up_token = uasync_call_soon(mgr->instance->ua, h, cm_deliver_up_cb); + entry->last_traffic_tb = entry->start_tb = get_time_tb(); + entry->timeout_notified = 0; + entry->reverse_started = 0; entry->reverse_next_tb = 0; entry->indirect_started = 0; entry->indirect_next_tb = 0; + memset(&entry->selection, 0, sizeof(entry->selection)); + entry->db_loaded |= (uint8_t)loaded_from_db; + int local_public = cm_has_direct_ip(mgr, group->local_node), peer_public = cm_has_direct_ip(mgr, target); + entry->reverse_start_tb = entry->start_tb + (local_public && !peer_public ? 0 : CONN_MGR_REVERSE_DELAY_MS * 10); + entry->indirect_start_tb = entry->start_tb + (!local_public && !peer_public ? 0 : CONN_MGR_INDIRECT_DELAY_MS * 10); + entry->deadline_tb = entry->indirect_start_tb + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS * 10; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: start group=%016llx peer=%016llx public=%d/%d reverse=%llums indirect=%llums", + (unsigned long long)gid, (unsigned long long)node_id, local_public, peer_public, + (unsigned long long)((entry->reverse_start_tb - entry->start_tb) / 10), + (unsigned long long)((entry->indirect_start_tb - entry->start_tb) / 10)); + cm_start_phase_direct(entry); + if (entry->state == CONN_MGR_STATE_DISCONNECTED) return 0; + if (!local_public && !peer_public) cm_start_local_scan(entry); + if (entry->reverse_start_tb == entry->start_tb) cm_start_phase_reverse(entry); + if (!entry->indirect_confirmed && entry->indirect_start_tb == entry->start_tb) cm_start_phase_indirect(entry); + cm_arm_timer(entry); return 0; } @@ -496,8 +511,9 @@ void conn_mgr_close(struct CONN_MGR_HANDLE* h) { } struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h) { - if (!h || h->closed || !h->entry || !h->entry->ncd_handle) return NULL; - return node_conn_direct_get_conn(h->entry->ncd_handle); + if (!h || h->closed || !h->entry || h->entry->state != CONN_MGR_STATE_CONNECTED || + h->entry->conn_type == CONN_TYPE_INDIRECT) return NULL; + return cm_direct_conn(h->entry->mgr, h->entry->node_id); } int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e) { @@ -510,325 +526,83 @@ int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e) { int ret = etcp_router_conn_send(r, e->dgram, e->len, 0); queue_entry_free(e); return ret; } -/* ═══════ локальное сканирование ═══════ */ - -static void cm_ping_cb(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len); -static void cm_ping_cb_impl(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len); - -struct cm_ping_ctx { struct CONN_MGR* mgr; uint64_t node_id; struct sockaddr_storage addr; struct ETCP_SOCKET* sock; uint8_t phase, attempt; }; - -/* LAN broadcast ping для обнаружения узла в локальной сети. Если ответит — - * соединение готово быстрее чем через интернет, без NAT. Запускается - * параллельно с DIRECT фазой. */ +/* Локальные пробы уже созданы DIRECT через NCD. Проверяем все LAN-адреса, + * не дублируя handshake и не создавая отдельные ping-контексты. */ void cm_start_local_scan(struct CONN_MGR_ENTRY* entry) { - struct TOPO_GROUP* group = entry->mgr->group; - struct TOPO_GROUP_NODE* target = topo_node_find_by_id(group, entry->node_id); - struct TOPO_NODE* ni = target ? topo_node_registry_find(entry->mgr->instance->topo_groups, target->node_id) : NULL; - if (!target || !ni) { entry->local_scan_state = CM_TRY_FAILED; return; } - for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { - if (a->type != TOPO_ADDR_INTERFACE) continue; - for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) { - if (m->id != a->socket_id) continue; - if (m->config_type != CFG_SERVER_TYPE_PRIVATE && m->config_type != CFG_SERVER_TYPE_LOCAL) continue; - struct ETCP_SOCKET* s = entry->mgr->instance->etcp_sockets; - while (s) { - if ((s->type == CFG_SERVER_TYPE_PRIVATE || s->type == CFG_SERVER_TYPE_LOCAL) && s->local_addr.ss_family == AF_INET) { - struct cm_ping_ctx* ctx = u_calloc(1, sizeof(struct cm_ping_ctx)); - if (!ctx) continue; - ctx->mgr = entry->mgr; ctx->node_id = entry->node_id; ctx->sock = s; ctx->phase = 0; ctx->attempt = 0; - struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; - memcpy(&sin.sin_addr.s_addr, a->addr, 4); sin.sin_port = htons(a->port); - memcpy(&ctx->addr, &sin, sizeof(sin)); - etcp_send_ping_to_socket(entry->mgr->instance, s, ni->public_key, &ctx->addr, CONN_MGR_LOCAL_SCAN_TIMEOUT_MS, cm_ping_cb, ctx, NULL, 0, 0); - return; - } - s = s->next; - } - } - } - entry->local_scan_state = CM_TRY_FAILED; -} - -static void cm_ping_cb(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len) -{ cm_ping_cb_impl(success, rtt, arg, nonce, resp_data, resp_data_len); } - -static void cm_ping_cb_impl(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len) { - struct cm_ping_ctx* ctx = (struct cm_ping_ctx*)arg; (void)nonce; (void)resp_data; (void)resp_data_len; - struct CONN_MGR_ENTRY* entry = cm_find_entry(ctx->mgr, ctx->node_id); - if (!entry || entry->state == CONN_MGR_STATE_DISCONNECTED) { u_free(ctx); return; } - if (entry->main_connect_state == CM_TRY_OK) { u_free(ctx); return; } - if (success && ctx->phase == 0) { - entry->local_scan_state = CM_TRY_OK; - topo_node_ping_update_rtt(entry->mgr->instance->topo_groups, entry->node_id, rtt); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: local scan OK for 0x%016llx rtt=%u", (unsigned long long)entry->node_id, rtt); - u_free(ctx); return; + struct TOPO_NODE* ni = topo_node_registry_find(entry->mgr->instance->topo_groups, entry->node_id); + if (!ni || !entry->ncd_handle) { + entry->local_scan_state = CM_TRY_FAILED; + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: LOCAL probe missing node/transport peer=%016llx", + (unsigned long long)entry->node_id); return; } - if (++ctx->attempt < CONN_MGR_LOCAL_SCAN_ATTEMPTS && ctx->phase == 0) { - struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(entry->mgr->group, entry->node_id); - struct TOPO_NODE* pub_ni = nq ? topo_node_registry_find(entry->mgr->instance->topo_groups, nq->node_id) : NULL; - etcp_send_ping_to_socket(entry->mgr->instance, ctx->sock, pub_ni ? pub_ni->public_key : NULL, - &ctx->addr, CONN_MGR_LOCAL_SCAN_TIMEOUT_MS, cm_ping_cb, ctx, NULL, 0, 0); - return; - } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: local scan failed for 0x%016llx after %d attempts", - (unsigned long long)entry->node_id, ctx->attempt); - entry->local_scan_state = CM_TRY_FAILED; u_free(ctx); -} - -/* ═══════ DIRECT фаза (NCD + ручная NAT-фильтрация линков) ═══════ */ - -/* Добавляет IPv4/IPv6 линки к conn. Единый цикл для адресов из BGP и из базы: - * протокол из a->protocol, приоритет NAT > INTERFACE, сопоставление сокетов - * через sock_match (UDP и TCP). Если ни одного линка не создалось — закрывает NCD - * и переходит к REVERSE/INDIRECT (для db_loaded — cleanup + TIMEOUT). */ -#define CM_MAX_SOCKET_VIEWS 32 -static void cm_direct_add_links(struct ETCP_CONN* conn, struct CONN_MGR_ENTRY* entry, - struct TOPO_NODE* ni, int db_loaded) { - int any = 0, v4_cnt = 0, v6_cnt = 0, v4_tcp = 0, v6_tcp = 0; - /* Плоский список наших сокетов: роль + локальный/публичный IP + тип */ - struct sock_view views[CM_MAX_SOCKET_VIEWS]; - int nviews = sock_collect_views(entry->mgr->instance, views, CM_MAX_SOCKET_VIEWS); - uint8_t prio[] = {TOPO_ADDR_NAT, TOPO_ADDR_INTERFACE}; - for (int pr = 0; pr < 2; pr++) { - for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { - if (a->type != prio[pr]) continue; - struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_family=AF_INET; - memcpy(&sin.sin_addr.s_addr,a->addr,4); sin.sin_port=htons(a->port); - struct sockaddr_storage sa; memcpy(&sa,&sin,sizeof(sin)); - uint32_t tip; memcpy(&tip, a->addr, 4); - const struct TOPO_SOCKMETA4* m = NULL; - for (const struct TOPO_SOCKMETA4* mm = ni->v4_sock_meta; mm; mm = mm->next) - if (mm->id == a->socket_id) { m = mm; break; } - uint8_t tcfg = m ? m->config_type : CFG_SERVER_TYPE_UNKNOWN; - uint8_t tnat = m ? m->nat_type : NAT_TYPE_UNKNOWN; - if (a->protocol & TOPO_PROTO_TCP) { - for (int vi = 0; vi < nviews; vi++) { - const struct sock_view* v = &views[vi]; - if (!v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue; - struct ETCP_LINK *tlink = etcp_link_new(conn, v->sock, &sa, 0); - if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); any = 1; v4_tcp++; v4_cnt++; } - } - } - if (a->protocol & TOPO_PROTO_UDP) { - for (int vi = 0; vi < nviews; vi++) { - const struct sock_view* v = &views[vi]; - if (v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue; - cm_warn_addr_busy(conn, v->sock, &sa); - if (etcp_link_new(conn, v->sock, &sa, 0)) { any = 1; v4_cnt++; } - } + int addresses = 0; + for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { + if (!(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) continue; + for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) + if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PRIV) { + addresses++; break; } - } } - /* v6 */ - for (const struct TOPO_ADDR6* a6=ni->v6_addrs;a6;a6=a6->next) { - int is_ll = (a6->addr[0] == 0xfe && (a6->addr[1] & 0xc0) == 0x80); - if (a6->protocol & TOPO_PROTO_TCP) { - struct sockaddr_in6 sin6; memset(&sin6,0,sizeof(sin6)); sin6.sin6_family=AF_INET6; - memcpy(&sin6.sin6_addr,a6->addr,16); sin6.sin6_port=htons(a6->port); - if (is_ll) { struct ETCP_SOCKET* sv=entry->mgr->instance->etcp_sockets; - while(sv){if(sv->local_addr.ss_family==AF_INET6&&sv->netif_index) - {sin6.sin6_scope_id=sv->netif_index;break;} sv=sv->next;} - } - struct sockaddr_storage sa; memcpy(&sa,&sin6,sizeof(sin6)); - { struct ETCP_SOCKET* s = entry->mgr->instance->etcp_sockets; - while (s) { if (s->is_tcp && s->local_addr.ss_family == AF_INET6 && s->type != CFG_SERVER_TYPE_PRIVATE) { - struct ETCP_LINK *tlink = etcp_link_new(conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a6->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a6->port); any = 1; v6_tcp++; v6_cnt++; } - } s = s->next; } - } - } - if (a6->protocol & TOPO_PROTO_UDP) { - uint8_t tc=cm_classify_v6_addr(a6->addr); if(tc==CM_V6_OTH) continue; - struct ETCP_SOCKET* s=entry->mgr->instance->etcp_sockets; - while(s){uint8_t sc=cm_sock_v6_classify(s);if(sc==CM_V6_ANY||sc==tc){cm_add_v6_link(conn,a6->addr,a6->port,s);any=1; v6_cnt++;} s=s->next;} - } + for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) { + uint8_t cls = cm_classify_v6_addr(a->addr); + if ((a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP)) && (cls == CM_V6_LL || cls == CM_V6_LOC)) addresses++; } + entry->local_scan_state = addresses ? CM_TRY_PENDING : CM_TRY_FAILED; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: LOCAL probes peer=%016llx addresses=%d shared_with_DIRECT=1 elapsed=%llums", + (unsigned long long)entry->node_id, addresses, (unsigned long long)((get_time_tb() - entry->start_tb) / 10)); + if (!addresses) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: no advertised local addresses peer=%016llx", + (unsigned long long)entry->node_id); +} - if (any) { - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: direct_add_links node=0x%016llx db=%d v4=%d/tcp=%d v6=%d/tcp=%d conn=[%s]", - (unsigned long long)entry->node_id, db_loaded, v4_cnt, v4_tcp, v6_cnt, v6_tcp, conn->log_name); +void cm_start_phase_direct(struct CONN_MGR_ENTRY* entry) { + if (!entry->ncd_handle && node_conn_direct_open(entry->mgr->instance, entry->node_id, cm_ncd_callback, entry, + &entry->ncd_handle, NULL) == NCD_ERR) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: NCD open failed peer=%016llx", (unsigned long long)entry->node_id); return; } - node_conn_direct_close(entry->ncd_handle); entry->ncd_handle=NULL; - if (db_loaded) { - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: db_node 0x%016llx direct failed (no compatible addr/socket)", (unsigned long long)entry->node_id); - cm_cleanup_db_node(entry); cm_deliver_event(entry, CONN_EVENT_TIMEOUT); - } else { - cm_start_parallel(entry); - } + // Входящий REVERSE добавляет новые адреса, не заменяя уже идущие пробы. + if (node_conn_direct_update_node(entry->mgr->instance, entry->node_id) < 0) + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: address update failed peer=%016llx", (unsigned long long)entry->node_id); + if (!entry->direct_up) entry->main_connect_state = CM_TRY_PENDING; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: DIRECT probes peer=%016llx elapsed=%llums transport=%p", + (unsigned long long)entry->node_id, (unsigned long long)((get_time_tb() - entry->start_tb) / 10), + (void*)node_conn_direct_get_conn(entry->ncd_handle)); } -/* DIRECT фаза: открывает NCD-соединение с пустым ni (0 линков из NCD), затем - * вручную добавляет NAT-фильтрованные линки через cm_direct_add_links. - * NCD управляет таймаутом (2с). При успехе — cm_ncd_callback доставит UP, - * при таймауте — переключится на REVERSE или INDIRECT. */ -void cm_start_phase_direct(struct CONN_MGR_ENTRY* entry) { - if (entry->main_connect_state == CM_TRY_OK) return; - struct TOPO_GROUP* group = entry->mgr->group; - struct TOPO_GROUP_NODE* target = topo_node_find_by_id(group, entry->node_id); - struct TOPO_NODE* ni = target ? topo_node_registry_find(entry->mgr->instance->topo_groups, target->node_id) : NULL; - if (!target || !ni) { cm_deliver_event(entry, CONN_EVENT_TIMEOUT); return; } - - struct TOPO_NODE ni_e; memset(&ni_e, 0, sizeof(ni_e)); ni_e.node_id = target->node_id; - memcpy(ni_e.public_key, ni->public_key, SC_PUBKEY_SIZE); - int r = node_conn_direct_open_node(entry->mgr->instance, entry->node_id, cm_ncd_callback, entry, - &entry->ncd_handle, &ni_e, NULL); - if (r == NCD_ERR) { cm_deliver_event(entry, CONN_EVENT_TIMEOUT); return; } - - cm_direct_add_links(node_conn_direct_get_conn(entry->ncd_handle), entry, ni, entry->db_loaded); -} - -/* ═══════ REVERSE + INDIRECT (параллельный запуск) ═══════ */ - -/* После провала DIRECT запускаем ОБА пути параллельно: INDIRECT (гарантированный - * fallback через релей) и REVERSE (оптимизация — прямой коннект, если пир дозвонится). - * Reverse строго приоритетнее: его INIT побеждает, indirect откладывается (indirect_ready). */ -static void cm_start_parallel(struct CONN_MGR_ENTRY* entry) { - struct TOPO_GROUP* g = entry->mgr->group; - struct TOPO_GROUP_NODE* t = topo_node_find_by_id(g, entry->node_id); - int can_reverse = cm_has_direct_ip(entry->mgr, g->local_node) && !(t && cm_has_direct_ip(entry->mgr, t)); - int can_indirect = entry->mgr->best_candidate_count > 0; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: direct failed 0x%016llx — parallel start reverse=%d indirect=%d (candidates=%u)", - (unsigned long long)entry->node_id, can_reverse, can_indirect, entry->mgr->best_candidate_count); - if (can_indirect) cm_start_phase_indirect(entry); - if (can_reverse) cm_start_phase_reverse(entry); /* keep ncd_handle: target will reuse this conn for incoming INIT */ - else { struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL; if (ncd) node_conn_direct_force_close(ncd); } - if (!entry->reverse_active && !entry->indirect_active) cm_deliver_event(entry, CONN_EVENT_TIMEOUT); -} - -/* REVERSE фаза: у нас прямой IP, у цели нет. Отправляем DIRECT_REQ с НАШИМИ адресами - * цели через BGP-маршрут ("подключись ко мне"). Ждём обратного INIT (дедлайн 3с), - * периодически ретрансмитим DIRECT_REQ (1с) — надёжность на случай потери пакета. */ +/* Запрос использует подписанный снимок отправителя: одинаковые правила IPv4/IPv6, UDP/TCP и Reality с DIRECT. */ void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry) { - struct TOPO_GROUP* group = entry->mgr->group; - struct TOPO_GROUP_NODE* local = group->local_node; - struct TOPO_NODE* lni = local ? topo_node_registry_find(entry->mgr->instance->topo_groups, local->node_id) : NULL; - if (!local || !lni) return; - uint32_t req_id = ++entry->mgr->next_request_id; entry->main.request_id = req_id; - uint8_t dc = 0; struct { uint8_t t; uint8_t ip[4]; uint16_t port; uint8_t sid; } addrs[8]; - for (const struct TOPO_ADDR4* a = lni->v4_addrs; a && dc < 8; a = a->next) { - if (a->type != TOPO_ADDR_NAT && a->type != TOPO_ADDR_INTERFACE) continue; - for (const struct TOPO_SOCKMETA4* m = lni->v4_sock_meta; m; m = m->next) { - if (m->id == a->socket_id && (m->config_type == CFG_SERVER_TYPE_PUBLIC || m->config_type == CFG_SERVER_TYPE_UNKNOWN - || m->nat_type == NAT_VERIFIED_EIM || m->nat_type == NAT_VERIFIED_DIRECT)) - { addrs[dc]=(typeof(addrs[0])){a->type}; memcpy(addrs[dc].ip,a->addr,4); addrs[dc].port=htons(a->port); addrs[dc].sid=a->socket_id; dc++; break; } - } - } - if (!dc) return; - size_t sz = CM_DIRECT_REQ_H_SIZE + (size_t)dc * 8; uint8_t* pkt = u_malloc(sz); - if (!pkt) return; - struct CM_DIRECT_REQ* req = (struct CM_DIRECT_REQ*)pkt; memset(req,0,sizeof(*req)); - req->cmd=ETCP_RT_ID_CONN_MGR; req->subcmd=CM_SUBCMD_DIRECT_REQ; req->request_id=req_id; req->addr_count=dc; - uint8_t* p = pkt + CM_DIRECT_REQ_H_SIZE; - for (uint8_t i=0;idgram=u_malloc(1+sz); - if (!qe->dgram) { queue_entry_free(qe); u_free(pkt); return; } - qe->dgram[0]=ETCP_RT_ID_CONN_MGR; memcpy(qe->dgram+1,pkt,sz); qe->len=(uint16_t)(1+sz); - /* копия для ретрансмита (etcp_route_send забирает владение qe) */ - struct cm_reverse_pending* rp = u_calloc(1, sizeof(*rp)); - if (rp) { - rp->request_id=req_id; rp->entry=entry; - rp->pkt=u_malloc(1+sz); - if (rp->pkt) { memcpy(rp->pkt, qe->dgram, 1+sz); rp->pkt_len=1+sz; } - rp->next=entry->mgr->reverse_pending; entry->mgr->reverse_pending=rp; - } - etcp_route_send(entry->mgr->instance, group->group_id, entry->node_id, qe, 1, 0); - u_free(pkt); - entry->reverse_active = 1; - entry->reverse_deadline_tb = get_time_tb() + CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS*10; - entry->reverse_timer = uasync_set_timeout(entry->mgr->instance->ua, CONN_MGR_REVERSE_RETRANSMIT_MS*10, - entry, cm_reverse_tick_cb, "conn_mgr_reverse"); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: phase 2 (reverse) sent DIRECT_REQ to 0x%016llx with %u addrs", - (unsigned long long)entry->node_id, dc); -} - -/* Периодический тик REVERSE: ретрансмит DIRECT_REQ, по дедлайну — сдаёмся, закрываем - * NCD; если indirect уже завершил обмен (indirect_ready) — финализируем INDIRECT. */ -void cm_reverse_tick_cb(void* arg) { - struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)arg; - entry->reverse_timer = NULL; - if (!entry->reverse_active) return; - if (get_time_tb() >= entry->reverse_deadline_tb) { - entry->reverse_active = 0; - { struct cm_reverse_pending** pp = &entry->mgr->reverse_pending; - while (*pp) { if ((*pp)->entry == entry) { struct cm_reverse_pending* rp = *pp; *pp = rp->next; if (rp->pkt) u_free(rp->pkt); u_free(rp); break; } pp = &(*pp)->next; } - } - { struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL; - if (ncd) node_conn_direct_force_close(ncd); } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: reverse TIMEOUT 0x%016llx — indirect_ready=%d indirect_active=%d", - (unsigned long long)entry->node_id, entry->indirect_ready, entry->indirect_active); - if (entry->indirect_ready) cm_finalize_indirect(entry); - else if (!entry->indirect_active) cm_deliver_event(entry, CONN_EVENT_TIMEOUT); - return; - } - for (struct cm_reverse_pending* x = entry->mgr->reverse_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: reverse retransmit DIRECT_REQ 0x%016llx", (unsigned long long)entry->node_id); - break; - } + uint64_t now = get_time_tb(); + if (!entry->reverse_started) { + entry->reverse_started = 1; entry->reverse_request_id = ++entry->mgr->next_request_id; + entry->reverse_deadline_tb = now + CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS * 10; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: REVERSE start peer=%016llx elapsed=%llums", + (unsigned long long)entry->node_id, (unsigned long long)((now - entry->start_tb) / 10)); } - entry->reverse_timer = uasync_set_timeout(entry->mgr->instance->ua, CONN_MGR_REVERSE_RETRANSMIT_MS*10, - entry, cm_reverse_tick_cb, "conn_mgr_reverse"); + if (now >= entry->reverse_deadline_tb) { entry->reverse_next_tb = UINT64_MAX; return; } + struct CM_DIRECT_REQ req = { ETCP_RT_ID_CONN_MGR, CM_SUBCMD_DIRECT_REQ, entry->reverse_request_id }; + cm_send_control(entry->mgr, entry->node_id, &req, sizeof(req)); + entry->reverse_next_tb = now + CONN_MGR_REVERSE_RETRANSMIT_MS * 10; } -/* ═══════ обработка DIRECT_REQ (REVERSE входящие) ═══════ */ - -/* Принимающая сторона REVERSE: получили DIRECT_REQ от инициатора — открываем - * NCD-соединение к нему, добавляем линки по адресам из запроса с NAT-фильтрацией - * (sock_match). Целевая сторона может быть за NAT — поэтому линк создаём от любого - * совместимого сокета (не только public), чтобы её исходящий INIT достиг инициатора. */ void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* data, size_t len) { - if (!mgr) return; - struct CM_DIRECT_REQ* req = (struct CM_DIRECT_REQ*)data; - if (len < CM_DIRECT_REQ_H_SIZE + (size_t)req->addr_count * 8) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: DIRECT_REQ too short"); return; } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: got DIRECT_REQ from 0x%016llx with %u addrs", - (unsigned long long)src, req->addr_count); - - struct TOPO_NODE* src_ni = topo_node_registry_find(mgr->instance->topo_groups, src); - if (!src_ni) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: DIRECT_REQ from unknown node 0x%016llx", (unsigned long long)src); return; } - - /* Целевая сторона REVERSE. Если уже есть entry к инициатору (своё open идёт - * или связь уже есть) — ничего не делаем: обратный INIT сматчится с нашим conn. */ - struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src); - if (entry && entry->state == CONN_MGR_STATE_CONNECTED) return; - if (entry && entry->state == CONN_MGR_STATE_CONNECTING) return; - if (!entry) entry = cm_ensure_entry(mgr, src); + if (!mgr || len != sizeof(struct CM_DIRECT_REQ)) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed DIRECT_REQ peer=%016llx len=%zu", (unsigned long long)src, len); + return; + } + (void)data; + struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, src); + if (!ni) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: REVERSE unknown peer=%016llx", (unsigned long long)src); return; } + struct CONN_MGR_ENTRY* entry = cm_ensure_entry(mgr, src); if (!entry) return; - entry->state = CONN_MGR_STATE_CONNECTING; entry->reverse_active = 1; - entry->main_connect_state = CM_TRY_PENDING; entry->db_loaded = 0; entry->conn_type = CONN_TYPE_NONE; - - struct TOPO_NODE nbuf; memset(&nbuf,0,sizeof(nbuf)); nbuf.node_id=src; memcpy(nbuf.public_key,src_ni->public_key,SC_PUBKEY_SIZE); - if (node_conn_direct_open_node(mgr->instance, src, cm_ncd_callback, entry, &entry->ncd_handle, &nbuf, NULL) == NCD_ERR) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: DIRECT_REQ open_node failed for 0x%016llx", (unsigned long long)src); - cm_entry_cleanup(entry); return; - } - struct ETCP_CONN* newc = node_conn_direct_get_conn(entry->ncd_handle); if (!newc) { node_conn_direct_close(entry->ncd_handle); entry->ncd_handle = NULL; return; } - - struct sock_view views[CM_MAX_SOCKET_VIEWS]; - int nviews = sock_collect_views(mgr->instance, views, CM_MAX_SOCKET_VIEWS); - uint8_t* p = (uint8_t*)data + CM_DIRECT_REQ_H_SIZE; - for (uint8_t i=0;iaddr_count;i++) { - uint8_t type=*p++; uint8_t ip[4]; memcpy(ip,p,4);p+=4; uint16_t port; memcpy(&port,p,2);p+=2; uint8_t sid=*p++; - uint32_t tip; memcpy(&tip, ip, 4); - uint8_t tcfg = CFG_SERVER_TYPE_UNKNOWN, tnat = NAT_TYPE_UNKNOWN; - for (const struct TOPO_SOCKMETA4* m = src_ni->v4_sock_meta; m; m = m->next) - if (m->id == sid) { tcfg = m->config_type; tnat = m->nat_type; break; } - for (int vi = 0; vi < nviews; vi++) { - const struct sock_view* v = &views[vi]; - if (v->is_tcp || !sock_match(v, type, tcfg, tnat, tip)) continue; - cm_add_v4_link(newc, ip, port, v->sock); - break; - } + 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; } + if (entry->direct_up && cm_direct_conn(mgr, src)) return; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: REVERSE request peer=%016llx path=%u existing_transport=%d", + (unsigned long long)src, entry->conn_type, entry->ncd_handle != NULL); + cm_start_phase_direct(entry); + cm_arm_timer(entry); } /* ═══════ disconnect / recv ═══════ */ @@ -856,15 +630,28 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry uint64_t group_id; memcpy(&group_id, entry->dgram + ROUTER_SVC_GROUP_OFF, 8); uint64_t src; memcpy(&src, entry->dgram + ROUTER_SVC_SRC_OFF, 8); struct TOPO_GROUP* grp = topo_groups_find(conn->instance->topo_groups, group_id); - struct CONN_MGR* mgr = grp ? grp->conn_mgr : conn->instance->conn_mgr; + struct CONN_MGR* mgr = grp ? grp->conn_mgr : NULL; + if (!mgr) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: packet for unknown group=%016llx peer=%016llx", + (unsigned long long)group_id, (unsigned long long)src); + queue_dgram_free(entry); queue_entry_free(entry); return; + } if (mgr) { struct CONN_MGR_ENTRY* e = cm_find_entry(mgr, src); if (e && e->state == CONN_MGR_STATE_CONNECTED) e->last_traffic_tb = get_time_tb(); } switch (sub) { case CM_SUBCMD_DIRECT_REQ: cm_handle_direct_req(src, mgr, d, len); break; case CM_SUBCMD_DIRECT_RESP: break; - case CM_SUBCMD_INTERM_EXCHANGE_REQ: if (len >= CM_EXCHANGE_REQ_SIZE) cm_handle_interm_exchange_req(src, mgr, (struct CM_EXCHANGE_REQ*)d); break; - case CM_SUBCMD_INTERM_EXCHANGE_RESP: if (mgr) cm_handle_interm_exchange_resp(mgr, d, len); break; + case CM_SUBCMD_INTERM_EXCHANGE_REQ: + if (len == CM_EXCHANGE_REQ_SIZE) cm_handle_interm_exchange_req(src, mgr, (struct CM_EXCHANGE_REQ*)d); + else DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed EXCHANGE_REQ peer=%016llx len=%zu", (unsigned long long)src, len); + break; + case CM_SUBCMD_INTERM_EXCHANGE_RESP: if (mgr) cm_handle_interm_exchange_resp(mgr, src, d, len); break; case CM_SUBCMD_INTERM_SELECTED: if (mgr) cm_handle_interm_selected(mgr, src, d, len); break; - case CM_SUBCMD_DISCONNECT: if (len >= CM_DISCONNECT_SIZE && mgr) cm_handle_disconnect(mgr, ((struct CM_DISCONNECT*)d)->node_id); break; + case CM_SUBCMD_INTERM_ACK: if (mgr) cm_handle_interm_ack(mgr, src, d, len); break; + case CM_SUBCMD_DISCONNECT: + if (len == CM_DISCONNECT_SIZE && ((struct CM_DISCONNECT*)d)->node_id == src) cm_handle_disconnect(mgr, src); + else DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed DISCONNECT peer=%016llx len=%zu", (unsigned long long)src, len); + break; + default: DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: unknown command=%u peer=%016llx", sub, (unsigned long long)src); break; } queue_dgram_free(entry); queue_entry_free(entry); } diff --git a/src/routing_layer/conn_mgr_doc.md b/src/routing_layer/conn_mgr_doc.md index 10d1f05c..6ea2b67d 100644 --- a/src/routing_layer/conn_mgr_doc.md +++ b/src/routing_layer/conn_mgr_doc.md @@ -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, резерв, реальные данные и обработку кандидатов. diff --git a/src/routing_layer/conn_mgr_indirect.c b/src/routing_layer/conn_mgr_indirect.c index 3d67d6ba..723e10e4 100644 --- a/src/routing_layer/conn_mgr_indirect.c +++ b/src/routing_layer/conn_mgr_indirect.c @@ -1,206 +1,212 @@ -/* conn_mgr_indirect.c — INDIRECT фаза + exchange-обработчики */ +/* conn_mgr_indirect.c — согласование и проверка пути через посредников. */ #include "conn_mgr_priv.h" #include "etcp.h" #include #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;ibest_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;iyour_candidates[i].node_id; - if (nid==self||nid==target) continue; /* сам/цель не может быть посредником */ - uint16_t our=0; for(uint8_t j=0;jmgr->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;imy_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;iCONN_MGR_MAX_INTERMEDIARIES?CONN_MGR_MAX_INTERMEDIARIES:cnt; - for(uint8_t i=0;iintermediaries[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;imgr->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;icached.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(lenmy_count>4||resp->your_count>4)return; - if(lenyour_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,lencached)?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;icached.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(lencount>CONN_MGR_MAX_INTERMEDIARIES?CONN_MGR_MAX_INTERMEDIARIES:sel->count; - if(lenstate==CONN_MGR_STATE_CONNECTED)return; /* симметричный случай — UP уже доставлен инициаторским путём */ - if(!entry->indirect_active && !entry->reverse_active)return; /* не INDIRECT/REVERSE фаза */ - for(uint8_t i=0;iintermediaries[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;icandidate_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;ibest_candidates[i]; - resp.your_count=req->candidate_count>4?4:req->candidate_count; - for(uint8_t i=0;icandidates[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); } diff --git a/src/routing_layer/conn_mgr_priv.h b/src/routing_layer/conn_mgr_priv.h index 8eac076f..cc6aa054 100644 --- a/src/routing_layer/conn_mgr_priv.h +++ b/src/routing_layer/conn_mgr_priv.h @@ -26,9 +26,8 @@ struct stcp_client; #define CONN_MGR_CANDIDATE_PING_TB 20000 #define CONN_MGR_BG_PING_INTERVAL_TB 1000 #define CONN_MGR_BG_PING_CYCLE_MIN_TB 600000 -#define CONN_MGR_LOCAL_SCAN_ATTEMPTS 3 -#define CONN_MGR_LOCAL_SCAN_TIMEOUT_MS 100 -#define CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS 2000 +#define CONN_MGR_REVERSE_DELAY_MS 500 +#define CONN_MGR_INDIRECT_DELAY_MS 1000 #define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 3000 #define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000 #define CONN_MGR_REVERSE_RETRANSMIT_MS 1000 @@ -61,15 +60,16 @@ enum { CM_SUBCMD_INTERM_EXCHANGE_RESP= 0x04, CM_SUBCMD_INTERM_SELECTED = 0x05, CM_SUBCMD_DISCONNECT = 0x06, + CM_SUBCMD_INTERM_ACK = 0x07, }; struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed)); #pragma pack(push, 1) -struct CM_DIRECT_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t addr_count; }; +struct CM_DIRECT_REQ { uint8_t cmd, subcmd; uint32_t request_id; }; struct CM_DIRECT_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t accepted; }; -struct CM_EXCHANGE_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t candidate_count; struct CONN_MGR_CANDIDATE candidates[4]; }; -struct CM_EXCHANGE_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t my_count, your_count; struct CONN_MGR_CANDIDATE my_candidates[4], your_candidates[4]; }; +struct CM_EXCHANGE_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t candidate_count; struct CONN_MGR_CANDIDATE candidates[CONN_MGR_MAX_CANDIDATES]; }; +struct CM_EXCHANGE_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t my_count, your_count; struct CONN_MGR_CANDIDATE my_candidates[CONN_MGR_MAX_CANDIDATES], your_candidates[CONN_MGR_MAX_CANDIDATES]; }; struct CM_INTERM_SEL { uint8_t cmd, subcmd; uint32_t request_id; uint8_t count; struct CONN_MGR_CANDIDATE selected[3]; }; struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; }; #pragma pack(pop) @@ -83,15 +83,6 @@ struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; }; /* ═══════ внутренние структуры ═══════ */ -struct cm_reverse_pending { struct cm_reverse_pending* next; uint32_t request_id; struct CONN_MGR_ENTRY* entry; uint8_t* pkt; size_t pkt_len; }; - -struct cm_exchange_pending { - struct cm_exchange_pending* next; uint32_t request_id; - struct CONN_MGR_ENTRY* entry; void* timer; - struct CM_EXCHANGE_RESP cached; uint8_t received, probed; - uint8_t* pkt; size_t pkt_len; -}; - struct CONN_MGR_HANDLE { uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg; struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next; @@ -106,13 +97,14 @@ struct CONN_MGR_ENTRY { uint8_t state, conn_type; uint64_t last_traffic_tb; struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle; - uint8_t ncd_down_notified; - uint64_t intermediaries[3]; uint8_t intermediariy_count, rr_idx; + uint64_t intermediaries[3]; uint8_t intermediariy_count; uint8_t local_scan_state, main_connect_state, db_loaded; - uint8_t reverse_active, indirect_active, indirect_ready; - void* reverse_timer; - uint64_t reverse_deadline_tb, indirect_deadline_tb; - struct { void* timer; uint32_t request_id; void* conn_ctx; } main; + uint8_t reverse_started, indirect_started, indirect_confirmed, direct_up, timeout_notified, ever_up; + void* timer; void* refresh_token; + uint64_t start_tb, reverse_start_tb, indirect_start_tb, deadline_tb; + uint64_t reverse_next_tb, reverse_deadline_tb, indirect_next_tb; + uint32_t reverse_request_id, exchange_request_id, peer_exchange_request_id; + struct CM_INTERM_SEL selection; /* Исходящий выбор до подтверждения пира. */ struct CONN_MGR* mgr; }; @@ -124,10 +116,7 @@ struct CONN_MGR { size_t bg_ping_cursor; uint64_t bg_ping_cycle_start_tb; struct CONN_MGR_CANDIDATE best_candidates[CONN_MGR_MAX_CANDIDATES]; uint8_t best_candidate_count; struct stcp_client* cand_tcp_pings[CONN_MGR_MAX_CANDIDATES]; /* влётные candidate TCP-пинги (для отмены при destroy) */ - uint32_t next_request_id, direct_timeout_ms; uint8_t initialized; - struct cm_reverse_pending* reverse_pending; - struct cm_exchange_pending* exchange_pending; - uint8_t _unused[0]; + uint32_t next_request_id; uint8_t initialized; }; /* ═══════ декларации внутренних функций ═══════ */ @@ -151,28 +140,26 @@ void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry); void cm_start_local_scan(struct CONN_MGR_ENTRY* entry); void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* data, size_t len); void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id); -void cm_reverse_tick_cb(void* arg); void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry); -void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s); -void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s); +void cm_refresh_path(struct CONN_MGR_ENTRY* entry); +void cm_arm_timer(struct CONN_MGR_ENTRY* entry); +int cm_send_control(struct CONN_MGR* mgr, uint64_t node_id, const void* packet, size_t len); +uint16_t cm_candidate_rtt(struct CONN_MGR* mgr, uint64_t node_id); +struct ETCP_CONN* cm_direct_conn(struct CONN_MGR* mgr, uint64_t node_id); int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq); -int cm_has_local_addr(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq); uint8_t cm_classify_v6_addr(const uint8_t addr[16]); -uint8_t cm_sock_v6_classify(const struct ETCP_SOCKET* s); #define CM_V6_LL 0 #define CM_V6_LOC 1 #define CM_V6_DIR 2 #define CM_V6_OTH 3 -#define CM_V6_ANY 4 /* indirect */ -void cm_exchange_tick_cb(void* arg); void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp); -void cm_exchange_probe_retry_cb(void* arg); void cm_handle_interm_exchange_req(uint64_t src, struct CONN_MGR* mgr, struct CM_EXCHANGE_REQ* req); -void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len); +void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len); void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len); +void cm_handle_interm_ack(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len); /* monitor */ void cm_bg_ping_timer_cb(void* arg); diff --git a/src/routing_layer/etcp_router.c b/src/routing_layer/etcp_router.c index 8b1537e0..ef07d725 100644 --- a/src/routing_layer/etcp_router.c +++ b/src/routing_layer/etcp_router.c @@ -22,6 +22,7 @@ in ---> {Q} -> [src, etcp] --> ... --> [dst,etcp] -> {asm_q} -> {buf q} ---> out #include "etcp.h" #include "utun_instance.h" #include "topo_group.h" +#include "conn_mgr.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/ll_queue.h" @@ -294,16 +295,11 @@ static struct ETCP_ROUTER_CONN* router_conn_find(struct UTUN_INSTANCE* inst, // ==================================================================== // Найти ETCP_CONN для отправки (учитывая indirect-посредников). NULL = нет маршрута. -static struct ETCP_CONN* router_route_conn(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id) { +static struct ETCP_CONN* router_route_conn(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id) { struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); if (group) { - struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, remote_node_id); - if (nq && nq->conn_mgr_type == CONN_TYPE_INDIRECT && nq->conn_mgr_intermediariy_count > 0) { - for (uint8_t i = 0; i < nq->conn_mgr_intermediariy_count; i++) { - struct ETCP_CONN* c = topo_group_find_conn_for_node(group, nq->conn_mgr_intermediaries[i]); - if (c) return c; - } - } + struct ETCP_CONN* preferred = conn_mgr_route_conn(group->conn_mgr, remote_node_id, svc_id == ETCP_RT_ID_CONN_MGR); + if (preferred) return preferred; return topo_group_find_conn_for_node(group, remote_node_id); } /* group_id=0 (или группа не найдена) — глобальная маршрутизация: прямое соединение по node_id */ @@ -312,7 +308,7 @@ static struct ETCP_CONN* router_route_conn(struct UTUN_INSTANCE* inst, uint64_t // Тонкая обёртка над router_route_conn для rconn (next_hop для отправки). static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) { - return router_route_conn(rconn->inst, rconn->group_id, rconn->remote_node_id); + return router_route_conn(rconn->inst, rconn->group_id, rconn->remote_node_id, rconn->svc_id); } // Есть ли физический маршрут до узла (прямой/indirect/глобальный): 1 — есть, 0 — нет. @@ -320,7 +316,17 @@ static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) { int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id) { if (!inst) return 0; if (dst_node_id == inst->node_id) return 1; - return router_route_conn(inst, group_id, dst_node_id) != NULL; + return router_route_conn(inst, group_id, dst_node_id, 0) != NULL; +} + +struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_t group_id, + uint64_t dst_node_id, uint8_t svc_id) { + struct ETCP_ROUTER_PATH path = {0}; + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "path snapshot without instance"); return path; } + struct ETCP_CONN* conn = router_route_conn(inst, group_id, dst_node_id, svc_id); + path.type = conn_mgr_connection_type(conn, dst_node_id); + if (path.type != CONN_TYPE_NONE) path.next_hop_node_id = conn->peer_node_id; + return path; } // Собрать и закодировать (подпись/шифрование) пакет в начале отправки. diff --git a/src/routing_layer/etcp_router.h b/src/routing_layer/etcp_router.h index bdc9dc15..08cad314 100644 --- a/src/routing_layer/etcp_router.h +++ b/src/routing_layer/etcp_router.h @@ -244,6 +244,12 @@ struct ETCP_ROUTER_CONN* etcp_router_conn_get(struct UTUN_INSTANCE* inst, // 1 = найден транспорт (для self всегда 1); это не гарантия UP/готовности логического канала. int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id); +// Снимок текущего пути отправки сервиса, только uasync-поток; без создания канала/изменения очередей. +// type — CONN_TYPE_*; отсутствие живого пути: NONE и next_hop_node_id=0. +struct ETCP_ROUTER_PATH { uint8_t type; uint64_t next_hop_node_id; }; +struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_t group_id, + uint64_t dst_node_id, uint8_t svc_id); + // Отправить данные с авто-seq и контролем inflight // data: payload без svc_id, mode: ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (0 = обычный) // Копирует data; 0=принято в очередь, <0=ошибка. Доставка подтверждается отдельно протоколом. diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index dc0180b7..b9a45bd7 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -217,6 +217,7 @@ static void topo_group_log_exchange(struct TOPO_GROUP* group, struct TOPO_GROUP_ (unsigned long long)group->group_id, (unsigned long long)peer->conn->peer_node_id, (unsigned long long)peer->local_epoch, peer->table_sent, peer->table_received, topo_group_peer_ready(group, peer->conn->peer_node_id)); + conn_mgr_routes_changed(group->conn_mgr); } static struct TOPOMSG_HEADER topo_header(struct TOPO_GROUP_CONN_ITEM* peer, uint8_t cmd) { @@ -1153,6 +1154,7 @@ static void topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROU for (struct topo_node_cbk_entry* c = group->node_cbks; c; c = c->next) c->fn(group, id, event, c->arg); topo_group_publish_route(group, id); topo_recovery_changed(group); + conn_mgr_routes_changed(group->conn_mgr); } /* TABLE_COMPLETE подтверждает только NODEINFO текущего обмена. Сбой обмена diff --git a/tests/test_call.c b/tests/test_call.c index 23a536df..ddb9a1c5 100644 --- a/tests/test_call.c +++ b/tests/test_call.c @@ -20,6 +20,7 @@ #include "../routing_layer/conn_mgr_priv.h" #include "../utun_instance.h" #include "../transport_layer/etcp.h" +#include "../transport_layer/etcp_connections.h" #include "../transport_layer/secure_channel.h" #include "../ntp_time.h" #include "../lib/u_async.h" @@ -118,6 +119,26 @@ static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; #define OK() do { G_PASSED++; printf("OK\n"); } while(0) #define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0) +/* Тип определяется по живым линкам, даже когда NCD остаётся UP при смене направления. */ +static int test_transport_direction(void) { + struct ETCP_CONN* conn = u_calloc(1, sizeof(*conn)); + struct ETCP_LINK incoming = {0}, outgoing = {0}; + if (!conn) return 0; + conn->peer_node_id = 42; conn->links_up = 1; conn->links = &incoming; + incoming.initialized = incoming.link_status = incoming.is_server = 1; + outgoing.initialized = outgoing.link_status = 1; incoming.next = &outgoing; + int ok = conn_mgr_connection_type(conn, 42) == CONN_TYPE_DIRECT; + outgoing.link_status = 0; + ok &= conn_mgr_connection_type(conn, 42) == CONN_TYPE_REVERSE; + incoming.link_status = 0; + ok &= conn_mgr_connection_type(conn, 42) == CONN_TYPE_NONE; + incoming.link_status = 1; + ok &= conn_mgr_connection_type(conn, 43) == CONN_TYPE_INDIRECT; + conn->close_requested = 1; + ok &= conn_mgr_connection_type(conn, 43) == CONN_TYPE_NONE; + u_free(conn); return ok; +} + /* ── крипто-хелперы ── */ static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) { @@ -432,6 +453,9 @@ int main(int argc, char** argv) { printf("=== test_call ===\n"); fflush(stdout); + TEST("live transport: mixed links, reverse, none, intermediary"); + if (test_transport_direction()) OK(); else FAIL("incorrect transport direction"); + TEST("single-thread: call happy path + decline (2 nodes)"); { struct UASYNC* ua = uasync_create(); if (!ua) { FAIL("uasync_create failed"); return 1; } diff --git a/tests/test_conn_mgr_handles.c b/tests/test_conn_mgr_handles.c index b71512b3..c57de991 100644 --- a/tests/test_conn_mgr_handles.c +++ b/tests/test_conn_mgr_handles.c @@ -143,6 +143,8 @@ static void test3_verify(void* arg) { fail("test5: open failed"); return; } cb1_ok = 0; + conn_mgr_routes_changed(mgr); + if (!gh1->entry->refresh_token) { fail("test5: refresh not scheduled"); return; } cm_handle_disconnect(mgr, g_nid_b); if (gh1->entry || gh1->deliver_up_token || conn_mgr_get_conn(gh1)) { fail("test5: handle not detached"); return; } conn_mgr_close(gh1); gh1 = NULL; diff --git a/tests/test_conn_mgr_phases.c b/tests/test_conn_mgr_phases.c index 9a24ee4f..37a1b986 100644 --- a/tests/test_conn_mgr_phases.c +++ b/tests/test_conn_mgr_phases.c @@ -30,6 +30,9 @@ #include "topo_node.h" #include "conn_mgr.h" #include "conn_mgr_priv.h" +#include "etcp_router.h" +#include "../src/call/call.h" +#include "../src/chat/chat_event.h" #include "route_connectivity.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" @@ -48,6 +51,34 @@ static volatile int ev_a = -1, ev_b = -1; static struct CONN_MGR_HANDLE *h_a = NULL, *h_b = NULL; static int g_failed = 0; +static int up_count, down_count, timeout_count, data_count, expected_marker; +static uint64_t data_hop; +#define TEST_CM_SERVICE 200 + +static uint64_t incoming_call, connection_call, call_next_hop; +static uint8_t call_connection_type; +static int call_accepted, call_ended, call_connection_events, call_media_frames; +static void call_event_recv(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type == CHAT_EVT_CALL_INCOMING && inst == inst_b && len >= 17 + data[0]) + memcpy(&incoming_call, data + 1 + data[0] + 8, 8); + if (inst != inst_a) return; + if (type == CHAT_EVT_CALL_ACCEPTED) call_accepted = 1; + if (type == CHAT_EVT_CALL_ENDED) call_ended = 1; + if (type != CHAT_EVT_CALL_CONNECTION) return; + if (len != 17 || data[8] > CONN_TYPE_INDIRECT || call_ended) { g_failed = 1; return; } + memcpy(&connection_call, data, 8); call_connection_type = data[8]; memcpy(&call_next_hop, data + 9, 8); + call_connection_events++; +} +static void call_media_recv(struct UTUN_INSTANCE* inst, uint64_t id, const uint8_t* data, int len, void* arg) { + (void)inst; (void)id; (void)data; (void)len; (void)arg; call_media_frames++; +} +static int call_incoming_ready(void) { return incoming_call != 0; } +static int call_active(void) { return call_accepted; } +static int call_is_direct(void) { return call_connection_type == CONN_TYPE_DIRECT || call_connection_type == CONN_TYPE_REVERSE; } +static int call_is_indirect(void) { return call_connection_type == CONN_TYPE_INDIRECT; } +static int call_has_no_route(void) { return call_connection_type == CONN_TYPE_NONE && call_next_hop == 0; } +static int call_media_delivered(void) { return call_media_frames > 0; } + static char temp_dir[] = "/tmp/utun_cm_phases_XXXXXX"; #define TEMP_DIR_TEMPLATE "/tmp/utun_cm_phases_XXXXXX" @@ -75,6 +106,9 @@ static void fail(const char* msg) { fprintf(stderr, "FAIL: %s\n", msg); fflush(s static void conn_cb_a(struct CONN_MGR_HANDLE* h, uint64_t node, uint64_t grp, enum conn_mgr_event ev, void* arg) { (void)h; (void)node; (void)grp; (void)arg; ev_a = (int)ev; + if (ev == CONN_EVENT_UP) up_count++; + else if (ev == CONN_EVENT_DOWN) down_count++; + else timeout_count++; } static void conn_cb_b(struct CONN_MGR_HANDLE* h, uint64_t node, uint64_t grp, enum conn_mgr_event ev, void* arg) { (void)h; (void)node; (void)grp; (void)arg; ev_b = (int)ev; @@ -215,10 +249,14 @@ static int scenario_setup(void) { static void scenario_teardown(void) { if (h_a) { conn_mgr_close(h_a); h_a = NULL; } if (h_b) { conn_mgr_close(h_b); h_b = NULL; } + incoming_call = connection_call = call_next_hop = 0; call_connection_type = CONN_TYPE_NONE; + call_accepted = call_ended = call_connection_events = call_media_frames = 0; if (inst_a) { inst_a->running = 0; utun_instance_destroy(inst_a); inst_a = NULL; } if (inst_b) { inst_b->running = 0; utun_instance_destroy(inst_b); inst_b = NULL; } if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); inst_c = NULL; } cleanup_configs(); + up_count = down_count = timeout_count = data_count = expected_marker = 0; + ev_a = ev_b = -1; } /* ─── Сценарий 1: DIRECT ─── */ @@ -252,7 +290,9 @@ static int run_reverse(void) { ev_a = -1; conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a); if (!poll_until(ev_a_up, CONN_TIMEOUT_MS)) { fail("REVERSE: no CONN_EVENT_UP"); return -1; } - if (conn_type_of(inst_a, nid_b) != CONN_TYPE_REVERSE) { fail("REVERSE: conn_type != REVERSE"); return -1; } + uint8_t ty = conn_type_of(inst_a, nid_b); + if (ty != CONN_TYPE_DIRECT && ty != CONN_TYPE_REVERSE) { fail("REVERSE: no direct path"); return -1; } + if (!h_a->entry->reverse_started) { fail("REVERSE: reverse did not start immediately"); return -1; } fprintf(stderr, " OK: REVERSE\n"); fflush(stderr); return 0; @@ -293,17 +333,216 @@ static int run_indirect(void) { static int candidates_a_and_b(void) { return best_candidates_ready(inst_a) && best_candidates_ready(inst_b); } static int ev_ab_up(void) { return ev_a == CONN_EVENT_UP && ev_b == CONN_EVENT_UP; } +/* Приём реальных данных: next hop на получателе отличает релей от прямого пути. */ +static void data_recv(struct ETCP_CONN* conn, struct ll_entry* e) { + if (e->len == ROUTER_SVC_PAYLOAD_OFF + 1) { + if (e->dgram[ROUTER_SVC_PAYLOAD_OFF] != ++expected_marker) fail("DATA: order/duplicate violation"); + data_count++; data_hop = conn->peer_node_id; + } + queue_dgram_free(e); queue_entry_free(e); +} +static int send_data(uint8_t marker, uint64_t hop) { + data_count = 0; + struct ETCP_ROUTER_PATH path = etcp_router_get_path(inst_a, TOPO_GROUP_UTUN, nid_b, ETCP_RT_ID_CALL); + if (path.next_hop_node_id != (hop == nid_a ? nid_b : hop) || path.type == CONN_TYPE_NONE || + (hop == nid_c && path.type != CONN_TYPE_INDIRECT)) { fail("DATA: call path snapshot differs from route"); return -1; } + struct ll_entry* e = ll_alloc_lldgram(2); + if (!e) return -1; + e->dgram[0] = TEST_CM_SERVICE; e->dgram[1] = marker; e->len = 2; + if (etcp_route_send(inst_a, TOPO_GROUP_UTUN, nid_b, e, 1, 0) < 0) { fail("DATA: send failed"); return -1; } + uint64_t end = get_time_tb() + 30000; + while (!data_count && get_time_tb() < end) uasync_poll(ua, POLL_MS); + if (!data_count || data_hop != hop || expected_marker != marker) { fail("DATA: wrong delivery/hop"); return -1; } + return 0; +} +static void poll_ms(int ms) { + uint64_t end = get_time_tb() + (uint64_t)ms * 10; + while (get_time_tb() < end) uasync_poll(ua, POLL_MS); +} +static int direct_a_ready(void) { + uint8_t ty = conn_type_of(inst_a, nid_b); + return (ty == CONN_TYPE_DIRECT || ty == CONN_TYPE_REVERSE) && topo_group_peer_ready(defgrp(inst_a), nid_b); +} + +/* Две публичные стороны: новые пробы стартуют от t0, даже когда DIRECT ещё идёт. */ +static int run_schedule(void) { + fprintf(stderr, "\n=== Scenario 4: 500/1000ms schedule ===\n"); + if (create_configs("public", "public") || scenario_setup()) return -1; + if (!poll_until(candidates_a_and_b, 8000)) { fail("SCHEDULE: no relay candidates"); return -1; } + mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000)); + mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000)); + // DIRECT остаётся pending в момент запуска обоих дополнительных вариантов. + inst_a->etcp_connect_timeout_tb = 100000; + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1; + struct CONN_MGR_ENTRY* entry = h_a->entry; + poll_ms(400); + if (entry->reverse_started || entry->indirect_started) { fail("SCHEDULE: premature start"); return -1; } + poll_ms(300); + if (!entry->reverse_started || entry->indirect_started || entry->main_connect_state != CM_TRY_PENDING) { + fail("SCHEDULE: REVERSE must start at 500ms without stopping DIRECT"); return -1; + } + if (!poll_until(ev_a_up, 2000) || !entry->indirect_started || entry->conn_type != CONN_TYPE_INDIRECT) { + fail("SCHEDULE: INDIRECT must start at 1000ms"); return -1; + } + if (get_time_tb() - entry->start_tb > 20000 || !entry->ncd_handle || entry->main_connect_state != CM_TRY_PENDING) { + fail("SCHEDULE: lost original probe or late relay"); return -1; + } + if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1; + fprintf(stderr, " OK: parallel schedule and real relay delivery\n"); + return 0; +} + +/* Открывает только A: INDIRECT сразу, затем поздний DIRECT после NCD timeout. */ +static int run_upgrade(void) { + fprintf(stderr, "\n=== Scenario 5: one-sided INDIRECT -> late DIRECT ===\n"); + if (create_configs("nat", "nat") || scenario_setup()) return -1; + if (!poll_until(candidates_a_and_b, 8000)) { fail("UPGRADE: no relay candidates"); return -1; } + struct ETCP_ROUTER_PATH fallback = etcp_router_get_path(inst_a, TOPO_GROUP_UTUN, nid_b, ETCP_RT_ID_CALL); + if (fallback.type != CONN_TYPE_INDIRECT || fallback.next_hop_node_id != nid_c || cm_find_entry(defgrp(inst_a)->conn_mgr, nid_b)) { + fail("UPGRADE: missing BGP fallback snapshot before CM open"); return -1; + } + mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000)); + mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000)); + inst_a->etcp_connect_timeout_tb = 1000; + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1; + struct CONN_MGR_ENTRY* entry = h_a->entry; + if (!entry->indirect_started || entry->reverse_started || entry->local_scan_state == CM_TRY_NONE) { + fail("UPGRADE: NAT/NAT must start DIRECT/LOCAL/INDIRECT immediately"); return -1; + } + struct NODE_CONN_DIRECT* original = entry->ncd_handle; + if (!poll_until(ev_a_up, 400) || entry->conn_type != CONN_TYPE_INDIRECT) { fail("UPGRADE: relay waited for reverse"); return -1; } + if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1; + poll_ms(150); + if (entry->main_connect_state != CM_TRY_FAILED || entry->ncd_handle != original || timeout_count || down_count) { + fail("UPGRADE: NCD timeout interrupted relay/probe"); return -1; + } + poll_ms(400); + if (!entry->reverse_started) { fail("UPGRADE: REVERSE not launched while relay works"); return -1; } + if (send_data(2, nid_c)) return -1; + // Первый сервисный handle на принимающей стороне сохраняет INDIRECT и начинает своё расписание. + struct CONN_MGR_ENTRY* inbound = cm_find_entry(defgrp(inst_b)->conn_mgr, nid_a); + if (!inbound || inbound->handles || inbound->conn_type != CONN_TYPE_INDIRECT) { + fail("UPGRADE: missing ownerless incoming relay path"); return -1; + } + uint64_t owner_start = get_time_tb(); + if (conn_mgr_open(inst_b, TOPO_GROUP_UTUN, nid_a, conn_cb_b, NULL, &h_b)) return -1; + if (h_b->entry != inbound || inbound->start_tb < owner_start || !inbound->ncd_handle || + inbound->reverse_start_tb != inbound->start_tb + CONN_MGR_REVERSE_DELAY_MS * 10 || + inbound->conn_type != CONN_TYPE_INDIRECT || !inbound->indirect_confirmed) { + fail("UPGRADE: incoming owner lost relay or did not start direct probes"); return -1; + } + poll_ms(10); + if (ev_b != CONN_EVENT_UP) { fail("UPGRADE: incoming owner did not receive UP"); return -1; } + // Настоящий звонок использует тот же путь; GUI получает отдельные события, без повторных CM UP. + if (call_init(inst_a) || call_init(inst_b)) { fail("CALL PATH: service initialization failed"); return -1; } + chat_event_set_handler(inst_a, call_event_recv); chat_event_set_handler(inst_b, call_event_recv); + call_set_media_recv_cb(inst_b, call_media_recv, NULL); + uint64_t call_id = UINT64_C(0x123456789abcdef); + char group_id[32]; snprintf(group_id, sizeof(group_id), "%llu", (unsigned long long)TOPO_GROUP_UTUN); + call_start(inst_a, group_id, nid_b, call_id); + if (!poll_until(call_incoming_ready, 3000) || incoming_call != call_id) { fail("CALL PATH: no incoming call"); return -1; } + call_accept(inst_b, call_id); + if (!poll_until(call_active, 3000) || connection_call != call_id || !call_is_indirect() || call_next_hop != nid_c) { + fail("CALL PATH: relay status missing"); return -1; + } + uint8_t frame[] = {0xf8, 0xff, 0xfe}; + if (call_send_media(inst_a, call_id, frame, sizeof(frame)) || !poll_until(call_media_delivered, 3000)) { + fail("CALL PATH: relay media delivery failed"); return -1; + } + // Появился пригодный адрес B. Обновление дополняет существующий NCD. + mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 2)); + if (node_conn_direct_update_node(inst_a, nid_b) < 0 || !poll_until(direct_a_ready, 4000)) { + fail("UPGRADE: no late direct READY path"); return -1; + } + if (entry->ncd_handle != original || up_count != 1 || down_count || timeout_count || !entry->intermediariy_count) { + fail("UPGRADE: duplicate UP/lost probe or reserve"); return -1; + } + if (send_data(3, nid_a)) return -1; + if (!poll_until(call_is_direct, 1400) || call_next_hop != nid_b) { fail("CALL PATH: late direct status missing"); return -1; } + // Без новых кадров/CM UP статус не повторяется на очередном тике. + int events = call_connection_events; + poll_ms(1100); + if (call_connection_events != events) { fail("CALL PATH: duplicate unchanged status"); return -1; } + // Потеря прямого транспорта должна вернуть передачу на проверенный резерв без DOWN. + struct ETCP_CONN* direct = node_conn_direct_get_conn(original); + if (etcp_conn_ref_take(direct)) { fail("UPGRADE: cannot hold direct transport"); return -1; } + while (direct->links) etcp_link_close(direct->links); + etcp_conn_ref_free(direct); + conn_mgr_routes_changed(defgrp(inst_a)->conn_mgr); + if (entry->conn_type != CONN_TYPE_INDIRECT || down_count || up_count != 1) { + fail("UPGRADE: direct loss did not fall back without DOWN"); return -1; + } + if (send_data(4, nid_c)) return -1; + if (!poll_until(call_is_indirect, 1400) || call_next_hop != nid_c) { fail("CALL PATH: reserve status missing"); return -1; } + // WITHDRAW последнего пути к B: живой первый транспорт до C больше не даёт подключения к B. + struct TOPO_GROUP_NODE* target = topo_node_find_by_id(defgrp(inst_a), nid_b); + while (target && target->paths && target->paths->head) + topo_group_remove_path(target, ((struct TOPO_NODEPATH*)target->paths->head)->conn); + conn_mgr_routes_changed(defgrp(inst_a)->conn_mgr); + if (!poll_until(call_has_no_route, 1400)) { fail("CALL PATH: no-route status missing"); return -1; } + call_hangup(inst_a, call_id); + events = call_connection_events; + poll_ms(1100); + if (!call_ended || call_connection_events != events) { fail("CALL PATH: event after call end"); return -1; } + fprintf(stderr, " OK: one UP, retained NCD, ordered delivery relay -> direct -> reserve\n"); + fprintf(stderr, " OK: call status relay -> direct -> reserve -> NONE, media and timer cleanup\n"); + return 0; +} + +static int run_local(void) { + fprintf(stderr, "\n=== Scenario 6: NAT/NAT LAN direct ===\n"); + if (create_configs("nat", "nat") || scenario_setup()) return -1; + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1; + if (h_a->entry->local_scan_state == CM_TRY_NONE) { fail("LOCAL: local probes did not start immediately"); return -1; } + if (!poll_until(direct_a_ready, 4000)) { fail("LOCAL: no LAN direct connection"); return -1; } + if (up_count != 1 || down_count || timeout_count) { fail("LOCAL: inconsistent events"); return -1; } + if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_a)) return -1; + fprintf(stderr, " OK: immediate LAN handshake and direct delivery\n"); + return 0; +} + +/* Пустой ответ, неизвестный RTT и неверный peer/request не дают ложного UP. */ +static int run_invalid_candidates(void) { + fprintf(stderr, "\n=== Scenario 7: candidate validation and cleanup ===\n"); + if (create_configs("nat", "nat") || scenario_setup()) return -1; + if (!poll_until(candidates_a_and_b, 8000)) return -1; + mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000)); + mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000)); + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1; + struct CONN_MGR_ENTRY* entry = h_a->entry; + struct CM_EXCHANGE_RESP resp = {0}; + resp.cmd = ETCP_RT_ID_CONN_MGR; resp.subcmd = CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id = entry->exchange_request_id; + cm_handle_interm_exchange_resp(defgrp(inst_a)->conn_mgr, nid_b, (uint8_t*)&resp, sizeof(resp)); + resp.my_count = 1; resp.my_candidates[0].node_id = nid_c; // RTT=0 — отсутствующий замер. + cm_handle_interm_exchange_resp(defgrp(inst_a)->conn_mgr, nid_b, (uint8_t*)&resp, sizeof(resp)); + resp.my_candidates[0].rtt = UINT16_MAX; + cm_handle_interm_exchange_resp(defgrp(inst_a)->conn_mgr, nid_b, (uint8_t*)&resp, sizeof(resp)); + resp.my_candidates[0].rtt = 1; + cm_handle_interm_exchange_resp(defgrp(inst_a)->conn_mgr, nid_c, (uint8_t*)&resp, sizeof(resp)); + resp.request_id++; + cm_handle_interm_exchange_resp(defgrp(inst_a)->conn_mgr, nid_b, (uint8_t*)&resp, sizeof(resp)); + if (up_count || entry->selection.count || entry->indirect_confirmed) { fail("VALIDATION: false path success"); return -1; } + // Один посредник в обоих списках должен остаться одним, а выбранный путь ждёт ACK. + resp.request_id = entry->exchange_request_id; resp.your_count = 1; resp.your_candidates[0] = resp.my_candidates[0]; + cm_handle_interm_exchange_resp(defgrp(inst_a)->conn_mgr, nid_b, (uint8_t*)&resp, sizeof(resp)); + if (entry->selection.count != 1 || up_count) { fail("VALIDATION: duplicate intermediary or premature UP"); return -1; } + conn_mgr_close(h_a); h_a = NULL; + if (cm_find_entry(defgrp(inst_a)->conn_mgr, nid_b)) { fail("VALIDATION: close left entry/timer alive"); return -1; } + poll_ms(1100); + if (up_count || down_count || timeout_count) { fail("VALIDATION: callback after close"); return -1; } + fprintf(stderr, " OK: empty/unknown/stale candidates rejected, no duplicate, close cancels callbacks\n"); + return 0; +} + /* ─── main ─── */ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); if (getenv("UTUN_TEST_DEBUG")) { - debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); - debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); - debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_DEBUG); - debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_INFO); } utun_instance_set_tun_init_enabled(0); route_connectivity_set_probe_timeout_ms(10); @@ -322,6 +561,15 @@ int main(void) { if (run_indirect() != 0) g_failed = 1; scenario_teardown(); + if (run_schedule() != 0) g_failed = 1; + scenario_teardown(); + if (run_upgrade() != 0) g_failed = 1; + scenario_teardown(); + if (run_local() != 0) g_failed = 1; + scenario_teardown(); + if (run_invalid_candidates() != 0) g_failed = 1; + scenario_teardown(); + if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; } fprintf(stderr, "%s\n", g_failed ? "RESULT: FAIL" : "RESULT: PASS"); fflush(stderr); diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt index e5406a98..50600898 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt @@ -21,7 +21,7 @@ object AppEventHandler : ChatEventCallback { val events: SharedFlow> = _events override fun onEvent(type: Int, data: ByteArray?) { - if (type == 12 || type == 13 || type in 34..40 || type == 42) { + if (type == 12 || type == 13 || type in 34..40 || type == 42 || type == 44) { mainHandler.post { CallController.onEvent(type, data) } } Log.d("utun-gui", "[EVENT] type=$type len=${data?.size ?: 0}") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt index 6649b385..228f57af 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt @@ -198,7 +198,8 @@ object CallController : TelecomCallManager.Listener { _route.value = AudioRoute.SPEAKER _headset.value = false _earpiece.value = true - _state.value = st.copy(phase = CallPhase.ENDED, reason = reason) + _state.value = st.copy(phase = CallPhase.ENDED, reason = reason, + connectionType = CallConnectionType.NONE, connectionNextHop = 0) syncCoreActivity() LogManager.addLog("INFO", "Call", "finished id=$id reason=$reason; audio/service/proximity released") scope.launch { @@ -253,7 +254,7 @@ object CallController : TelecomCallManager.Listener { return } if (type == 13) { reset(); return } - if (type !in 34..40 && type != 42) return + if (type !in 34..40 && type != 42 && type != 44) return if (!coreReady || !NativeLib.isReady()) { LogManager.addLog("WARN", "Call", "ignore event=$type while core stopped/restarting") return @@ -285,7 +286,8 @@ object CallController : TelecomCallManager.Listener { return } if (data == null || data.size < 8 || ((type == 37 || type == 38 || type == 40 || type == 42) && data.size < 9) || - (type == 39 && data.size != 24)) { badEvent(type, data); return } + (type == 39 && data.size != 24) || + (type == 44 && (data.size != 17 || (data[8].toInt() and 0xFF) > 3))) { badEvent(type, data); return } val id = ByteBuffer.wrap(data, 0, 8).order(ByteOrder.LITTLE_ENDIAN).long val st = _state.value if (st == null || st.callId != id || !st.ongoing) { @@ -328,6 +330,12 @@ object CallController : TelecomCallManager.Listener { _state.value = st.copy(stats = CallStats(metric(), metric(), metric(), metric(), metric(), metric(), metric(), metric())) } 42 -> _state.value = st.copy(bgpPath = String(data, 8, data.size - 8)) + 44 -> { + val connection = CallConnectionType.values()[data[8].toInt() and 0xFF] + val nextHop = ByteBuffer.wrap(data, 9, 8).order(ByteOrder.LITTLE_ENDIAN).long + _state.value = st.copy(connectionType = connection, connectionNextHop = nextHop) + LogManager.addLog("INFO", "Call", "connection id=$id type=$connection via=${nextHop.toULong().toString(16)}") + } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt index c73edcba..e06992e4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt @@ -3,6 +3,11 @@ package com.utun.chat.data /** Состояние единственной звонковой сессии; не зависит от Activity или ViewModel. */ enum class CallPhase { NONE, OUTGOING, RINGING, INCOMING, ACTIVE, ENDED } +/** Значения CONN_TYPE_* общего ядра. */ +enum class CallConnectionType(val label: String) { + NONE("нет подключения"), DIRECT("прямое"), REVERSE("reverse"), INDIRECT("indirect") +} + data class CallStats( val rttMs: Int, val bufferMs: Int, val tempoX100: Int, val dropped: Int, val underruns: Int, val minMs: Int, val maxMs: Int, val reserveMs: Int @@ -18,7 +23,9 @@ data class CallState( val durationSeconds: Int = 0, val bgpPath: String = "", val stats: CallStats? = null, - val startedAtElapsedRealtime: Long = 0 + val startedAtElapsedRealtime: Long = 0, + val connectionType: CallConnectionType = CallConnectionType.NONE, + val connectionNextHop: Long = 0 ) { val ongoing: Boolean get() = phase != CallPhase.NONE && phase != CallPhase.ENDED } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt index 4b253b60..f1bb907c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt @@ -169,6 +169,9 @@ fun CallScreen( color = Color(0xFFAEBCC8), fontSize = 16.sp ) + Spacer(Modifier.height(8.dp)) + Text("Подключение: ${st?.connectionType?.label ?: "нет подключения"}", + color = Color(0xFFAEBCC8), fontSize = 13.sp) val bgpPath = st?.bgpPath ?: "" if (bgpPath.isNotEmpty()) { Spacer(Modifier.height(8.dp)) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt index 796e7559..51b7e934 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt @@ -50,6 +50,8 @@ fun IncomingCallDialog( Text(name, color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Medium) Spacer(Modifier.height(6.dp)) Text("Входящий звонок…", color = Color(0xFFAEBCC8), fontSize = 14.sp) + Spacer(Modifier.height(6.dp)) + Text("Подключение: ${st.connectionType.label}", color = Color(0xFFAEBCC8), fontSize = 13.sp) Spacer(Modifier.height(28.dp)) Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) { diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 1862d12a..28e7d2cd 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -161,6 +161,10 @@ CallWindow::CallWindow(QWidget* parent) m_bgpPathLabel->setWordWrap(true); m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;"); m_bgpPathLabel->hide(); + m_connectionLabel = new QLabel(this); + m_connectionLabel->setAlignment(Qt::AlignCenter); + m_connectionLabel->setStyleSheet("color: gray; font-size: 13px;"); + setConnectionType(0, 0); m_statsLabel = new QLabel(this); m_statsLabel->setAlignment(Qt::AlignCenter); m_statsLabel->setWordWrap(true); @@ -185,6 +189,7 @@ CallWindow::CallWindow(QWidget* parent) lay->addWidget(m_peerLabel); lay->addWidget(m_statusLabel); lay->addWidget(m_durationLabel); + lay->addWidget(m_connectionLabel); lay->addWidget(m_bgpPathLabel); lay->addWidget(m_statsLabel); lay->addStretch(1); @@ -231,6 +236,13 @@ void CallWindow::setBgpPath(const QString& path) { m_bgpPathLabel->setVisible(!path.isEmpty()); } +void CallWindow::setConnectionType(uint8_t type, quint64 nextHop) { + const QString names[] = { QStringLiteral("нет подключения"), QStringLiteral("прямое"), + QStringLiteral("reverse"), QStringLiteral("indirect") }; + m_connectionLabel->setText(QStringLiteral("Подключение: %1").arg(names[type <= 3 ? type : 0])); + m_connectionLabel->setToolTip(type == 3 ? QStringLiteral("Посредник: 0x%1").arg(nextHop, 16, 16, QChar('0')) : QString()); +} + void CallWindow::setState(State s) { m_state = s; switch (s) { @@ -256,6 +268,7 @@ void CallWindow::setState(State s) { m_muteBtn->setChecked(false); break; case Ended: + setConnectionType(0, 0); m_statusLabel->setText(QStringLiteral("Завершён")); m_durationTimer->stop(); break; diff --git a/tools/chatgui/src/callwindow.h b/tools/chatgui/src/callwindow.h index f61c24bd..e0dd53fd 100644 --- a/tools/chatgui/src/callwindow.h +++ b/tools/chatgui/src/callwindow.h @@ -23,6 +23,7 @@ public: void setPeerName(const QString& name); void setBgpPath(const QString& path); + void setConnectionType(uint8_t type, quint64 nextHop); void setAudioStats(int rtt, int buffer, int tempo, int dropped, int underruns, int minimum, int maximum, int reserve); void setState(State s); State state() const { return m_state; } @@ -55,6 +56,7 @@ private: QLabel* m_statusLabel; QLabel* m_durationLabel; QLabel* m_bgpPathLabel; + QLabel* m_connectionLabel; QLabel* m_statsLabel; QPushButton* m_acceptBtn; QPushButton* m_declineBtn; diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index ef5f212f..1bb2d5b1 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -465,6 +465,11 @@ void MainWindow::setupBridgeCallbacks() { gui_bridge_set_call_path_cb([](uint64_t call_id, const char* text, int len) { if (s_mainWindow) s_mainWindow->onCallPath(call_id, QString::fromUtf8(text, len)); }); + gui_bridge_set_call_connection_cb([](uint64_t id, uint8_t type, uint64_t nextHop) { + if (!s_mainWindow) return; + auto* w = s_mainWindow->m_activeCalls.value(id, nullptr); + if (w && w->state() != CallWindow::Ended) w->setConnectionType(type, nextHop); + }); /* ── рация: индикация говорящего ── */ gui_bridge_set_radio_talk_cb([](uint64_t group_id, uint64_t src_node_id, uint8_t on) { diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 64587657..a68047f5 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -51,6 +51,7 @@ struct UTUN_INSTANCE; #define GUI_EVT_DM_MESSAGES 41 /* data: [conv_id_len:1][conv_id:var][count:2]([dir:1][seq:8][ts:8][author:8][ct_len:1][ct:var][data_len:2][data:var])* */ #define GUI_EVT_CALL_PATH 42 /* data: [call_id:8][text:var] — BGP-маршрут до пира звонка */ #define GUI_EVT_RADIO_TALK 43 /* data: [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */ +#define GUI_EVT_CALL_CONNECTION 44 /* data: [call_id:8][CONN_TYPE:1][next_hop_node_id:8] */ /* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */ #define MEMBER_DETAIL_LINK_SIZE 64 @@ -204,6 +205,9 @@ void gui_bridge_set_call_stats_cb(gui_call_stats_fn cb); typedef void (*gui_call_path_fn)(uint64_t call_id, const char* text, int len); void gui_bridge_set_call_path_cb(gui_call_path_fn cb); +typedef void (*gui_call_connection_fn)(uint64_t call_id, uint8_t type, uint64_t next_hop_node_id); +void gui_bridge_set_call_connection_cb(gui_call_connection_fn cb); + /* Callback индикации говорящего в рации (вызывается из GUI-потока) * group_id — канал, src_node_id — кто говорит, on — 1=начал / 0=закончил. */ typedef void (*gui_radio_talk_fn)(uint64_t group_id, uint64_t src_node_id, uint8_t on); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 0e787e25..678de4e5 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -56,6 +56,7 @@ static gui_call_ended_fn g_call_declined_cb = nullptr; static gui_call_error_fn g_call_error_cb = nullptr; static gui_call_stats_fn g_call_stats_cb = nullptr; static gui_call_path_fn g_call_path_cb = nullptr; +static gui_call_connection_fn g_call_connection_cb = nullptr; static gui_radio_talk_fn g_radio_talk_cb = nullptr; static gui_dm_msg_received_fn g_dm_msg_received_cb = nullptr; static gui_dm_conv_updated_fn g_dm_conv_updated_cb = nullptr; @@ -323,6 +324,15 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { if (g_call_path_cb) g_call_path_cb(callId, (const char*)d + 8, dlen - 8); } break; + case GUI_EVT_CALL_CONNECTION: + if (dlen == 17 && d[8] <= 3) { + uint64_t callId, nextHop; + memcpy(&callId, d, 8); memcpy(&nextHop, d + 9, 8); + if (g_call_connection_cb) g_call_connection_cb(callId, d[8], nextHop); + } else { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: invalid CALL_CONNECTION size=%d", dlen); + } + break; case GUI_EVT_CALL_STATS: if (dlen == 24) { uint64_t callId; memcpy(&callId, d, 8); @@ -512,6 +522,10 @@ void gui_bridge_set_call_path_cb(gui_call_path_fn cb) { g_call_path_cb = cb; } +void gui_bridge_set_call_connection_cb(gui_call_connection_fn cb) { + g_call_connection_cb = cb; +} + void gui_bridge_set_radio_talk_cb(gui_radio_talk_fn cb) { g_radio_talk_cb = cb; }