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 AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка.
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`, Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`,
обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних обёртка `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) */ uint8_t no_route_strikes; /* caller: подряд ретрансмитов без маршрута (fail-fast) */
void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */ void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */
void* teardown_timer; /* отложенный разрыв conn_mgr (500мс после завершения) */ 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) */ uint64_t last_media_tb; /* время последнего CALL_MEDIA (0.1ms) */
uint16_t tx_media_seq; /* seq следующего исходящего CALL_MEDIA */ uint16_t tx_media_seq; /* seq следующего исходящего CALL_MEDIA */
struct ll_queue* tx_q; /* FIFO закодированных CALL_MEDIA (backpressure) */ 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]; char text[512];
int off = 0; int off = 0;
if (hc <= 1) { if (!hc) {
off = snprintf(text, sizeof(text), "маршрут: прямой"); off = snprintf(text, sizeof(text), "BGP: нет маршрута");
} else if (hc == 1) {
off = snprintf(text, sizeof(text), "BGP: прямой");
} else { } 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--) { for (int i = hc - 1; i >= 1 && off < (int)sizeof(text) - 48; i--) {
char hop[48]; char hop[48];
struct TOPO_NODE* ni = topo_node_registry_find(ctx->inst->topo_groups, hops[i]); 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); 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 (после того, /* Отложенный разрыв: единственная точка, где закрывается 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->role == CALL_ROLE_CALLER ? "caller" : "callee",
s->c_rx_frames, s->c_tx_frames, s->c_tx_dropped); 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); call_event_id(ctx, CHAT_EVT_CALL_ENDED, s->call_id, reason);
/* Закрытие запроса CM откладываем на 500мс для отправки финального сигналинга. */ /* Закрытие запроса 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) { if (event == CONN_EVENT_UP) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: CM path ready id=%016llx peer=%016llx", CALL_ID, 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); (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 { } else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: CM event=%d id=%016llx peer=%016llx; delivery continues via router", 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_ID, event, (unsigned long long)s->call_id, (unsigned long long)node_id);
} }
call_watch_connection(s);
(void)h; (void)group_id; (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, DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: accepted id=%016llx -> ACTIVE (router delivery)", CALL_ID,
(unsigned long long)s->call_id); (unsigned long long)s->call_id);
call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0); 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->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. */ /* Периодическое событие CALL_STATS (раз в 1с): rtt_ms, buffer_ms, tempo_x100. */
static void call_stats_timer_cb(void* arg) { static void call_post_stats(struct call_session* s) {
struct call_session* s = (struct call_session*)arg;
if (!s || !s->ctx) return;
struct call_ctx* ctx = s->ctx; struct call_ctx* ctx = s->ctx;
s->stats_timer = NULL;
if (s->state != CALL_ST_ACTIVE) return;
int buffer_ms = 0, tempo_x100 = 100; int buffer_ms = 0, tempo_x100 = 100;
struct call_jitter_range range = {0, 0, 60}; 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) if (ctx->audio_ops && ctx->audio_ops->get_stats)
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns, &range); 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 = struct ETCP_ROUTER_CONN* rconn =
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL); 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]; uint8_t buf[24];
size_t off = 0; 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; buf[off++] = (uint8_t)(value >> 8); buf[off++] = (uint8_t)value;
} }
chat_event_post(ctx->inst, CHAT_EVT_CALL_STATS, buf, (int)off); 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); (unsigned long long)group, (unsigned long long)inv_group);
call_event_incoming(ctx, group, from, call_id); call_event_incoming(ctx, group, from, call_id);
call_post_bgp_path(ctx, s); call_watch_connection(s);
/* watchdog: вызывающий шлёт кодограммы INVITE каждые 300мс; 2с тишины → вызов сорван */ /* 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"); 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); call_end(ctx, s, CALL_REASON_CONNECT_FAIL);
return; 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->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"); 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): звонок можно инициировать из // Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core). // CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение // Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
// устанавливается через conn_mgr (3-фазное DIRECT→REVERSE→INDIRECT). // оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT).
// //
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline // Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
// (gui_bridge_post_uasync_fn / uasync_post). // (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", [37]="CALL_ENDED", [38]="CALL_DECLINED", [39]="CALL_STATS",
[40]="CALL_ERROR", [41]="DM_MESSAGES", [42]="CALL_PATH", [40]="CALL_ERROR", [41]="DM_MESSAGES", [42]="CALL_PATH",
[43]="RADIO_TALK", [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); 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); 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_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_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_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); 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 handle и не привязан временем жизни к одному ETCP_CONN.
* *
* NCD обеспечивает общий прямой мультиадресный транспорт для BGP/topo_group. * NCD обеспечивает общий прямой мультиадресный транспорт для BGP/topo_group.
* NCD handle попытки CM после установления связи должен перейти группе либо быть закрыт; * CM удерживает свой NCD handle для позднего UP и мониторинга прямого пути;
* группа приобретает независимое NCD-владение при согласовании своей сессии.
* CM handle остаётся у медиа-сервиса. Закрытие CM handle отменяет его запрос, а не владение группы. * CM handle остаётся у медиа-сервиса. Закрытие CM handle отменяет его запрос, а не владение группы.
* Это контракт разделения ролей; механизм передачи транспорта группе относится к NCD/topo_group.
* Групповые сессии открывают через topo_group_peer_open(). Все операции — в потоке uasync. */ * Групповые сессии открывают через topo_group_peer_open(). Все операции — в потоке uasync. */
#ifndef CONN_MGR_H #ifndef CONN_MGR_H
#define 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 обязателен. * Возврат 0 — запрос принят, -1 — ошибка запуска. cb обязателен.
* Ошибка до запуска может возвращаться без callback. В callback h может быть NULL. * Ошибка до запуска может возвращаться без callback. В callback h может быть NULL.
* Успешно выданный handle закрывает вызывающий, в том числе после TIMEOUT. * Успешно выданный 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); struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h);
/** Передать e->dgram через маршрутизатор CM. Освобождает entry при ненулевом 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, сортировка по возрастанию). */ /** Обновить RTT кандидата-посредника (≤3, сортировка по возрастанию). */
void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uint16_t rtt); 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 #ifdef __cplusplus
} }
#endif #endif

865
src/routing_layer/conn_mgr_core.c

@ -31,14 +31,19 @@
/* ═══════ forward-декларации ═══════ */ /* ═══════ forward-декларации ═══════ */
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); 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, 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) { conn_mgr_cb_t cb, void* cb_arg) {
struct CONN_MGR_HANDLE* h = u_calloc(1, sizeof(*h)); 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->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->ua = entry->mgr->instance->ua;
h->entry = entry; h->next = entry->handles; entry->handles = h; 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; return CM_V6_DIR;
} }
uint8_t cm_sock_v6_classify(const struct ETCP_SOCKET* s) { /* Та же классификация, что при создании линков; EIM и прямой IPv6 доступны извне. */
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. */
int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq) { int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq) {
if (!nq) return 0; struct TOPO_NODE* ni = nq ? topo_node_registry_find(mgr->instance->topo_groups, nq->node_id) : NULL;
struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id);
if (!ni) return 0; 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) { 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) 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] if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PUB) return 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;
} }
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; 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; 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)); 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; 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; 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) { void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) {
if (!entry) return; if (!entry) return;
// Сначала исключаем из поиска и запрещаем NCD callbacks; освобождение памяти — вне текущего стека.
struct CONN_MGR* mgr = entry->mgr; struct CONN_MGR* mgr = entry->mgr;
if (cm_find_entry(mgr, entry->node_id) != entry) return; if (cm_find_entry(mgr, entry->node_id) != entry) return;
entry->state = CONN_MGR_STATE_DISCONNECTED; entry->state = CONN_MGR_STATE_DISCONNECTED;
queue_remove_data(mgr->entries, &entry->ll); 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) { for (struct CONN_MGR_HANDLE* h = entry->handles; h; h = h->next) {
if (h->deliver_up_token) { if (h->deliver_up_token) { uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL; }
uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL; h->entry = NULL; // Владелец обязан закрыть отсоединённый handle.
}
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);
} }
entry->handles = NULL; 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; struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL;
if (ncd) node_conn_direct_force_close(ncd); if (ncd) node_conn_direct_force_close(ncd);
cm_clear_nodeinfo(entry->mgr, entry->node_id); cm_clear_nodeinfo(mgr, entry->node_id);
entry->state = CONN_MGR_STATE_DISCONNECTED; entry->conn_type = CONN_TYPE_NONE; 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); 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); topo_nodeq_remove_node(group, nq);
} }
/* ═══════ NCD коллбэки ═══════ */ /* Прямой транспорт пригоден только вместе с READY-сессией этой группы. */
struct ETCP_CONN* cm_direct_conn(struct CONN_MGR* mgr, uint64_t node_id) {
/* NCD (node conn direct) события → conn_mgr для DIRECT/REVERSE фаз: struct ETCP_CONN* conn = topo_group_find_conn_for_node(mgr->group, node_id);
* UP — соединение поднялось, помечаем CONNECTED, доставляем UP всем handle'ам. return conn && conn->peer_node_id == node_id && conn->links_up && !conn->close_requested &&
* TIMEOUT — NCD таймаут истёк, переходим к REVERSE или INDIRECT, либо фейлим db_node. topo_group_peer_ready(mgr->group, node_id) ? conn : NULL;
* 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; /* READY первого плеча недостаточно: посредник должен ещё объявлять маршрут к получателю. */
if (!entry || !entry->mgr || entry->state == CONN_MGR_STATE_DISCONNECTED) return; static struct ETCP_CONN* cm_intermediary_conn(struct CONN_MGR_ENTRY* entry, uint64_t node_id) {
switch (ncd_ev) { struct ETCP_CONN* conn = cm_direct_conn(entry->mgr, node_id);
case NCD_EVENT_UP: struct TOPO_GROUP_NODE* target = topo_node_find_by_id(entry->mgr->group, entry->node_id);
entry->ncd_down_notified = 0; for (struct ll_entry* e = conn && target && target->paths ? target->paths->head : NULL; e; e = e->next)
if (entry->state == CONN_MGR_STATE_CONNECTED) return; if (((struct TOPO_NODEPATH*)e)->conn == conn) return conn;
{ int won = entry->reverse_active ? CONN_TYPE_REVERSE : CONN_TYPE_DIRECT; return NULL;
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", uint16_t cm_candidate_rtt(struct CONN_MGR* mgr, uint64_t node_id) {
entry->reverse_active, entry->indirect_active); } struct ETCP_CONN* conn = cm_direct_conn(mgr, node_id);
if (entry->main.timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->main.timer); entry->main.timer = NULL; } if (!conn) return UINT16_MAX;
if (entry->reverse_timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->reverse_timer); entry->reverse_timer = NULL; } uint16_t rtt = conn->rtt_last;
entry->main_connect_state = CM_TRY_OK; if (!rtt) {
entry->conn_type = entry->reverse_active ? CONN_TYPE_REVERSE : CONN_TYPE_DIRECT; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(mgr->group, node_id);
entry->state = CONN_MGR_STATE_CONNECTED; // Нулевой текущий RTT на loopback допустим, если успешный замер уже был.
entry->reverse_active = 0; entry->indirect_active = 0; entry->indirect_ready = 0; if (conn->last_rtt_cb_time) return 1;
{ struct cm_reverse_pending** pp = &entry->mgr->reverse_pending; if (nq && cm_is_rtt_fresh(nq, get_time_tb())) {
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; } rtt = nq->connectivity.real_min_rtt;
} if (!rtt) rtt = nq->connectivity.nat_min_rtt;
{ struct cm_exchange_pending** pp = &entry->mgr->exchange_pending; if (!rtt) rtt = nq->connectivity.interface_min_rtt;
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;
} }
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->main_connect_state = CM_TRY_FAILED;
/* Entry без ожидающих handle (цель REVERSE) — эскалация бессмысленна, просто чистим */ DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: direct probe timeout peer=%016llx; retaining transport path=%u",
if (!entry->handles) { (unsigned long long)entry->node_id, entry->conn_type);
{ struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL; } else {
if (ncd) node_conn_direct_force_close(ncd); } entry->direct_up = 0;
cm_entry_cleanup(entry); if (ev == NCD_EVENT_CLOSED) { entry->ncd_handle = NULL; node_conn_direct_close(ncd_h); }
return; DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: direct transport lost peer=%016llx event=%d",
} (unsigned long long)entry->node_id, ev);
/* Инициатор с handle'ами: параллельный запуск REVERSE + INDIRECT. Повторные }
* NCD-таймауты (шаримый NCD оживляется другими модулями) игнорируем — иначе cm_refresh_path(entry);
* протекут таймеры и выстрелят в освобождённый entry. */ if (entry->state != CONN_MGR_STATE_DISCONNECTED) cm_arm_timer(entry);
if (entry->reverse_active || entry->indirect_active) return; }
cm_start_parallel(entry);
break; int cm_send_control(struct CONN_MGR* mgr, uint64_t node_id, const void* packet, size_t len) {
case NCD_EVENT_CLOSED: struct ll_entry* qe = ll_alloc_lldgram(1 + len);
if (entry->ncd_handle == ncd_h) entry->ncd_handle = NULL; if (!qe) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: control allocation failed peer=%016llx", (unsigned long long)node_id); return -1; }
node_conn_direct_close(ncd_h); qe->dgram[0] = ETCP_RT_ID_CONN_MGR; memcpy(qe->dgram + 1, packet, len); qe->len = (uint16_t)(1 + len);
if (entry->state != CONN_MGR_STATE_CONNECTED) { int ret = etcp_route_send(mgr->instance, mgr->group->group_id, node_id, qe, 1, 0);
cm_ncd_callback(NULL, NCD_EVENT_TIMEOUT, entry); if (ret < 0) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: control send failed peer=%016llx ret=%d",
return; (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 ═══════ */ /* ═══════ 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)); struct CONN_MGR* mgr = u_calloc(1, sizeof(struct CONN_MGR));
if (!mgr) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "alloc failed"); return NULL; } if (!mgr) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "alloc failed"); return NULL; }
mgr->instance = group->instance; mgr->group = group; 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, mgr->entries = queue_new(mgr->instance->ua, 256,
offsetof(struct CONN_MGR_ENTRY, node_id) - sizeof(struct ll_entry), offsetof(struct CONN_MGR_ENTRY, node_id) - sizeof(struct ll_entry),
8, "conn_mgr_entries"); 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) { void conn_mgr_destroy(struct CONN_MGR* mgr) {
if (!mgr) return; if (!mgr) return;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 0 enter mgr=%p entries=%p exch=%p rev=%p", mgr->initialized = 0;
mgr, mgr->entries, mgr->exchange_pending, mgr->reverse_pending); if (mgr->bg_ping_timer) uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer);
if (mgr->bg_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer); mgr->bg_ping_timer = NULL; } if (mgr->candidate_ping_timer) uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer);
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");
#ifdef UTUN_HAVE_STANDBY #ifdef UTUN_HAVE_STANDBY
if (mgr->bg_ping_wait) { standby_wait_cancel(mgr->bg_ping_wait); mgr->bg_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); mgr->candidate_ping_wait = NULL; } if (mgr->candidate_ping_wait) standby_wait_cancel(mgr->candidate_ping_wait);
#endif #endif
/* Досрочно закрыть влётные candidate TCP-пинги — иначе их stcp_client утечёт до uasync_destroy. */
for (int i = 0; i < CONN_MGR_MAX_CANDIDATES; i++) 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; } if (mgr->cand_tcp_pings[i]) stcp_client_destroy(mgr->cand_tcp_pings[i]);
{ size_t ec = queue_entry_count(mgr->entries); while (mgr->entries->head) cm_entry_cleanup((struct CONN_MGR_ENTRY*)mgr->entries->head);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries count=%zu", ec); queue_free(mgr->entries);
struct ll_entry* e = mgr->entries->head; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: destroyed group=%016llx", (unsigned long long)mgr->group->group_id);
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); }
u_free(mgr); 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); struct TOPO_NODE* ni = topo_node_sqlite_node_load(mgr->instance->topo_sqlite_db, mgr->instance->topo_groups, node_id);
if (ni) { if (ni) {
ni = topo_node_registry_store(mgr->instance->topo_groups, 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) { if (qe) {
target = (struct TOPO_GROUP_NODE*)qe; target = (struct TOPO_GROUP_NODE*)qe;
memset((uint8_t*)target + sizeof(struct ll_entry), 0, sizeof(*target) - sizeof(struct ll_entry)); 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); 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); struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg);
if (!h) return -1; if (out_handle) *out_handle = h; 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; 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); 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); 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; 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); entry = cm_ensure_entry(mgr, node_id);
if (!entry) return -1; 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); 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; 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; if (entry->timer) { uasync_cancel_timeout(mgr->instance->ua, entry->timer); entry->timer = NULL; }
entry->db_loaded = (uint8_t)loaded_from_db; if (already_up) h->deliver_up_token = uasync_call_soon(mgr->instance->ua, h, cm_deliver_up_cb);
if (cm_has_local_addr(mgr, group->local_node) && cm_has_local_addr(mgr, target)) entry->last_traffic_tb = entry->start_tb = get_time_tb();
{ entry->local_scan_state = CM_TRY_PENDING; cm_start_local_scan(entry); } entry->timeout_notified = 0;
entry->main_connect_state = CM_TRY_PENDING; cm_start_phase_direct(entry); 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; 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) { struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h) {
if (!h || h->closed || !h->entry || !h->entry->ncd_handle) return NULL; if (!h || h->closed || !h->entry || h->entry->state != CONN_MGR_STATE_CONNECTED ||
return node_conn_direct_get_conn(h->entry->ncd_handle); 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) { 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; int ret = etcp_router_conn_send(r, e->dgram, e->len, 0); queue_entry_free(e); return ret;
} }
/* ═══════ локальное сканирование ═══════ */ /* Локальные пробы уже созданы DIRECT через NCD. Проверяем все LAN-адреса,
* не дублируя handshake и не создавая отдельные ping-контексты. */
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 фазой. */
void cm_start_local_scan(struct CONN_MGR_ENTRY* entry) { void cm_start_local_scan(struct CONN_MGR_ENTRY* entry) {
struct TOPO_GROUP* group = entry->mgr->group; struct TOPO_NODE* ni = topo_node_registry_find(entry->mgr->instance->topo_groups, entry->node_id);
struct TOPO_GROUP_NODE* target = topo_node_find_by_id(group, entry->node_id); if (!ni || !entry->ncd_handle) {
struct TOPO_NODE* ni = target ? topo_node_registry_find(entry->mgr->instance->topo_groups, target->node_id) : NULL; entry->local_scan_state = CM_TRY_FAILED;
if (!target || !ni) { entry->local_scan_state = CM_TRY_FAILED; return; } DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: LOCAL probe missing node/transport peer=%016llx",
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { (unsigned long long)entry->node_id); return;
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;
} }
if (++ctx->attempt < CONN_MGR_LOCAL_SCAN_ATTEMPTS && ctx->phase == 0) { int addresses = 0;
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(entry->mgr->group, entry->node_id); for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
struct TOPO_NODE* pub_ni = nq ? topo_node_registry_find(entry->mgr->instance->topo_groups, nq->node_id) : NULL; if (!(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) continue;
etcp_send_ping_to_socket(entry->mgr->instance, ctx->sock, pub_ni ? pub_ni->public_key : NULL, for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next)
&ctx->addr, CONN_MGR_LOCAL_SCAN_TIMEOUT_MS, cm_ping_cb, ctx, NULL, 0, 0); if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PRIV) {
return; addresses++; break;
}
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++; }
}
} }
}
} }
/* v6 */ for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) {
for (const struct TOPO_ADDR6* a6=ni->v6_addrs;a6;a6=a6->next) { uint8_t cls = cm_classify_v6_addr(a->addr);
int is_ll = (a6->addr[0] == 0xfe && (a6->addr[1] & 0xc0) == 0x80); if ((a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP)) && (cls == CM_V6_LL || cls == CM_V6_LOC)) addresses++;
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;}
}
} }
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) { void cm_start_phase_direct(struct CONN_MGR_ENTRY* entry) {
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]", if (!entry->ncd_handle && node_conn_direct_open(entry->mgr->instance, entry->node_id, cm_ncd_callback, entry,
(unsigned long long)entry->node_id, db_loaded, v4_cnt, v4_tcp, v6_cnt, v6_tcp, conn->log_name); &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; return;
} }
node_conn_direct_close(entry->ncd_handle); entry->ncd_handle=NULL; // Входящий REVERSE добавляет новые адреса, не заменяя уже идущие пробы.
if (db_loaded) { if (node_conn_direct_update_node(entry->mgr->instance, entry->node_id) < 0)
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: db_node 0x%016llx direct failed (no compatible addr/socket)", (unsigned long long)entry->node_id); DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: address update failed peer=%016llx", (unsigned long long)entry->node_id);
cm_cleanup_db_node(entry); cm_deliver_event(entry, CONN_EVENT_TIMEOUT); if (!entry->direct_up) entry->main_connect_state = CM_TRY_PENDING;
} else { DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: DIRECT probes peer=%016llx elapsed=%llums transport=%p",
cm_start_parallel(entry); (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), затем /* Запрос использует подписанный снимок отправителя: одинаковые правила IPv4/IPv6, UDP/TCP и Reality с DIRECT. */
* вручную добавляет 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с) — надёжность на случай потери пакета. */
void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry) { void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry) {
struct TOPO_GROUP* group = entry->mgr->group; uint64_t now = get_time_tb();
struct TOPO_GROUP_NODE* local = group->local_node; if (!entry->reverse_started) {
struct TOPO_NODE* lni = local ? topo_node_registry_find(entry->mgr->instance->topo_groups, local->node_id) : NULL; entry->reverse_started = 1; entry->reverse_request_id = ++entry->mgr->next_request_id;
if (!local || !lni) return; entry->reverse_deadline_tb = now + CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS * 10;
uint32_t req_id = ++entry->mgr->next_request_id; entry->main.request_id = req_id; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: REVERSE start peer=%016llx elapsed=%llums",
uint8_t dc = 0; struct { uint8_t t; uint8_t ip[4]; uint16_t port; uint8_t sid; } addrs[8]; (unsigned long long)entry->node_id, (unsigned long long)((now - entry->start_tb) / 10));
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;
}
} }
entry->reverse_timer = uasync_set_timeout(entry->mgr->instance->ua, CONN_MGR_REVERSE_RETRANSMIT_MS*10, if (now >= entry->reverse_deadline_tb) { entry->reverse_next_tb = UINT64_MAX; return; }
entry, cm_reverse_tick_cb, "conn_mgr_reverse"); 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) { void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* data, size_t len) {
if (!mgr) return; if (!mgr || len != sizeof(struct CM_DIRECT_REQ)) {
struct CM_DIRECT_REQ* req = (struct CM_DIRECT_REQ*)data; DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed DIRECT_REQ peer=%016llx len=%zu", (unsigned long long)src, len);
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; } return;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: got DIRECT_REQ from 0x%016llx with %u addrs", }
(unsigned long long)src, req->addr_count); (void)data;
struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, src);
struct TOPO_NODE* src_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; }
if (!src_ni) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: DIRECT_REQ from unknown node 0x%016llx", (unsigned long long)src); return; } struct CONN_MGR_ENTRY* entry = cm_ensure_entry(mgr, src);
/* Целевая сторона 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 (!entry) return; if (!entry) return;
entry->state = CONN_MGR_STATE_CONNECTING; entry->reverse_active = 1; if (entry->state == CONN_MGR_STATE_DISCONNECTED) {
entry->main_connect_state = CM_TRY_PENDING; entry->db_loaded = 0; entry->conn_type = CONN_TYPE_NONE; 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;
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->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 ═══════ */ /* ═══════ 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 group_id; memcpy(&group_id, entry->dgram + ROUTER_SVC_GROUP_OFF, 8);
uint64_t src; memcpy(&src, entry->dgram + ROUTER_SVC_SRC_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 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(); } 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) { switch (sub) {
case CM_SUBCMD_DIRECT_REQ: cm_handle_direct_req(src, mgr, d, len); break; case CM_SUBCMD_DIRECT_REQ: cm_handle_direct_req(src, mgr, d, len); break;
case CM_SUBCMD_DIRECT_RESP: 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_REQ:
case CM_SUBCMD_INTERM_EXCHANGE_RESP: if (mgr) cm_handle_interm_exchange_resp(mgr, d, len); break; 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_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); queue_dgram_free(entry); queue_entry_free(entry);
} }

329
src/routing_layer/conn_mgr_doc.md

@ -1,215 +1,114 @@
# conn_mgr — Менеджер установки соединений # Connection Manager
## 1. Назначение `conn_mgr` подбирает путь для медиа, файлов и звонков. Один менеджер принадлежит
Трёхфазный асинхронный менеджер подключения к удалённым нодам. Отвечает за установку ETCP-соединения с выбором оптимального способа: одной группе. Несколько сервисов могут удерживать свои handles к одному узлу:
транспорт и пробы общие, callbacks и отмена принадлежат каждому владельцу.
1. **DIRECT** — прямое INIT-рукопожатие со всеми известными адресами цели (IPv4/IPv6, TCP/UDP, с NAT-фильтрацией через `sock_match`) Все операции выполняются в uasync-потоке.
2. **REVERSE** — если у нас прямой IP, а цель за NAT: отправляем наши адреса через BGP (`DIRECT_REQ`), цель подключается к нам
3. **INDIRECT** — обмениваемся через BGP списками кандидатов-посредников, выбираем общих по минимальной сумме RTT, трафик идёт через `etcp_router` ## Расписание подключения
Также выполняет фоновые задачи: периодический ping всех BGP-нод, поддержание списка лучших посредников, idle-таймаут для неактивных соединений, локальное сканирование сети (LAN broadcast ping). Все сроки отсчитываются от одного `start_tb`, а не от таймаута предыдущей пробы.
Один `CONN_MGR` обслуживает одну `TOPO_GROUP` (создаётся в `conn_mgr_init(group)`). Внутри — кеш `entries` (по одной записи на целевой узел) и список `handle`'ов (по одному на каждый вызов `conn_mgr_open`). | Доступность извне | DIRECT | LOCAL | REVERSE | INDIRECT |
|---|---|---|---|---|
## 2. Как пользоваться | Мы доступны, пир нет | Сразу | В составе DIRECT | Сразу | 1000 мс |
| Оба доступны | Сразу | В составе DIRECT | 500 мс | 1000 мс |
### Инициализация | Мы недоступны, пир доступен | Сразу | В составе DIRECT | 500 мс | 1000 мс |
```c | Оба недоступны | Сразу | Сразу | 500 мс | Сразу |
struct CONN_MGR* mgr = conn_mgr_init(group);
// Регистрируется в etcp_router через conn_mgr_router_recv_handler (ETCP_RT_ID_CONN_MGR) Доступность определяется по опубликованным адресам и общей классификации
// Запускает фоновые таймеры: bg_ping, candidate_ping `sock_match`: публичный IPv4, EIM/прямой NAT, пригодный глобальный IPv6.
``` Приватные адреса EIM-интерфейса не считаются внешними.
### Подключение (handle-based) DIRECT использует NCD, который создаёт UDP/TCP-линки по известной записи узла,
```c включая IPv4/IPv6, LAN и Reality-параметры. LOCAL — те же локальные handshake-пробы:
static void my_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, отдельные ping-контексты и повторные линки не создаются.
enum conn_mgr_event event, void* arg) { REVERSE просит получателя открыть/дополнить NCD к отправителю по уже известному
if (event == CONN_EVENT_UP) { /* связь есть */ } подписанному снимку. Запрос обрабатывается и при CONNECTING, и при работе через
else if (event == CONN_EVENT_DOWN) { /* временный обрыв, сам восстановится */ } посредника. Повторы запроса идут раз в секунду в течение трёх секунд.
else if (event == CONN_EVENT_TIMEOUT) { /* не удалось, закройте handle */ }
} Запуск нового способа не закрывает NCD и не удаляет предыдущие линки. NCD TIMEOUT
по умолчанию через две секунды завершает только ожидание первой прямой попытки;
struct CONN_MGR_HANDLE* h = NULL; удерживаемый handle продолжает получать поздний UP.
conn_mgr_open(inst, group_id, node_id, my_cb, arg, &h);
``` ## Рабочий путь
### Отправка данных Состояние доставки (`state`, `conn_type`) отделено от состояния проб.
```c Посредник используется сразу после успешного согласования, пока прямой путь
struct ll_entry* e = queue_entry_new(0); ещё устанавливается. Прямой транспорт становится рабочим после READY сессии
e->dgram = data; e->len = len; нужной группы. Тогда он имеет приоритет независимо от порядка ответов INDIRECT.
conn_mgr_send(h, e); // забирает владение e, освобождает внутри
struct ETCP_CONN* conn = conn_mgr_get_conn(h); // NULL для INDIRECT Переходы между посредником и прямым путём не сбрасывают состояние `ETCP_ROUTER_CONN`,
``` его очереди, seq и неподтверждённые пакеты. Список подтверждённых посредников
сохраняется как резерв. Для выбора посредника проверяются READY первого плеча
### Закрытие и наличие объявления маршрута к получателю именно от этого посредника.
```c
conn_mgr_close(h); // убирает ОДИН handle; последний рвёт соединение DIRECT/REVERSE определяются по текущим живым линкам: наличие исходящего означает
``` DIRECT, только входящие — REVERSE. Метка первого UP не используется как постоянный тип.
Окно звонка показывает фактический путь router, включая BGP fallback до готовности CM:
### Кандидаты-посредники DIRECT/REVERSE/INDIRECT/NONE. Событие `CHAT_EVT_CALL_CONNECTION` содержит call_id,
`cm_candidate_ping_timer_cb` (раз в ~2с) пополняет `best_candidates` (топ-3 по RTT) из узлов группы с активным соединением и пингует/probe'ит их. Список используется фазой INDIRECT. Внешний код (`route_connectivity`) лишь заполняет `nq->connectivity`, откуда берутся свежие RTT. тип и next hop; публикуется при изменении. Проверка идёт раз в секунду общим таймером
звонка, сразу при начале/принятии и при событиях CM. Завершение сбрасывает статус
### Нюансы и отменяет таймер. Отображение одинаково в Qt и Android.
- Несколько `conn_mgr_open` на один узел → несколько handle на одном entry; все получают события
- Повторный `open` для уже подключённой ноды вернёт handle сразу, UP доставляется отложенно (`uasync_call_soon`) Один timer на entry обслуживает ближайшее запланированное действие, повторы и
- Для `open` в состоянии CONNECTING новый handle добавляется в список ожидания общий предел ожидания. READY и изменение маршрутов перепроверяют пути через
- Для INDIRECT-соединений `conn_mgr_get_conn` возвращает `NULL`, `conn_mgr_send` идёт через `etcp_router` (первый посредник с живым conn) `conn_mgr_routes_changed()`, без периодического опроса работающих соединений.
- Пока есть активный ETCP-линк к ноде — соединение считается DIRECT Начальное ожидание ограничено 15 секундами от срока запуска INDIRECT, даже если
нет кандидатов/RTT/ответа. Получение ответа не отменяет этот предел.
## 3. API Входящие записи без сервисных handles удаляются по этому сроку; групповые владельцы
сохраняют общий транспорт.
### События (enum conn_mgr_event)
После готовности прямого пути новые дополнительные пробы не запускаются.
| Событие | Значение | Смысл | Уже начатое согласование INDIRECT может завершиться и сохранить резерв.
|---------|----------|-------| После начального бюджета поздние NCD/READY всё ещё могут улучшить путь. Бесконечные
| `CONN_EVENT_UP` | 0 | Связь есть, можно отправлять данные | повторные поиски посредников и гарантированное восстановление не предусмотрены.
| `CONN_EVENT_DOWN` | 1 | Временный обрыв, handle жив, само восстановится |
| `CONN_EVENT_TIMEOUT` | 2 | Подключение не удалось, handle жив, закройте сами | ## INDIRECT
### Коллбэк 1. Инициатор отправляет EXCHANGE_REQ со своими кандидатами.
```c 2. Получатель отвечает EXCHANGE_RESP: свои кандидаты и RTT до кандидатов инициатора.
typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h, 3. Инициатор объединяет кандидатов без дублей, исключает себя/получателя,
uint64_t node_id, uint64_t group_id, неизвестные RTT и узлы без прямой READY-сессии. Выбирает до трёх по сумме RTT
enum conn_mgr_event event, void* arg); обоих плеч; при равных RTT — по node_id.
``` 4. INTERM_SELECTED отправляется через выбранный next hop. Пока нет подтверждения,
`h` может быть `NULL` (ошибки до создания handle). `node_id`/`group_id` всегда валидны. сервис ещё не получает UP.
5. Получатель проверяет своё плечо и возвращает INTERM_ACK через выбранного
### Корневые структуры посредника. Инициатор принимает только узлы из собственного выбора.
| Структура | Описание | Пустой список и RTT=0/UINT16_MAX не дают успеха. Реальный нулевой текущий RTT
|-----------|----------| после уже выполненного замера округляется до 1, чтобы отличать его от отсутствия
| `CONN_MGR` | Корневая структура менеджера. `instance`, `group`, очередь `entries`, топ-3 `best_candidates`, таймеры (`bg_ping_timer`, `candidate_ping_timer`), `bg_ping_cursor`, списки `reverse_pending`/`exchange_pending`, `next_request_id` | замера. При отсутствии пригодных кандидатов EXCHANGE повторяется раз в секунду
| `CONN_MGR_ENTRY` | Запись о подключении к одной ноде: `state`, `conn_type`, `last_traffic_tb`, список `handles`, `ncd_handle`, `intermediaries[3]`/`rr_idx`, `local_scan_state`/`main_connect_state`, `db_loaded`, `main.phase` | до общего дедлайна; новые кандидаты могут появиться благодаря фоновым проверкам.
| `CONN_MGR_HANDLE` | Handle для одного вызова `conn_mgr_open`: `node_id`, `group_id`, `cb`, `cb_arg`, ссылка на `entry`, `next`, `deliver_up_token` (отложенный UP) | Запрос работает и при одностороннем `conn_mgr_open()`.
| `CONN_MGR_CANDIDATE` | Пара `{node_id, rtt}` — запись о кандидате-посреднике (packed) | Ответы привязаны одновременно к отправителю и request_id. Повторы идемпотентны;
| `cm_reverse_pending` | Запись об ожидании REVERSE-подключения (связный список) | поздний ответ не может вытеснить рабочий прямой путь.
| `cm_exchange_pending` | Запись об ожидании INDIRECT-обмена: кешированный `EXCHANGE_RESP`, флаги `received`/`probed`, таймер probe-повторов |
Wire-структуры описаны в `conn_mgr_priv.h`. DIRECT_REQ содержит только request_id.
### Жизненный цикл INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{node_id, rtt}`.
Совместимость с прежним адресным DIRECT_REQ не поддерживается.
| Функция | Описание |
|---------|----------| ## API и владение
| `conn_mgr_init(group)` | Выделяет CONN_MGR, создаёт очередь entries, запускает bg_ping и candidate_ping таймеры |
| `conn_mgr_destroy(mgr)` | Отменяет таймеры, освобождает все entries, exchange_pending, reverse_pending | - `conn_mgr_open()` создаёт отдельный handle. Если попытка уже идёт, добавляет
| `conn_mgr_router_recv_handler(conn, entry)` | Обработчик входящих пакетов `ETCP_RT_ID_CONN_MGR` из etcp_router; диспетчеризация по subcmd | ожидающего; если путь готов, доставляет UP отложенно.
Первый сервисный handle входящего пути запускает своё расписание проб,
### Подключение / отключение сохраняя уже работающий путь через посредника.
- UP означает появление пригодного пути. Улучшение пути не повторяет UP.
| Функция | Описание | - DOWN означает потерю рабочих путей; при живом резерве DOWN не доставляется.
|---------|----------| - TIMEOUT означает, что начальное подключение не получило ни одного рабочего пути.
| `conn_mgr_open(inst, group_id, node_id, cb, cb_arg, &h)` | Единая точка входа. Резолвит группу, запускает 3-фазное подключение. Если уже подключена — сразу отложенный UP. Если в процессе — добавляет handle в список | Ошибка отдельной пробы при живом посреднике не становится TIMEOUT для сервиса.
| `conn_mgr_close(h)` | Убирает один handle. Последний handle при DIRECT/REVERSE шлёт DISCONNECT и чистит entry | - DOWN/TIMEOUT не освобождают handle. Владелец обязан вызвать `conn_mgr_close()`.
| `conn_mgr_send(h, entry)` | Отправляет данные через `etcp_router`; для INDIRECT — через первого посредника с живым conn; обновляет `last_traffic_tb` | - Закрытие последнего handle отменяет CM timer и callbacks и закрывает только
| `conn_mgr_get_conn(h)` | ETCP_CONN для DIRECT/REVERSE, NULL для INDIRECT | NCD-владение CM. Группа и другие NCD-владельцы сохраняют транспорт.
- `conn_mgr_get_conn()` возвращает заимствованный прямой транспорт либо NULL
### Кандидаты-посредники при INDIRECT/отсоединённом handle. Данные следует отправлять через router.
| Функция | Описание | Диагностика CM — категория `general`: INFO показывает расписание, выбор и смену
|---------|----------| пути с временем от старта; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR —
| `conn_mgr_update_best_candidates(mgr, node_id, rtt)` | Вставляет/обновляет кандидата в отсортированном топ-3 по RTT. Худшие кандидаты вытесняются | ошибки, недоступные пути и некорректные пакеты. Создание адресных линков NCD —
категория `connection`.
## 4. Алгоритм подключения
Проверки: `test_conn_mgr`, `test_conn_mgr_handles`, `test_conn_mgr_already_connected`,
> Нумерация фаз в коде ведётся через `entry->main.phase`: DIRECT = 0, REVERSE = 2, INDIRECT = 3 (значение 1 не используется). `test_conn_mgr_phases`. Последний проверяет расписание, LAN, односторонний INDIRECT,
поздний DIRECT после NCD TIMEOUT, резерв, реальные данные и обработку кандидатов.
### `conn_mgr_open` / `cm_open` (core)
1. Отклоняет `node_id == 0`.
2. Ищет целевой `TOPO_GROUP_NODE`; если нет — подгружает из SQLite (`db_loaded = 1`).
3. Уже `CONNECTED` → новый handle + отложенный UP.
4. Уже `CONNECTING` → добавляет handle в список ожидания.
5. Есть живой ETCP-линк → помечает `CONNECTED` (DIRECT), открывает NCD, отложенный UP.
6. Иначе: новый entry → `CONNECTING`. При локальных адресах у обеих сторон — запускает локальный скан параллельно, затем фазу DIRECT.
### Фаза 1 — DIRECT (`cm_start_phase_direct`)
- `node_conn_direct_open_node` открывает NCD-соединение (пустой `ni`, коллбэк `cm_ncd_callback`).
- `cm_direct_add_links` вручную добавляет линки: перебор адресов в приоритете `NAT > INTERFACE`, v4+v6, TCP+UDP, сопоставление сокетов через `sock_match`.
- Хоть один линк → ждём INIT handshake. Ноль линков → закрыть NCD, перейти к REVERSE/INDIRECT (для `db_loaded` — cleanup + TIMEOUT).
- NCD держит таймаут 2с. Результаты через `cm_ncd_callback`:
- `NCD_EVENT_UP` → `conn_type = (main.phase==2) ? REVERSE : DIRECT`, `CONNECTED`, регистрация conn в группе (`topo_group_new_conn`) и доставка UP.
- `NCD_EVENT_TIMEOUT` → выбор следующей фазы.
- `NCD_EVENT_DOWN` → доставка DOWN (handle жив).
### Локальный скан (`cm_start_local_scan`)
Параллельно DIRECT. Если у обеих сторон приватные/локальные адреса — LAN ping (`etcp_send_ping_to_socket`, до 3 попыток по 100мс). Успех только обновляет RTT (`topo_node_ping_update_rtt`) и помечает узел достижимым локально (`local_scan_state`); соединение при этом устанавливает DIRECT-фаза обычным путём.
### Фаза 2 — REVERSE (`cm_start_phase_reverse`)
Запускается при `NCD_EVENT_TIMEOUT`, если у нас прямой IP, а у цели нет (`cm_has_direct_ip(local) && !cm_has_direct_ip(target)`):
1. Собираем наши публичные/EIM/direct адреса.
2. Шлём `DIRECT_REQ` (наши адреса) цели через BGP-маршрут `etcp_route_send`, таймер 15с. При таймауте NCD-соединение фазы DIRECT **не закрывается** — оно остаётся в `instance->connections`, чтобы обратный INIT цели переиспользовал его. (В пути «ноль линков» из `cm_direct_add_links` NCD закрывается — линков для INIT-matching всё равно нет.)
3. Цель (`cm_handle_direct_req`) открывает NCD к нам и добавляет линки к нашим адресам с NAT-фильтрацией (`sock_match`) — линк создаётся от любого совместимого сокета (цель может быть за NAT).
4. Цель дозванивается → её INIT матчится с нашим conn по `peer_node_id` → conn поднимается → `cm_ncd_callback(UP)` с `main.phase==2` → `conn_type=REVERSE`, `CONNECTED`, UP.
5. Таймаут → `cm_reverse_timeout_cb` (закрывает NCD) → INDIRECT.
### Фаза 3 — INDIRECT (indirect.c)
1. `cm_start_phase_indirect`: пустой `best_candidates` → TIMEOUT. Иначе шлёт `INTERM_EXCHANGE_REQ` со своим топ-4 кандидатов, таймер 15с.
2. Цель (`cm_handle_interm_exchange_req`) пробегает RTT до кандидатов инициатора (probe при протухших), шлёт `EXCHANGE_RESP` со своими кандидатами + замерами RTT.
3. Цель (`cm_handle_interm_exchange_resp`) кеширует ответ, проверяет свежесть RTT до своих кандидатов; протухшие — probe + ретрай 200мс; свежие → `cm_compute_intermediaries`.
4. `cm_compute_intermediaries` суммирует RTT по общим узлам, сортирует, берёт топ-3, шлёт `INTERM_SELECTED`, помечает `conn_type=INDIRECT`, `CONNECTED`, UP.
5. Цель (`cm_handle_interm_selected`) находит свой entry по `src`, сохраняет посредников, помечает `CONNECTED`, `cm_update_nodeinfo` + UP (если ещё не было).
Трафик идёт через `etcp_router` (первый посредник с живым conn; `rr_idx` зарезервирован, не используется).
## 5. Протокол (ETCP_RT_ID_CONN_MGR)
### Подкоманды
| Константа | Значение | Описание |
|-----------|----------|----------|
| `CM_SUBCMD_DIRECT_REQ` | 0x01 | Фаза 2: "я за NAT, подключись ко мне" — наши адреса |
| `CM_SUBCMD_DIRECT_RESP` | 0x02 | Ответ на DIRECT_REQ (зарезервирован) |
| `CM_SUBCMD_INTERM_EXCHANGE_REQ` | 0x03 | Фаза 3 инициатор: наш топ-4 кандидатов |
| `CM_SUBCMD_INTERM_EXCHANGE_RESP` | 0x04 | Фаза 3 ответ: свои + чужие кандидаты с RTT обеих сторон |
| `CM_SUBCMD_INTERM_SELECTED` | 0x05 | Фаза 3 финал: выбранные посредники |
| `CM_SUBCMD_DISCONNECT` | 0x06 | Уведомление о разрыве |
### Пакеты протокола (все packed)
| Структура | Поля |
|-----------|------|
| `CM_DIRECT_REQ` | `cmd, subcmd, request_id, addr_count` + массив адресов `{type, ip[4], port, sock_id}` |
| `CM_DIRECT_RESP` | `cmd, subcmd, request_id, accepted` |
| `CM_EXCHANGE_REQ` | `cmd, subcmd, request_id, candidate_count, candidates[4]` |
| `CM_EXCHANGE_RESP` | `cmd, subcmd, request_id, my_count, your_count, my_candidates[4], your_candidates[4]` |
| `CM_INTERM_SEL` | `cmd, subcmd, request_id, count, selected[3]` |
| `CM_DISCONNECT` | `cmd, subcmd, node_id` |
## 6. Фоновые процессы (monitor.c)
| Таймер | Интервал | Описание |
|--------|----------|----------|
| `bg_ping` | ~100мс (один узел за тик, цикл ≥10с) | Пинг всех BGP-нод без активного соединения, поддерживает свежие RTT для best_candidates |
| `candidate_ping` | ~2с | Удаляет протухших кандидатов (>30с), обновляет `best_candidates`, probe'ит неактивных |
## 7. Ключевые константы
| Константа | Значение | Смысл |
|-----------|----------|-------|
| `CONN_MGR_MAX_CANDIDATES` | 3 | Макс. число лучших посредников в списке |
| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Макс. число выбранных посредников для одного соединения |
| `CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS` | 2000 | Таймаут фазы DIRECT |
| `CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS` | 15000 | Таймаут фазы REVERSE |
| `CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS` | 15000 | Таймаут фазы INDIRECT |
| `CONN_MGR_CANDIDATE_STALE_TB` | 300000 | Порог устаревания кандидата (~30с) |
| `CONN_MGR_CANDIDATE_PING_TB` | 20000 | Интервал ping кандидатов (~2с) |
| `CONN_MGR_BG_PING_INTERVAL_TB` | 1000 | Интервал фонового ping нод (~100мс) |
| `CONN_MGR_BG_PING_CYCLE_MIN_TB` | 100000 | Мин. длительность цикла обхода всех нод (~10с) |
| `CONN_MGR_LOCAL_SCAN_ATTEMPTS` | 3 | Попыток локального сканирования |
| `CONN_MGR_LOCAL_SCAN_TIMEOUT_MS` | 100 | Таймаут одной попытки локального скана |
| `CACHE_MS` | 20000 | Срок свежести RTT-замера (~20с) |
## 8. Типы соединений (итоговый результат)
| Тип | Значение | Описание |
|-----|----------|----------|
| `CONN_TYPE_NONE` | 0 | Нет соединения |
| `CONN_TYPE_DIRECT` | 1 | Прямое соединение |
| `CONN_TYPE_REVERSE` | 2 | Цель подключилась к нам |
| `CONN_TYPE_INDIRECT` | 3 | Через посредника |
## 9. Тесты
| Тест | Покрытие |
|------|----------|
| `tests/test_conn_mgr.c` | DIRECT (существующее соединение + узел из БД), TIMEOUT |
| `tests/test_conn_mgr_already_connected.c` | DIRECT к уже подключённому узлу |
| `tests/test_conn_mgr_phases.c` | Все три фазы на loopback-топологии C(relay)↔A/B: DIRECT, REVERSE (A public + B nat), INDIRECT (A/B nat через C). Адреса цели «портятся» на неверный порт, чтобы DIRECT честно провалился; таймаут probe уменьшен до 10мс через `route_connectivity_set_probe_timeout_ms()` |

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

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_CANDIDATE_PING_TB 20000
#define CONN_MGR_BG_PING_INTERVAL_TB 1000 #define CONN_MGR_BG_PING_INTERVAL_TB 1000
#define CONN_MGR_BG_PING_CYCLE_MIN_TB 600000 #define CONN_MGR_BG_PING_CYCLE_MIN_TB 600000
#define CONN_MGR_LOCAL_SCAN_ATTEMPTS 3 #define CONN_MGR_REVERSE_DELAY_MS 500
#define CONN_MGR_LOCAL_SCAN_TIMEOUT_MS 100 #define CONN_MGR_INDIRECT_DELAY_MS 1000
#define CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS 2000
#define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 3000 #define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 3000
#define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000 #define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000
#define CONN_MGR_REVERSE_RETRANSMIT_MS 1000 #define CONN_MGR_REVERSE_RETRANSMIT_MS 1000
@ -61,15 +60,16 @@ enum {
CM_SUBCMD_INTERM_EXCHANGE_RESP= 0x04, CM_SUBCMD_INTERM_EXCHANGE_RESP= 0x04,
CM_SUBCMD_INTERM_SELECTED = 0x05, CM_SUBCMD_INTERM_SELECTED = 0x05,
CM_SUBCMD_DISCONNECT = 0x06, CM_SUBCMD_DISCONNECT = 0x06,
CM_SUBCMD_INTERM_ACK = 0x07,
}; };
struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed)); struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed));
#pragma pack(push, 1) #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_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_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[4], your_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[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_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; }; struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; };
#pragma pack(pop) #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 { struct CONN_MGR_HANDLE {
uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg; 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; struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next;
@ -106,13 +97,14 @@ struct CONN_MGR_ENTRY {
uint8_t state, conn_type; uint8_t state, conn_type;
uint64_t last_traffic_tb; uint64_t last_traffic_tb;
struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle; struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle;
uint8_t ncd_down_notified; uint64_t intermediaries[3]; uint8_t intermediariy_count;
uint64_t intermediaries[3]; uint8_t intermediariy_count, rr_idx;
uint8_t local_scan_state, main_connect_state, db_loaded; uint8_t local_scan_state, main_connect_state, db_loaded;
uint8_t reverse_active, indirect_active, indirect_ready; uint8_t reverse_started, indirect_started, indirect_confirmed, direct_up, timeout_notified, ever_up;
void* reverse_timer; void* timer; void* refresh_token;
uint64_t reverse_deadline_tb, indirect_deadline_tb; uint64_t start_tb, reverse_start_tb, indirect_start_tb, deadline_tb;
struct { void* timer; uint32_t request_id; void* conn_ctx; } main; 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; struct CONN_MGR* mgr;
}; };
@ -124,10 +116,7 @@ struct CONN_MGR {
size_t bg_ping_cursor; uint64_t bg_ping_cycle_start_tb; 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 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) */ struct stcp_client* cand_tcp_pings[CONN_MGR_MAX_CANDIDATES]; /* влётные candidate TCP-пинги (для отмены при destroy) */
uint32_t next_request_id, direct_timeout_ms; uint8_t initialized; uint32_t next_request_id; uint8_t initialized;
struct cm_reverse_pending* reverse_pending;
struct cm_exchange_pending* exchange_pending;
uint8_t _unused[0];
}; };
/* ═══════ декларации внутренних функций ═══════ */ /* ═══════ декларации внутренних функций ═══════ */
@ -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_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_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_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_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_refresh_path(struct CONN_MGR_ENTRY* entry);
void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s); 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_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_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_LL 0
#define CM_V6_LOC 1 #define CM_V6_LOC 1
#define CM_V6_DIR 2 #define CM_V6_DIR 2
#define CM_V6_OTH 3 #define CM_V6_OTH 3
#define CM_V6_ANY 4
/* indirect */ /* indirect */
void cm_exchange_tick_cb(void* arg);
void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp); void cm_compute_intermediaries(struct CONN_MGR_ENTRY* entry, struct CM_EXCHANGE_RESP* resp);
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_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_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 */ /* monitor */
void cm_bg_ping_timer_cb(void* arg); 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 "etcp.h"
#include "utun_instance.h" #include "utun_instance.h"
#include "topo_group.h" #include "topo_group.h"
#include "conn_mgr.h"
#include "../lib/debug_config.h" #include "../lib/debug_config.h"
#include "../lib/mem.h" #include "../lib/mem.h"
#include "../lib/ll_queue.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 = нет маршрута. // Найти 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); struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
if (group) { if (group) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, remote_node_id); struct ETCP_CONN* preferred = conn_mgr_route_conn(group->conn_mgr, remote_node_id, svc_id == ETCP_RT_ID_CONN_MGR);
if (nq && nq->conn_mgr_type == CONN_TYPE_INDIRECT && nq->conn_mgr_intermediariy_count > 0) { if (preferred) return preferred;
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;
}
}
return topo_group_find_conn_for_node(group, remote_node_id); return topo_group_find_conn_for_node(group, remote_node_id);
} }
/* group_id=0 (или группа не найдена) — глобальная маршрутизация: прямое соединение по 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 для отправки). // Тонкая обёртка над router_route_conn для rconn (next_hop для отправки).
static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) { 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 — нет. // Есть ли физический маршрут до узла (прямой/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) { int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id) {
if (!inst) return 0; if (!inst) return 0;
if (dst_node_id == inst->node_id) return 1; 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/готовности логического канала. // 1 = найден транспорт (для self всегда 1); это не гарантия UP/готовности логического канала.
int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id); 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 // Отправить данные с авто-seq и контролем inflight
// data: payload без svc_id, mode: ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (0 = обычный) // data: payload без svc_id, mode: ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (0 = обычный)
// Копирует data; 0=принято в очередь, <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)group->group_id, (unsigned long long)peer->conn->peer_node_id,
(unsigned long long)peer->local_epoch, peer->table_sent, peer->table_received, (unsigned long long)peer->local_epoch, peer->table_sent, peer->table_received,
topo_group_peer_ready(group, peer->conn->peer_node_id)); 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) { 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); 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_group_publish_route(group, id);
topo_recovery_changed(group); topo_recovery_changed(group);
conn_mgr_routes_changed(group->conn_mgr);
} }
/* TABLE_COMPLETE подтверждает только NODEINFO текущего обмена. Сбой обмена /* TABLE_COMPLETE подтверждает только NODEINFO текущего обмена. Сбой обмена

24
tests/test_call.c

@ -20,6 +20,7 @@
#include "../routing_layer/conn_mgr_priv.h" #include "../routing_layer/conn_mgr_priv.h"
#include "../utun_instance.h" #include "../utun_instance.h"
#include "../transport_layer/etcp.h" #include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#include "../transport_layer/secure_channel.h" #include "../transport_layer/secure_channel.h"
#include "../ntp_time.h" #include "../ntp_time.h"
#include "../lib/u_async.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 OK() do { G_PASSED++; printf("OK\n"); } while(0)
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } 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]) { 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); 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)"); { TEST("single-thread: call happy path + decline (2 nodes)"); {
struct UASYNC* ua = uasync_create(); struct UASYNC* ua = uasync_create();
if (!ua) { FAIL("uasync_create failed"); return 1; } 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; fail("test5: open failed"); return;
} }
cb1_ok = 0; 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); 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; } if (gh1->entry || gh1->deliver_up_token || conn_mgr_get_conn(gh1)) { fail("test5: handle not detached"); return; }
conn_mgr_close(gh1); gh1 = NULL; conn_mgr_close(gh1); gh1 = NULL;

258
tests/test_conn_mgr_phases.c

@ -30,6 +30,9 @@
#include "topo_node.h" #include "topo_node.h"
#include "conn_mgr.h" #include "conn_mgr.h"
#include "conn_mgr_priv.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 "route_connectivity.h"
#include "../lib/u_async.h" #include "../lib/u_async.h"
#include "../lib/debug_config.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 struct CONN_MGR_HANDLE *h_a = NULL, *h_b = NULL;
static int g_failed = 0; 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"; static char temp_dir[] = "/tmp/utun_cm_phases_XXXXXX";
#define TEMP_DIR_TEMPLATE "/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) { 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; (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) { 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; (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) { static void scenario_teardown(void) {
if (h_a) { conn_mgr_close(h_a); h_a = NULL; } if (h_a) { conn_mgr_close(h_a); h_a = NULL; }
if (h_b) { conn_mgr_close(h_b); h_b = 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_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_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; } if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); inst_c = NULL; }
cleanup_configs(); cleanup_configs();
up_count = down_count = timeout_count = data_count = expected_marker = 0;
ev_a = ev_b = -1;
} }
/* ─── Сценарий 1: DIRECT ─── */ /* ─── Сценарий 1: DIRECT ─── */
@ -252,7 +290,9 @@ static int run_reverse(void) {
ev_a = -1; ev_a = -1;
conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a); 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 (!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); fprintf(stderr, " OK: REVERSE\n"); fflush(stderr);
return 0; 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 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; } 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 ─── */ /* ─── main ─── */
int main(void) { int main(void) {
debug_config_init(); debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR); debug_set_level(DEBUG_LEVEL_ERROR);
if (getenv("UTUN_TEST_DEBUG")) { if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_INFO);
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_GENERAL, DEBUG_LEVEL_DEBUG); 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); utun_instance_set_tun_init_enabled(0);
route_connectivity_set_probe_timeout_ms(10); route_connectivity_set_probe_timeout_ms(10);
@ -322,6 +561,15 @@ int main(void) {
if (run_indirect() != 0) g_failed = 1; if (run_indirect() != 0) g_failed = 1;
scenario_teardown(); 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; } if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }
fprintf(stderr, "%s\n", g_failed ? "RESULT: FAIL" : "RESULT: PASS"); fflush(stderr); 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 val events: SharedFlow<Pair<Int, ByteArray?>> = _events
override fun onEvent(type: Int, data: ByteArray?) { 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) } mainHandler.post { CallController.onEvent(type, data) }
} }
Log.d("utun-gui", "[EVENT] type=$type len=${data?.size ?: 0}") 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 _route.value = AudioRoute.SPEAKER
_headset.value = false _headset.value = false
_earpiece.value = true _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() syncCoreActivity()
LogManager.addLog("INFO", "Call", "finished id=$id reason=$reason; audio/service/proximity released") LogManager.addLog("INFO", "Call", "finished id=$id reason=$reason; audio/service/proximity released")
scope.launch { scope.launch {
@ -253,7 +254,7 @@ object CallController : TelecomCallManager.Listener {
return return
} }
if (type == 13) { reset(); 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()) { if (!coreReady || !NativeLib.isReady()) {
LogManager.addLog("WARN", "Call", "ignore event=$type while core stopped/restarting") LogManager.addLog("WARN", "Call", "ignore event=$type while core stopped/restarting")
return return
@ -285,7 +286,8 @@ object CallController : TelecomCallManager.Listener {
return return
} }
if (data == null || data.size < 8 || ((type == 37 || type == 38 || type == 40 || type == 42) && data.size < 9) || 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 id = ByteBuffer.wrap(data, 0, 8).order(ByteOrder.LITTLE_ENDIAN).long
val st = _state.value val st = _state.value
if (st == null || st.callId != id || !st.ongoing) { 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())) _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)) 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. */ /** Состояние единственной звонковой сессии; не зависит от Activity или ViewModel. */
enum class CallPhase { NONE, OUTGOING, RINGING, INCOMING, ACTIVE, ENDED } 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( data class CallStats(
val rttMs: Int, val bufferMs: Int, val tempoX100: Int, val dropped: Int, val rttMs: Int, val bufferMs: Int, val tempoX100: Int, val dropped: Int,
val underruns: Int, val minMs: Int, val maxMs: Int, val reserveMs: 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 durationSeconds: Int = 0,
val bgpPath: String = "", val bgpPath: String = "",
val stats: CallStats? = null, 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 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), color = Color(0xFFAEBCC8),
fontSize = 16.sp fontSize = 16.sp
) )
Spacer(Modifier.height(8.dp))
Text("Подключение: ${st?.connectionType?.label ?: "нет подключения"}",
color = Color(0xFFAEBCC8), fontSize = 13.sp)
val bgpPath = st?.bgpPath ?: "" val bgpPath = st?.bgpPath ?: ""
if (bgpPath.isNotEmpty()) { if (bgpPath.isNotEmpty()) {
Spacer(Modifier.height(8.dp)) 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) Text(name, color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text("Входящий звонок…", color = Color(0xFFAEBCC8), fontSize = 14.sp) 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)) Spacer(Modifier.height(28.dp))
Row(horizontalArrangement = Arrangement.spacedBy(48.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->setWordWrap(true);
m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;"); m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;");
m_bgpPathLabel->hide(); 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 = new QLabel(this);
m_statsLabel->setAlignment(Qt::AlignCenter); m_statsLabel->setAlignment(Qt::AlignCenter);
m_statsLabel->setWordWrap(true); m_statsLabel->setWordWrap(true);
@ -185,6 +189,7 @@ CallWindow::CallWindow(QWidget* parent)
lay->addWidget(m_peerLabel); lay->addWidget(m_peerLabel);
lay->addWidget(m_statusLabel); lay->addWidget(m_statusLabel);
lay->addWidget(m_durationLabel); lay->addWidget(m_durationLabel);
lay->addWidget(m_connectionLabel);
lay->addWidget(m_bgpPathLabel); lay->addWidget(m_bgpPathLabel);
lay->addWidget(m_statsLabel); lay->addWidget(m_statsLabel);
lay->addStretch(1); lay->addStretch(1);
@ -231,6 +236,13 @@ void CallWindow::setBgpPath(const QString& path) {
m_bgpPathLabel->setVisible(!path.isEmpty()); 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) { void CallWindow::setState(State s) {
m_state = s; m_state = s;
switch (s) { switch (s) {
@ -256,6 +268,7 @@ void CallWindow::setState(State s) {
m_muteBtn->setChecked(false); m_muteBtn->setChecked(false);
break; break;
case Ended: case Ended:
setConnectionType(0, 0);
m_statusLabel->setText(QStringLiteral("Завершён")); m_statusLabel->setText(QStringLiteral("Завершён"));
m_durationTimer->stop(); m_durationTimer->stop();
break; break;

2
tools/chatgui/src/callwindow.h

@ -23,6 +23,7 @@ public:
void setPeerName(const QString& name); void setPeerName(const QString& name);
void setBgpPath(const QString& path); 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 setAudioStats(int rtt, int buffer, int tempo, int dropped, int underruns, int minimum, int maximum, int reserve);
void setState(State s); void setState(State s);
State state() const { return m_state; } State state() const { return m_state; }
@ -55,6 +56,7 @@ private:
QLabel* m_statusLabel; QLabel* m_statusLabel;
QLabel* m_durationLabel; QLabel* m_durationLabel;
QLabel* m_bgpPathLabel; QLabel* m_bgpPathLabel;
QLabel* m_connectionLabel;
QLabel* m_statsLabel; QLabel* m_statsLabel;
QPushButton* m_acceptBtn; QPushButton* m_acceptBtn;
QPushButton* m_declineBtn; 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) { 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)); 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) { 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_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_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_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) */ /* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */
#define MEMBER_DETAIL_LINK_SIZE 64 #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); 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); 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-потока) /* Callback индикации говорящего в рации (вызывается из GUI-потока)
* group_id — канал, src_node_id — кто говорит, on — 1=начал / 0=закончил. */ * 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); 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_error_fn g_call_error_cb = nullptr;
static gui_call_stats_fn g_call_stats_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_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_radio_talk_fn g_radio_talk_cb = nullptr;
static gui_dm_msg_received_fn g_dm_msg_received_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; 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); if (g_call_path_cb) g_call_path_cb(callId, (const char*)d + 8, dlen - 8);
} }
break; 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: case GUI_EVT_CALL_STATS:
if (dlen == 24) { if (dlen == 24) {
uint64_t callId; memcpy(&callId, d, 8); 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; 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) { void gui_bridge_set_radio_talk_cb(gui_radio_talk_fn cb) {
g_radio_talk_cb = cb; g_radio_talk_cb = cb;
} }

Loading…
Cancel
Save