Browse Source

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

master
evgeny 3 weeks ago
parent
commit
111432aa64
  1. 43
      doc/tasks.md
  2. 71
      src/call/call.c
  3. 2
      src/call/call.h
  4. 3
      src/chat/chat_event.c
  5. 1
      src/chat/chat_event.h
  6. 18
      src/routing_layer/conn_mgr.h
  7. 865
      src/routing_layer/conn_mgr_core.c
  8. 329
      src/routing_layer/conn_mgr_doc.md
  9. 354
      src/routing_layer/conn_mgr_indirect.c
  10. 55
      src/routing_layer/conn_mgr_priv.h
  11. 26
      src/routing_layer/etcp_router.c
  12. 6
      src/routing_layer/etcp_router.h
  13. 2
      src/routing_layer/topo_group.c
  14. 24
      tests/test_call.c
  15. 2
      tests/test_conn_mgr_handles.c
  16. 258
      tests/test_conn_mgr_phases.c
  17. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt
  18. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt
  19. 9
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt
  20. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt
  21. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt
  22. 13
      tools/chatgui/src/callwindow.cpp
  23. 2
      tools/chatgui/src/callwindow.h
  24. 5
      tools/chatgui/src/mainwindow.cpp
  25. 4
      tools/chatgui/transport/gui_bridge.h
  26. 14
      tools/chatgui/transport/gui_bridge_impl.cpp

43
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, без внешних

71
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");
}

2
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).

3
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);
}

1
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);

18
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

865
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;i<dc;i++) { *p++=addrs[i].t; memcpy(p,addrs[i].ip,4);p+=4; memcpy(p,&addrs[i].port,2);p+=2; *p++=addrs[i].sid; }
struct ll_entry* qe = queue_entry_new(0);
if (!qe) { u_free(pkt); return; }
qe->dgram=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;i<req->addr_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);
}

329
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, резерв, реальные данные и обработку кандидатов.

354
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 <string.h>
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/u_async.h"
#include "etcp_router.h"
#include "utun_instance.h"
#include "topo_group.h"
#include "route_connectivity.h"
/* INDIRECT фаза: отправляет EXCHANGE_REQ с нашими лучшими кандидатами (best_candidates)
* цели через BGP-маршрут. Ожидаем EXCHANGE_RESP — по RTT-замерам с обеих сторон
* выберем общего посредника с минимальной суммой RTT (cm_compute_intermediaries).
* Периодически ретрансмитим EXCHANGE_REQ (1с) — надёжность на случай потери пакета. */
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry) {
struct CONN_MGR* mgr = entry->mgr;
if (!mgr->best_candidate_count) return;
entry->main.request_id = ++mgr->next_request_id;
struct CM_EXCHANGE_REQ req; memset(&req,0,sizeof(req));
req.cmd=ETCP_RT_ID_CONN_MGR; req.subcmd=CM_SUBCMD_INTERM_EXCHANGE_REQ; req.request_id=entry->main.request_id;
req.candidate_count = mgr->best_candidate_count > 4 ? 4 : mgr->best_candidate_count;
for (uint8_t i=0;i<req.candidate_count;i++) req.candidates[i]=mgr->best_candidates[i];
struct ll_entry* qe = queue_entry_new(0); if (!qe) return;
qe->dgram=u_malloc(1+sizeof(req)); qe->dgram[0]=ETCP_RT_ID_CONN_MGR; memcpy(qe->dgram+1,&req,sizeof(req)); qe->len=(uint16_t)(1+sizeof(req));
/* копия для ретрансмита (etcp_route_send забирает владение qe) */
struct cm_exchange_pending* ep = u_calloc(1,sizeof(*ep));
if (ep) {
ep->request_id=entry->main.request_id; ep->entry=entry;
ep->pkt=u_malloc(1+sizeof(req));
if (ep->pkt) { memcpy(ep->pkt, qe->dgram, 1+sizeof(req)); ep->pkt_len=1+sizeof(req); }
ep->next=mgr->exchange_pending; mgr->exchange_pending=ep;
}
etcp_route_send(mgr->instance, mgr->group->group_id, entry->node_id, qe, 1, 0);
entry->indirect_active = 1;
entry->indirect_deadline_tb = get_time_tb() + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS*10;
if (entry->main.timer) { uasync_cancel_timeout(mgr->instance->ua, entry->main.timer); entry->main.timer = NULL; }
entry->main.timer = uasync_set_timeout(mgr->instance->ua, CONN_MGR_EXCHANGE_RETRANSMIT_MS*10, entry, cm_exchange_tick_cb, "conn_mgr_interm_exch");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: phase 3 sent EXCHANGE_REQ to 0x%016llx with %u candidates",
(unsigned long long)entry->node_id, req.candidate_count);
/* Оба плеча посредника должны быть прямыми READY-сессиями. RTT=0/FFFF — нет замера. */
static int cm_valid_candidate(struct CONN_MGR* mgr, uint64_t target, uint64_t nid, uint16_t remote_rtt) {
return nid && nid != mgr->instance->node_id && nid != target && remote_rtt && remote_rtt != UINT16_MAX &&
cm_candidate_rtt(mgr, nid) != UINT16_MAX;
}
/* Периодический тик INDIRECT: ретрансмит EXCHANGE_REQ, по дедлайну — таймаут обмена. */
void cm_exchange_tick_cb(void* arg) {
struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)arg;
entry->main.timer = NULL;
if (!entry->indirect_active) return;
if (get_time_tb() >= entry->indirect_deadline_tb) {
entry->indirect_active = 0;
{ struct cm_exchange_pending** pp=&entry->mgr->exchange_pending;
while(*pp){if((*pp)->entry==entry){struct cm_exchange_pending* ep=*pp; *pp=ep->next; if(ep->pkt)u_free(ep->pkt); u_free(ep); break;} pp=&(*pp)->next;} }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: exchange timeout for 0x%016llx", (unsigned long long)entry->node_id);
if (!entry->reverse_active) cm_deliver_event(entry, CONN_EVENT_TIMEOUT);
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry) {
struct CONN_MGR* mgr = entry->mgr;
if (!entry->indirect_started) {
entry->indirect_started = 1; entry->exchange_request_id = ++mgr->next_request_id;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT start peer=%016llx elapsed=%llums candidates=%u",
(unsigned long long)entry->node_id, (unsigned long long)((get_time_tb() - entry->start_tb) / 10),
mgr->best_candidate_count);
}
entry->indirect_next_tb = get_time_tb() + CONN_MGR_EXCHANGE_RETRANSMIT_MS * 10;
if (entry->selection.count) {
cm_send_control(mgr, entry->node_id, &entry->selection, sizeof(entry->selection));
return;
}
for (struct cm_exchange_pending* x = entry->mgr->exchange_pending; x; x = x->next) {
if (x->entry == entry && x->pkt) {
struct ll_entry* qe = queue_entry_new(0);
if (qe) {
qe->dgram = u_malloc(x->pkt_len);
if (qe->dgram) { memcpy(qe->dgram, x->pkt, x->pkt_len); qe->len = (uint16_t)x->pkt_len;
etcp_route_send(entry->mgr->instance, entry->mgr->group->group_id, entry->node_id, qe, 1, 0); }
else queue_entry_free(qe);
}
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: exchange retransmit EXCHANGE_REQ 0x%016llx", (unsigned long long)entry->node_id);
break;
}
struct CM_EXCHANGE_REQ req = {0};
req.cmd = ETCP_RT_ID_CONN_MGR; req.subcmd = CM_SUBCMD_INTERM_EXCHANGE_REQ; req.request_id = entry->exchange_request_id;
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) {
uint64_t nid = mgr->best_candidates[i].node_id;
uint16_t rtt = cm_candidate_rtt(mgr, nid);
if (nid == entry->node_id || rtt == UINT16_MAX) continue;
req.candidates[req.candidate_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt};
}
entry->main.timer = uasync_set_timeout(entry->mgr->instance->ua, CONN_MGR_EXCHANGE_RETRANSMIT_MS*10,
entry, cm_exchange_tick_cb, "conn_mgr_interm_exch");
// Даже пустой запрос позволяет получить кандидатов другой стороны. Повторы подхватят новые замеры.
cm_send_control(mgr, entry->node_id, &req, sizeof(req));
}
/* Финализация INDIRECT: пометить CONNECTED и доставить UP. Вызывается либо сразу
* (reverse не активен), либо из cm_reverse_tick_cb после reverse-таймаута. */
void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry) {
entry->conn_type = CONN_TYPE_INDIRECT;
entry->state = CONN_MGR_STATE_CONNECTED;
entry->indirect_active = 0; entry->indirect_ready = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT CONNECTED 0x%016llx via %u intermediaries",
(unsigned long long)entry->node_id, entry->intermediariy_count);
cm_update_nodeinfo(entry);
cm_deliver_event(entry, CONN_EVENT_UP);
if (!entry->intermediariy_count) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: empty intermediary selection peer=%016llx", (unsigned long long)entry->node_id);
return;
}
entry->indirect_confirmed = 1;
uint8_t old_type = entry->conn_type;
cm_refresh_path(entry); // Готовый прямой путь имеет приоритет; повторного UP при улучшении нет.
if (entry->state != CONN_MGR_STATE_DISCONNECTED && entry->conn_type == old_type) cm_update_nodeinfo(entry);
}
/* Вычисляет лучших общих посредников по данным из EXCHANGE_RESP:
* your_candidates — кандидаты инициатора + НАШ RTT до них
* my_candidates — НАШИ кандидаты + RTT цели до них
* Суммирует RTT для каждого общего узла, сортирует, выбирает топ-3.
* Отправляет INTERM_SELECTED инициатору и доставляет CONN_EVENT_UP. */
/* Объединение кандидатов обеих сторон без дублей, выбор по сумме двух действительных RTT. */
void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp) {
struct { uint64_t nid; uint64_t rtt; } all[8]; uint8_t cnt=0;
uint8_t your_n = resp->your_count > 4 ? 4 : resp->your_count;
uint8_t my_n = resp->my_count > 4 ? 4 : resp->my_count;
uint64_t self = entry->mgr->instance->node_id, target = entry->node_id;
for (uint8_t i=0;i<your_n&&cnt<8;i++) {
uint64_t nid=resp->your_candidates[i].node_id;
if (nid==self||nid==target) continue; /* сам/цель не может быть посредником */
uint16_t our=0; for(uint8_t j=0;j<CONN_MGR_MAX_CANDIDATES;j++) if(entry->mgr->best_candidates[j].node_id==nid){our=entry->mgr->best_candidates[j].rtt;break;}
all[cnt]=(typeof(all[0])){nid,(uint64_t)our+resp->your_candidates[i].rtt}; cnt++;
}
for (uint8_t i=0;i<my_n&&cnt<8;i++) {
uint64_t nid=resp->my_candidates[i].node_id;
if (nid==self||nid==target) continue; /* сам/цель не может быть посредником */
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(entry->mgr->group,nid);
uint16_t our=nq?topo_get_chain_rtt(nq):0; if(our==0xFFFF)our=0;
all[cnt]=(typeof(all[0])){nid,(uint64_t)our+resp->my_candidates[i].rtt}; cnt++;
}
for (uint8_t i=0;i<cnt;i++) for(uint8_t j=i+1;j<cnt;j++) if(all[j].rtt<all[i].rtt){typeof(all[0])t=all[i];all[i]=all[j];all[j]=t;}
uint8_t sel=cnt>CONN_MGR_MAX_INTERMEDIARIES?CONN_MGR_MAX_INTERMEDIARIES:cnt;
for(uint8_t i=0;i<sel;i++)entry->intermediaries[i]=all[i].nid;
entry->intermediariy_count=sel; entry->rr_idx=0;
struct CM_INTERM_SEL pkt; memset(&pkt,0,sizeof(pkt));
pkt.cmd=ETCP_RT_ID_CONN_MGR; pkt.subcmd=CM_SUBCMD_INTERM_SELECTED; pkt.count=sel;
for(uint8_t i=0;i<sel;i++){pkt.selected[i].node_id=all[i].nid;pkt.selected[i].rtt=0;
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(entry->mgr->group,all[i].nid); if(nq)pkt.selected[i].rtt=topo_get_chain_rtt(nq);}
struct ll_entry* qe=queue_entry_new(0);
if(qe){qe->dgram=u_malloc(1+sizeof(pkt));qe->dgram[0]=ETCP_RT_ID_CONN_MGR;memcpy(qe->dgram+1,&pkt,sizeof(pkt));qe->len=(uint16_t)(1+sizeof(pkt));etcp_route_send(entry->mgr->instance,entry->mgr->group->group_id,entry->node_id,qe,1,0);}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: indirect OK for 0x%016llx via %u intermediaries",(unsigned long long)entry->node_id,sel);
/* reverse приоритетнее: если он ещё идёт — откладываем финализацию (indirect_ready),
* иначе финализируем INDIRECT сразу. */
if (entry->reverse_active) { entry->indirect_ready = 1; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: indirect deferred 0x%016llx (reverse active)", (unsigned long long)entry->node_id); return; }
cm_finalize_indirect(entry);
struct scored_candidate { uint64_t nid; uint32_t rtt; };
struct scored_candidate all[2 * CONN_MGR_MAX_CANDIDATES]; uint8_t count = 0;
for (int side = 0; side < 2; side++) {
uint8_t n = side ? resp->your_count : resp->my_count;
struct CONN_MGR_CANDIDATE* candidates = side ? resp->your_candidates : resp->my_candidates;
for (uint8_t i = 0; i < n; i++) {
uint64_t nid = candidates[i].node_id;
if (!cm_valid_candidate(entry->mgr, entry->node_id, nid, candidates[i].rtt)) continue;
int duplicate = 0;
for (uint8_t j = 0; j < count; j++) if (all[j].nid == nid) duplicate = 1;
if (duplicate) continue;
all[count].nid = nid; all[count++].rtt = (uint32_t)cm_candidate_rtt(entry->mgr, nid) + candidates[i].rtt;
}
}
for (uint8_t i = 0; i < count; i++) for (uint8_t j = i + 1; j < count; j++) {
if (all[j].rtt < all[i].rtt || (all[j].rtt == all[i].rtt && all[j].nid < all[i].nid)) {
struct scored_candidate tmp = all[i]; all[i] = all[j]; all[j] = tmp;
}
}
if (!count) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: no common measured READY intermediary peer=%016llx; retry pending",
(unsigned long long)entry->node_id);
return;
}
struct CM_INTERM_SEL* sel = &entry->selection; memset(sel, 0, sizeof(*sel));
sel->cmd = ETCP_RT_ID_CONN_MGR; sel->subcmd = CM_SUBCMD_INTERM_SELECTED; sel->request_id = entry->exchange_request_id;
sel->count = count > CONN_MGR_MAX_INTERMEDIARIES ? CONN_MGR_MAX_INTERMEDIARIES : count;
for (uint8_t i = 0; i < sel->count; i++) {
sel->selected[i].node_id = all[i].nid; sel->selected[i].rtt = cm_candidate_rtt(entry->mgr, all[i].nid);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: intermediary peer=%016llx via=%016llx sum_rtt=%u",
(unsigned long long)entry->node_id, (unsigned long long)all[i].nid, all[i].rtt);
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT selected peer=%016llx count=%u; verifying delivery",
(unsigned long long)entry->node_id, sel->count);
cm_send_control(entry->mgr, entry->node_id, sel, sizeof(*sel));
}
void cm_exchange_probe_retry_cb(void* arg) {
struct cm_exchange_pending* ep=(struct cm_exchange_pending*)arg; ep->timer=NULL;
uint8_t need=0; uint64_t now=get_time_tb();
for(uint8_t i=0;i<ep->cached.my_count&&i<4;i++){
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(ep->entry->mgr->group,ep->cached.my_candidates[i].node_id);
if(!nq) continue; /* self / не в группе — не посредник, пропускаем */
if(!cm_is_rtt_fresh(nq,now)){need++;break;}
}
if(!need){cm_compute_intermediaries(ep->entry,&ep->cached);struct cm_exchange_pending**pp=&ep->entry->mgr->exchange_pending;while(*pp){if(*pp==ep){*pp=ep->next;break;}pp=&(*pp)->next;}if(ep->pkt)u_free(ep->pkt);u_free(ep);}
else ep->timer=uasync_set_timeout(ep->entry->mgr->instance->ua,2000,ep,cm_exchange_probe_retry_cb,"cm_exch_probe");
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
if (len != sizeof(struct CM_EXCHANGE_RESP)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed EXCHANGE_RESP peer=%016llx len=%zu", (unsigned long long)src, len);
return;
}
struct CM_EXCHANGE_RESP resp; memcpy(&resp, data, sizeof(resp));
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if (!entry || !entry->indirect_started || entry->indirect_confirmed || entry->selection.count ||
entry->exchange_request_id != resp.request_id || get_time_tb() >= entry->deadline_tb) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale EXCHANGE_RESP peer=%016llx request=%u", (unsigned long long)src, resp.request_id);
return;
}
if (resp.my_count > CONN_MGR_MAX_CANDIDATES || resp.your_count > CONN_MGR_MAX_CANDIDATES) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid candidate counts peer=%016llx", (unsigned long long)src); return;
}
cm_compute_intermediaries(entry, &resp);
}
/* Инициатор получил EXCHANGE_RESP: кеширует ответ, проверяет свежесть RTT
* до СВОИХ кандидатов (тех, что в my_candidates ответа). Если протухли —
* probe + retry-таймер 200мс. Если свежие — cm_compute_intermediaries. */
void cm_handle_interm_exchange_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len) {
const struct CM_EXCHANGE_RESP* resp=(const struct CM_EXCHANGE_RESP*)data;
if(len<offsetof(struct CM_EXCHANGE_RESP,your_candidates))return;
if(resp->my_count>4||resp->your_count>4)return;
if(len<offsetof(struct CM_EXCHANGE_RESP,your_candidates)+(size_t)resp->your_count*sizeof(struct CONN_MGR_CANDIDATE))return;
struct cm_exchange_pending* ep=mgr->exchange_pending;
while(ep){if(ep->request_id==resp->request_id)break;ep=ep->next;}
if(!ep){DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: EXCHANGE_RESP req=%u no pending",resp->request_id);return;}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: EXCHANGE_RESP req=%u my=%u your=%u",
resp->request_id, resp->my_count, resp->your_count);
if(ep->entry->main.timer){uasync_cancel_timeout(mgr->instance->ua,ep->entry->main.timer);ep->entry->main.timer=NULL;}
memcpy(&ep->cached,resp,len<sizeof(ep->cached)?len:sizeof(ep->cached)); ep->received=1; ep->probed=0;
uint8_t need=0; uint64_t now=get_time_tb();
for(uint8_t i=0;i<ep->cached.my_count&&i<4;i++){
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(mgr->group,ep->cached.my_candidates[i].node_id);
if(!nq) continue; /* self / не в группе — не посредник, пропускаем */
if(!cm_is_rtt_fresh(nq,now)){need++;route_connectivity_probe_node(mgr->instance,mgr->group,nq);}
}
if(!need){ep->probed=1;cm_compute_intermediaries(ep->entry,&ep->cached);struct cm_exchange_pending**pp=&mgr->exchange_pending;while(*pp){if(*pp==ep){*pp=ep->next;break;}pp=&(*pp)->next;}if(ep->pkt)u_free(ep->pkt);u_free(ep);return;}
ep->timer=uasync_set_timeout(mgr->instance->ua,2000,ep,cm_exchange_probe_retry_cb,"cm_exch_probe");
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
if (len != sizeof(struct CM_INTERM_SEL)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed INTERM_SELECTED peer=%016llx len=%zu", (unsigned long long)src, len);
return;
}
struct CM_INTERM_SEL sel; memcpy(&sel, data, sizeof(sel));
if (!sel.count || sel.count > CONN_MGR_MAX_INTERMEDIARIES) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid intermediary count=%u peer=%016llx", sel.count, (unsigned long long)src); return;
}
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if (!entry) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selection without exchange peer=%016llx", (unsigned long long)src); return; }
if (entry->peer_exchange_request_id != sel.request_id) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale selection peer=%016llx request=%u", (unsigned long long)src, sel.request_id);
return;
}
if (get_time_tb() >= entry->deadline_tb && !entry->indirect_confirmed) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selection after deadline peer=%016llx", (unsigned long long)src);
return;
}
struct CM_INTERM_SEL ack = {0}; ack.cmd = ETCP_RT_ID_CONN_MGR; ack.subcmd = CM_SUBCMD_INTERM_ACK; ack.request_id = sel.request_id;
for (uint8_t i = 0; i < sel.count; i++) {
uint64_t nid = sel.selected[i].node_id;
if (!cm_valid_candidate(mgr, src, nid, sel.selected[i].rtt)) continue;
int duplicate = 0;
for (uint8_t j = 0; j < ack.count; j++) if (ack.selected[j].node_id == nid) duplicate = 1;
if (!duplicate) ack.selected[ack.count++] = (struct CONN_MGR_CANDIDATE){nid, cm_candidate_rtt(mgr, nid)};
}
if (!ack.count) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: selected intermediaries unavailable peer=%016llx", (unsigned long long)src);
cm_send_control(mgr, src, &ack, sizeof(ack)); return;
}
for (uint8_t i = 0; i < ack.count; i++) entry->intermediaries[i] = ack.selected[i].node_id;
entry->intermediariy_count = ack.count;
// Публикуем путь до ACK, чтобы ответ прошёл через выбранного посредника.
cm_finalize_indirect(entry);
cm_send_control(mgr, src, &ack, sizeof(ack));
}
/* Финальный выбор посредников от инициатора (приходит второй стороне INDIRECT-
* обмена). Сохраняет выбранные intermediaries в entry, помечает CONNECTED. */
void cm_handle_interm_selected(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
const struct CM_INTERM_SEL* sel=(const struct CM_INTERM_SEL*)data;
if(len<offsetof(struct CM_INTERM_SEL,selected))return;
uint8_t c=sel->count>CONN_MGR_MAX_INTERMEDIARIES?CONN_MGR_MAX_INTERMEDIARIES:sel->count;
if(len<offsetof(struct CM_INTERM_SEL,selected)+(size_t)c*sizeof(struct CONN_MGR_CANDIDATE))return;
void cm_handle_interm_ack(struct CONN_MGR* mgr, uint64_t src, const uint8_t* data, size_t len) {
if (len != sizeof(struct CM_INTERM_SEL)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed INTERM_ACK peer=%016llx len=%zu", (unsigned long long)src, len); return;
}
struct CM_INTERM_SEL ack; memcpy(&ack, data, sizeof(ack));
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, src);
if(!entry)return;
if(entry->state==CONN_MGR_STATE_CONNECTED)return; /* симметричный случай — UP уже доставлен инициаторским путём */
if(!entry->indirect_active && !entry->reverse_active)return; /* не INDIRECT/REVERSE фаза */
for(uint8_t i=0;i<c;i++)entry->intermediaries[i]=sel->selected[i].node_id;
entry->intermediariy_count=c; entry->rr_idx=0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: got INTERM_SELECTED from 0x%016llx count=%u",(unsigned long long)src,c);
if (entry->reverse_active) { entry->indirect_ready = 1; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: indirect deferred 0x%016llx (reverse active)", (unsigned long long)entry->node_id); return; }
if (!entry || entry->exchange_request_id != ack.request_id || !entry->selection.count || entry->indirect_confirmed ||
get_time_tb() >= entry->deadline_tb) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: stale INTERM_ACK peer=%016llx request=%u", (unsigned long long)src, ack.request_id);
return;
}
if (ack.count > CONN_MGR_MAX_INTERMEDIARIES) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid ACK count=%u peer=%016llx", ack.count, (unsigned long long)src); return;
}
entry->intermediariy_count = 0;
for (uint8_t i = 0; i < ack.count; i++) {
uint64_t nid = ack.selected[i].node_id;
if (!cm_valid_candidate(mgr, src, nid, ack.selected[i].rtt)) continue;
for (uint8_t j = 0; j < entry->selection.count; j++) {
if (entry->selection.selected[j].node_id != nid) continue;
int duplicate = 0;
for (uint8_t k = 0; k < entry->intermediariy_count; k++) if (entry->intermediaries[k] == nid) duplicate = 1;
if (!duplicate) entry->intermediaries[entry->intermediariy_count++] = nid;
break;
}
}
if (!entry->intermediariy_count) {
memset(&entry->selection, 0, sizeof(entry->selection));
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: intermediary verification failed peer=%016llx; repeating exchange", (unsigned long long)src);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT confirmed peer=%016llx count=%u elapsed=%llums",
(unsigned long long)src, entry->intermediariy_count, (unsigned long long)((get_time_tb() - entry->start_tb) / 10));
cm_finalize_indirect(entry);
}
/* Цель получила EXCHANGE_REQ: измеряет RTT до кандидатов инициатора (если
* замеры протухли — запускает probe), отправляет EXCHANGE_RESP со СВОИМИ
* кандидатами + замерами RTT до кандидатов инициатора. */
void cm_handle_interm_exchange_req(uint64_t src, struct CONN_MGR* mgr, struct CM_EXCHANGE_REQ* req) {
if (!mgr) return;
if (req->candidate_count > 4) return;
uint64_t now=get_time_tb();
for(uint8_t i=0;i<req->candidate_count&&i<4;i++){
struct TOPO_GROUP_NODE* nq=topo_node_find_by_id(mgr->group,req->candidates[i].node_id);
if(nq&&cm_is_rtt_fresh(nq,now))continue; route_connectivity_probe_node(mgr->instance,mgr->group,nq);
}
struct CM_EXCHANGE_RESP resp; memset(&resp,0,sizeof(resp));
resp.cmd=ETCP_RT_ID_CONN_MGR; resp.subcmd=CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id=req->request_id;
resp.my_count=mgr->best_candidate_count>4?4:mgr->best_candidate_count;
for(uint8_t i=0;i<resp.my_count;i++)resp.my_candidates[i]=mgr->best_candidates[i];
resp.your_count=req->candidate_count>4?4:req->candidate_count;
for(uint8_t i=0;i<resp.your_count;i++){
resp.your_candidates[i].node_id=req->candidates[i].node_id;
struct TOPO_GROUP_NODE* nq2=topo_node_find_by_id(mgr->group,req->candidates[i].node_id);
resp.your_candidates[i].rtt=nq2?topo_get_chain_rtt(nq2):0;
}
size_t sz=offsetof(struct CM_EXCHANGE_RESP,your_candidates)+(size_t)resp.your_count*sizeof(struct CONN_MGR_CANDIDATE);
struct ll_entry* qe=queue_entry_new(0);
if(qe){qe->dgram=u_malloc(1+sz);qe->dgram[0]=ETCP_RT_ID_CONN_MGR;memcpy(qe->dgram+1,&resp,sz);qe->len=(uint16_t)(1+sz);etcp_route_send(mgr->instance,mgr->group->group_id,src,qe,1,0);}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,"conn_mgr: sent EXCHANGE_RESP to 0x%016llx my=%u your=%u",(unsigned long long)src,resp.my_count,resp.your_count);
if (req->candidate_count > CONN_MGR_MAX_CANDIDATES || !topo_node_registry_find(mgr->instance->topo_groups, src)) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid EXCHANGE_REQ peer=%016llx count=%u", (unsigned long long)src, req->candidate_count);
return;
}
struct CONN_MGR_ENTRY* entry = cm_ensure_entry(mgr, src);
if (!entry) return;
entry->peer_exchange_request_id = req->request_id;
if (entry->state == CONN_MGR_STATE_DISCONNECTED) {
entry->state = CONN_MGR_STATE_CONNECTING; entry->start_tb = get_time_tb();
entry->deadline_tb = entry->start_tb + CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS * 10;
cm_arm_timer(entry);
}
struct CM_EXCHANGE_RESP resp = {0};
resp.cmd = ETCP_RT_ID_CONN_MGR; resp.subcmd = CM_SUBCMD_INTERM_EXCHANGE_RESP; resp.request_id = req->request_id;
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) {
uint64_t nid = mgr->best_candidates[i].node_id; uint16_t rtt = cm_candidate_rtt(mgr, nid);
if (nid == src || rtt == UINT16_MAX) continue;
resp.my_candidates[resp.my_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt};
}
for (uint8_t i = 0; i < req->candidate_count; i++) {
uint64_t nid = req->candidates[i].node_id; uint16_t rtt = cm_candidate_rtt(mgr, nid);
if (nid == src || nid == mgr->instance->node_id || rtt == UINT16_MAX) continue;
resp.your_candidates[resp.your_count++] = (struct CONN_MGR_CANDIDATE){nid, rtt};
}
cm_send_control(mgr, src, &resp, sizeof(resp));
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: EXCHANGE_RESP peer=%016llx request=%u my=%u your=%u",
(unsigned long long)src, req->request_id, resp.my_count, resp.your_count);
}

55
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);

26
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;
}
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.

6
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=ошибка. Доставка подтверждается отдельно протоколом.

2
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 текущего обмена. Сбой обмена

24
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; }

2
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;

258
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);

2
tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt

@ -21,7 +21,7 @@ object AppEventHandler : ChatEventCallback {
val events: SharedFlow<Pair<Int, ByteArray?>> = _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}")

14
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)}")
}
}
}

9
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
}

3
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))

2
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)) {

13
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;

2
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;

5
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) {

4
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);

14
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;
}

Loading…
Cancel
Save