You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

780 lines
43 KiB

/**
* @file test_conn_mgr_phases.c
* @brief conn_mgr: все три варианта подключения (DIRECT / REVERSE / INDIRECT)
*
* Топология каждого сценария — три узла на loopback: C (публичный релей),
* A и B подключаются к C как клиенты, через BGP узнают друг друга.
*
* Сценарий 1 DIRECT: A=public, B=public → A подключается к B напрямую.
* Сценарий 2 REVERSE: A=public, B=nat → A не пробивает B (адрес испорчен),
* шлёт DIRECT_REQ, B подключается к A (обратное подключение).
* Сценарий 3 INDIRECT: A=nat, B=nat → оба без прямого адреса, выбирают
* посредника C через обмен кандидатами.
*
* Проверка честная: фазы выбираются реальным кодом (адреса цели испорчены на
* «неверный» порт, чтобы DIRECT провалился), RTT кандидата зреет настоящими
* probe-циклами (таймаут probe уменьшен до 10мс для скорости).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "test_utils.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "topo_group.h"
#include "topo_node.h"
#include "conn_mgr.h"
#include "conn_mgr_priv.h"
#include "etcp_router.h"
#include "../src/call/call.h"
#include "../src/chat/chat_event.h"
#include "route_connectivity.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define POLL_MS 5
#define LINK_TIMEOUT_MS 8000
#define BGP_TIMEOUT_MS 8000
#define CONN_TIMEOUT_MS 15000
static struct UASYNC* ua = NULL;
static struct UTUN_INSTANCE *inst_a = NULL, *inst_b = NULL, *inst_c = NULL;
static uint64_t nid_a = 0, nid_b = 0, nid_c = 0;
static volatile int ev_a = -1, ev_b = -1;
static struct CONN_MGR_HANDLE *h_a = NULL, *h_b = NULL;
static int g_failed = 0;
static int up_count, down_count, timeout_count, data_count, expected_marker;
static int close_on_timeout;
static uint64_t data_hop;
#define TEST_CM_SERVICE 200
static uint64_t incoming_call, connection_call, call_next_hop;
static uint8_t call_connection_type;
static int call_accepted, call_ended, call_connection_events, call_media_frames;
static void call_event_recv(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (type == CHAT_EVT_CALL_INCOMING && inst == inst_b && len >= 17 + data[0])
memcpy(&incoming_call, data + 1 + data[0] + 8, 8);
if (inst != inst_a) return;
if (type == CHAT_EVT_CALL_ACCEPTED) call_accepted = 1;
if (type == CHAT_EVT_CALL_ENDED) call_ended = 1;
if (type != CHAT_EVT_CALL_CONNECTION) return;
if (len != 17 || data[8] > CONN_TYPE_INDIRECT || call_ended) { g_failed = 1; return; }
memcpy(&connection_call, data, 8); call_connection_type = data[8]; memcpy(&call_next_hop, data + 9, 8);
call_connection_events++;
}
static void call_media_recv(struct UTUN_INSTANCE* inst, uint64_t id, const uint8_t* data, int len, void* arg) {
(void)inst; (void)id; (void)data; (void)len; (void)arg; call_media_frames++;
}
static int call_incoming_ready(void) { return incoming_call != 0; }
static int call_active(void) { return call_accepted; }
static int call_is_direct(void) { return call_connection_type == CONN_TYPE_DIRECT || call_connection_type == CONN_TYPE_REVERSE; }
static int call_is_indirect(void) { return call_connection_type == CONN_TYPE_INDIRECT; }
static int call_has_no_route(void) { return call_connection_type == CONN_TYPE_NONE && call_next_hop == 0; }
static int call_media_delivered(void) { return call_media_frames > 0; }
static char temp_dir[] = "/tmp/utun_cm_phases_XXXXXX";
#define TEMP_DIR_TEMPLATE "/tmp/utun_cm_phases_XXXXXX"
static char cfg_a[256], cfg_b[256], cfg_c[256];
static int base_port = 0;
/* ─── утилиты ─── */
static int write_file(const char* path, const char* fmt, ...) {
va_list ap; FILE* f = fopen(path, "w");
if (!f) return -1;
va_start(ap, fmt); vfprintf(f, fmt, ap); va_end(ap); fclose(f);
return 0;
}
static char* get_pubkey(const char* path) {
struct utun_config* cfg = parse_config(path);
if (!cfg) return NULL;
char* pub = u_strdup(cfg->global.my_public_key_hex);
free_config(cfg);
return pub;
}
static void fail(const char* msg) { fprintf(stderr, "FAIL: %s\n", msg); fflush(stderr); g_failed = 1; }
static void conn_cb_a(struct CONN_MGR_HANDLE* h, uint64_t node, uint64_t grp, enum conn_mgr_event ev, void* arg) {
(void)h; (void)node; (void)grp; (void)arg; ev_a = (int)ev;
if (ev == CONN_EVENT_UP) up_count++;
else if (ev == CONN_EVENT_DOWN) down_count++;
else timeout_count++;
if (ev == CONN_EVENT_TIMEOUT && close_on_timeout) { conn_mgr_close(h); h_a = NULL; }
}
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;
}
static int poll_until(int (*cond)(void), uint32_t timeout_ms) {
uint64_t start = get_time_tb();
while (!cond() && (get_time_tb() - start) < (uint64_t)timeout_ms * 10) uasync_poll(ua, POLL_MS);
return cond();
}
/* forward-декларации условий сценариев */
static int ev_a_up(void);
static int ev_ab_up(void);
static int candidates_a_and_b(void);
static int has_initialized_link(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->connections) return 0;
struct ll_entry* e = inst->connections->head;
while (e) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
struct ETCP_LINK* l = ce->conn->links;
while (l) { if (l->initialized) return 1; l = l->next; }
e = e->next;
}
return 0;
}
static struct TOPO_GROUP* defgrp(struct UTUN_INSTANCE* inst) { return topo_groups_get_default(inst->topo_groups); }
static int learned(struct UTUN_INSTANCE* inst, uint64_t node_id) {
return defgrp(inst) && topo_node_find_by_id(defgrp(inst), node_id) != NULL;
}
/* Имитируем свежий подписанный адрес цели с закрытым портом. Подпись обязательна:
* длительные сценарии повторяют BGP-обмен и должны сохранять исправные маршруты. */
static void mangle_addrs(struct UTUN_INSTANCE* inst, uint64_t node_id, uint16_t wrong_port) {
struct TOPO_NODE* ni = topo_node_registry_find(inst->topo_groups, node_id);
struct UTUN_INSTANCE* owner = node_id == nid_a ? inst_a : node_id == nid_b ? inst_b : inst_c;
if (!ni || !owner || owner->node_id != node_id) { fail("ADDR: missing node/owner"); return; }
for (struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) a->port = wrong_port;
for (struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) a6->port = wrong_port;
if (topo_node_sign_self(owner, ni) < 0 || topo_node_verify(ni) < 0) fail("ADDR: cannot sign simulated address");
}
static int best_candidates_ready(struct UTUN_INSTANCE* inst) {
struct TOPO_GROUP* g = defgrp(inst);
return g && g->conn_mgr && g->conn_mgr->best_candidate_count > 0;
}
static uint8_t conn_type_of(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct TOPO_GROUP_NODE* nq = defgrp(inst) ? topo_node_find_by_id(defgrp(inst), node_id) : NULL;
return nq ? nq->conn_mgr_type : CONN_TYPE_NONE;
}
/* ─── конфиги ─── */
static int write_node_config(const char* path, const char* tun_ip, const char* tun_if,
int srv_port, const char* type, const char* peer_pub, int peer_port) {
if (peer_pub) {
return write_file(path,
"[global]\n"
"tun_ip=%s/24\n"
"tun_ifname=%s\n"
"[server: srv]\n"
"addr=127.0.0.1:%d\n"
"type=%s\n"
"[client: to_relay]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=srv:127.0.0.1:%d\n"
"[allowed_keys]\n"
"allow_all=1\n",
tun_ip, tun_if, srv_port, type, peer_pub, peer_port);
}
return write_file(path,
"[global]\n"
"tun_ip=%s/24\n"
"tun_ifname=%s\n"
"[server: srv]\n"
"addr=127.0.0.1:%d\n"
"type=%s\n"
"[allowed_keys]\n"
"allow_all=1\n",
tun_ip, tun_if, srv_port, type);
}
/* Создаёт конфиги C (public relay), A и B с заданными типами сокетов. */
static int create_configs(const char* type_a, const char* type_b) {
strcpy(temp_dir, TEMP_DIR_TEMPLATE);
if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "mkdtemp failed\n"); return -1; }
base_port = 43000 + (getpid() % 12000);
int pc = base_port, pa = base_port + 1, pb = base_port + 2;
snprintf(cfg_c, sizeof(cfg_c), "%s/c.conf", temp_dir);
snprintf(cfg_a, sizeof(cfg_a), "%s/a.conf", temp_dir);
snprintf(cfg_b, sizeof(cfg_b), "%s/b.conf", temp_dir);
if (write_node_config(cfg_c, "10.90.0.3", "tun_c", pc, "public", NULL, 0) != 0) return -1;
if (config_ensure_keys_and_node_id(cfg_c) != 0) return -1;
char* pub_c = get_pubkey(cfg_c);
if (!pub_c) return -1;
if (write_node_config(cfg_a, "10.90.0.1", "tun_a", pa, type_a, pub_c, pc) != 0) { u_free(pub_c); return -1; }
if (write_node_config(cfg_b, "10.90.0.2", "tun_b", pb, type_b, pub_c, pc) != 0) { u_free(pub_c); return -1; }
u_free(pub_c);
if (config_ensure_keys_and_node_id(cfg_a) != 0) return -1;
if (config_ensure_keys_and_node_id(cfg_b) != 0) return -1;
return 0;
}
static void cleanup_configs(void) {
test_unlink(cfg_a); test_unlink(cfg_b); test_unlink(cfg_c);
test_rmdir(temp_dir);
}
/* ─── общий каркас сценария ─── */
static int has_initialized_link_all(void) {
return has_initialized_link(inst_a) && has_initialized_link(inst_b) && has_initialized_link(inst_c);
}
static int learned_ab(void) { return learned(inst_a, nid_b) && learned(inst_b, nid_a); }
/* Инициализирует инстансы и ждёт линков + BGP (A↔B узнают друг друга). */
static int scenario_setup(void) {
inst_c = utun_instance_create(ua, cfg_c);
inst_a = utun_instance_create(ua, cfg_a);
inst_b = utun_instance_create(ua, cfg_b);
if (!inst_c || !inst_a || !inst_b) { fail("instance create"); return -1; }
if (utun_instance_init(inst_c) != 0 || utun_instance_init(inst_a) != 0 || utun_instance_init(inst_b) != 0) {
fail("init_connections"); return -1;
}
nid_a = inst_a->node_id; nid_b = inst_b->node_id; nid_c = inst_c->node_id;
if (!poll_until(has_initialized_link_all, LINK_TIMEOUT_MS)) { fail("links not initialized"); return -1; }
if (!poll_until(learned_ab, BGP_TIMEOUT_MS)) { fail("BGP: A/B did not learn each other"); return -1; }
return 0;
}
static void scenario_teardown(void) {
if (h_a) { conn_mgr_close(h_a); h_a = NULL; }
if (h_b) { conn_mgr_close(h_b); h_b = NULL; }
incoming_call = connection_call = call_next_hop = 0; call_connection_type = CONN_TYPE_NONE;
call_accepted = call_ended = call_connection_events = call_media_frames = 0;
if (inst_a) { inst_a->running = 0; utun_instance_destroy(inst_a); inst_a = NULL; }
if (inst_b) { inst_b->running = 0; utun_instance_destroy(inst_b); inst_b = NULL; }
if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); inst_c = NULL; }
cleanup_configs();
up_count = down_count = timeout_count = data_count = expected_marker = 0;
close_on_timeout = 0;
ev_a = ev_b = -1;
}
/* ─── Сценарий 1: DIRECT ─── */
static int run_direct(void) {
fprintf(stderr, "\n=== Scenario 1: DIRECT ===\n"); fflush(stderr);
if (create_configs("public", "public") != 0) { fail("configs"); return -1; }
if (scenario_setup() != 0) return -1;
ev_a = -1;
conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a);
if (!poll_until(ev_a_up, CONN_TIMEOUT_MS)) { fail("DIRECT: no CONN_EVENT_UP"); return -1; }
if (conn_type_of(inst_a, nid_b) != CONN_TYPE_DIRECT) { fail("DIRECT: conn_type != DIRECT"); return -1; }
fprintf(stderr, " OK: DIRECT\n"); fflush(stderr);
return 0;
}
static int ev_a_up(void) { return ev_a == CONN_EVENT_UP; }
/* ─── Сценарий 2: REVERSE ─── */
static int run_reverse(void) {
fprintf(stderr, "\n=== Scenario 2: REVERSE ===\n"); fflush(stderr);
if (create_configs("public", "nat") != 0) { fail("configs"); return -1; }
if (scenario_setup() != 0) return -1;
/* Испортим адреса B в реестре A — DIRECT провалится, A уйдёт в REVERSE. */
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000));
inst_a->etcp_connect_timeout_tb = 3000; /* 300мс — быстрый провал DIRECT */
ev_a = -1;
conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a);
if (!poll_until(ev_a_up, CONN_TIMEOUT_MS)) { fail("REVERSE: no CONN_EVENT_UP"); return -1; }
uint8_t ty = conn_type_of(inst_a, nid_b);
if (ty != CONN_TYPE_DIRECT && ty != CONN_TYPE_REVERSE) { fail("REVERSE: no direct path"); return -1; }
if (!h_a->entry->reverse_started) { fail("REVERSE: reverse did not start immediately"); return -1; }
fprintf(stderr, " OK: REVERSE\n"); fflush(stderr);
return 0;
}
/* ─── Сценарий 3: INDIRECT ─── */
static int run_indirect(void) {
fprintf(stderr, "\n=== Scenario 3: INDIRECT ===\n"); fflush(stderr);
if (create_configs("nat", "nat") != 0) { fail("configs"); return -1; }
if (scenario_setup() != 0) return -1;
/* Ждём, пока candidate_ping реально заполнит список посредников (C). */
if (!poll_until(candidates_a_and_b, 8000)) { fail("INDIRECT: no candidates (C)"); return -1; }
/* Испортим адреса цели с обеих сторон — DIRECT провалится у обоих. */
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 = 3000;
inst_b->etcp_connect_timeout_tb = 3000;
ev_a = -1; ev_b = -1;
conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a);
conn_mgr_open(inst_b, TOPO_GROUP_UTUN, nid_a, conn_cb_b, NULL, &h_b);
if (!poll_until(ev_ab_up, CONN_TIMEOUT_MS)) { fail("INDIRECT: no CONN_EVENT_UP on both sides"); return -1; }
if (conn_type_of(inst_a, nid_b) != CONN_TYPE_INDIRECT) { fail("INDIRECT: A conn_type != INDIRECT"); return -1; }
if (conn_type_of(inst_b, nid_a) != CONN_TYPE_INDIRECT) { fail("INDIRECT: B conn_type != INDIRECT"); return -1; }
/* Посредником должен быть C. */
struct TOPO_GROUP_NODE* nq_b = topo_node_find_by_id(defgrp(inst_a), nid_b);
if (!nq_b || nq_b->conn_mgr_intermediariy_count < 1 || nq_b->conn_mgr_intermediaries[0] != nid_c) {
fail("INDIRECT: intermediary != C"); return -1;
}
fprintf(stderr, " OK: INDIRECT via C\n"); fflush(stderr);
return 0;
}
static int candidates_a_and_b(void) { return best_candidates_ready(inst_a) && best_candidates_ready(inst_b); }
static int ev_ab_up(void) { return ev_a == CONN_EVENT_UP && ev_b == CONN_EVENT_UP; }
/* Приём реальных данных: next hop на получателе отличает релей от прямого пути. */
static void data_recv(struct ETCP_CONN* conn, struct ll_entry* e) {
if (e->len == ROUTER_SVC_PAYLOAD_OFF + 1) {
if (e->dgram[ROUTER_SVC_PAYLOAD_OFF] != ++expected_marker) fail("DATA: order/duplicate violation");
data_count++; data_hop = conn->peer_node_id;
}
queue_dgram_free(e); queue_entry_free(e);
}
static int send_data(uint8_t marker, uint64_t hop) {
data_count = 0;
struct ETCP_ROUTER_PATH path = etcp_router_get_path(inst_a, TOPO_GROUP_UTUN, nid_b, ETCP_RT_ID_CALL);
if (path.next_hop_node_id != (hop == nid_a ? nid_b : hop) || path.type == CONN_TYPE_NONE ||
(hop == nid_c && path.type != CONN_TYPE_INDIRECT)) { fail("DATA: call path snapshot differs from route"); return -1; }
struct ll_entry* e = ll_alloc_lldgram(2);
if (!e) return -1;
e->dgram[0] = TEST_CM_SERVICE; e->dgram[1] = marker; e->len = 2;
if (etcp_route_send(inst_a, TOPO_GROUP_UTUN, nid_b, e, 1, 0) < 0) { fail("DATA: send failed"); return -1; }
uint64_t end = get_time_tb() + 30000;
while (!data_count && get_time_tb() < end) uasync_poll(ua, POLL_MS);
if (!data_count || data_hop != hop || expected_marker != marker) { fail("DATA: wrong delivery/hop"); return -1; }
return 0;
}
static void poll_ms(int ms) {
uint64_t end = get_time_tb() + (uint64_t)ms * 10;
while (get_time_tb() < end) uasync_poll(ua, POLL_MS);
}
static int direct_a_ready(void) {
uint8_t ty = conn_type_of(inst_a, nid_b);
return (ty == CONN_TYPE_DIRECT || ty == CONN_TYPE_REVERSE) && topo_group_peer_ready(defgrp(inst_a), nid_b);
}
/* Две публичные стороны: новые пробы стартуют от t0, даже когда DIRECT ещё идёт. */
static int run_schedule(void) {
fprintf(stderr, "\n=== Scenario 4: 500/1000ms schedule ===\n");
if (create_configs("public", "public") || scenario_setup()) return -1;
if (!poll_until(candidates_a_and_b, 8000)) { fail("SCHEDULE: no relay candidates"); return -1; }
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000));
mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000));
// DIRECT остаётся pending в момент запуска обоих дополнительных вариантов.
inst_a->etcp_connect_timeout_tb = 100000;
if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1;
struct CONN_MGR_ENTRY* entry = h_a->entry;
poll_ms(400);
if (entry->reverse_started || entry->indirect_started) { fail("SCHEDULE: premature start"); return -1; }
poll_ms(300);
if (!entry->reverse_started || entry->indirect_started || entry->main_connect_state != CM_TRY_PENDING) {
fail("SCHEDULE: REVERSE must start at 500ms without stopping DIRECT"); return -1;
}
if (!poll_until(ev_a_up, 2000) || !entry->indirect_started || entry->conn_type != CONN_TYPE_INDIRECT) {
fail("SCHEDULE: INDIRECT must start at 1000ms"); return -1;
}
if (get_time_tb() - entry->start_tb > 20000 || !entry->ncd_handle || entry->main_connect_state != CM_TRY_PENDING) {
fail("SCHEDULE: lost original probe or late relay"); return -1;
}
if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1;
fprintf(stderr, " OK: parallel schedule and real relay delivery\n");
return 0;
}
/* Открывает только A: INDIRECT сразу, затем поздний DIRECT после NCD timeout. */
static int run_upgrade(void) {
fprintf(stderr, "\n=== Scenario 5: one-sided INDIRECT -> late DIRECT ===\n");
if (create_configs("nat", "nat") || scenario_setup()) return -1;
if (!poll_until(candidates_a_and_b, 8000)) { fail("UPGRADE: no relay candidates"); return -1; }
struct ETCP_ROUTER_PATH fallback = etcp_router_get_path(inst_a, TOPO_GROUP_UTUN, nid_b, ETCP_RT_ID_CALL);
if (fallback.type != CONN_TYPE_INDIRECT || fallback.next_hop_node_id != nid_c || cm_find_entry(defgrp(inst_a)->conn_mgr, nid_b)) {
fail("UPGRADE: missing BGP fallback snapshot before CM open"); return -1;
}
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000));
mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000));
inst_a->etcp_connect_timeout_tb = 1000;
if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1;
struct CONN_MGR_ENTRY* entry = h_a->entry;
if (!entry->indirect_started || entry->reverse_started || entry->local_scan_state == CM_TRY_NONE) {
fail("UPGRADE: NAT/NAT must start DIRECT/LOCAL/INDIRECT immediately"); return -1;
}
struct NODE_CONN_DIRECT* original = entry->ncd_handle;
if (!poll_until(ev_a_up, 400) || entry->conn_type != CONN_TYPE_INDIRECT) { fail("UPGRADE: relay waited for reverse"); return -1; }
if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1;
poll_ms(150);
if (entry->main_connect_state != CM_TRY_FAILED || entry->ncd_handle != original || timeout_count || down_count) {
fail("UPGRADE: NCD timeout interrupted relay/probe"); return -1;
}
poll_ms(400);
if (!entry->reverse_started) { fail("UPGRADE: REVERSE not launched while relay works"); return -1; }
if (send_data(2, nid_c)) return -1;
// Первый сервисный handle на принимающей стороне сохраняет INDIRECT и начинает своё расписание.
struct CONN_MGR_ENTRY* inbound = cm_find_entry(defgrp(inst_b)->conn_mgr, nid_a);
if (!inbound || inbound->handles || inbound->conn_type != CONN_TYPE_INDIRECT) {
fail("UPGRADE: missing ownerless incoming relay path"); return -1;
}
uint64_t owner_start = get_time_tb();
if (conn_mgr_open(inst_b, TOPO_GROUP_UTUN, nid_a, conn_cb_b, NULL, &h_b)) return -1;
if (h_b->entry != inbound || inbound->start_tb < owner_start || !inbound->ncd_handle ||
inbound->reverse_start_tb != inbound->start_tb + CONN_MGR_REVERSE_DELAY_MS * 10 ||
inbound->conn_type != CONN_TYPE_INDIRECT || !inbound->indirect_confirmed) {
fail("UPGRADE: incoming owner lost relay or did not start direct probes"); return -1;
}
poll_ms(10);
if (ev_b != CONN_EVENT_UP) { fail("UPGRADE: incoming owner did not receive UP"); return -1; }
// Настоящий звонок использует тот же путь; GUI получает отдельные события, без повторных CM UP.
if (call_init(inst_a) || call_init(inst_b)) { fail("CALL PATH: service initialization failed"); return -1; }
chat_event_set_handler(inst_a, call_event_recv); chat_event_set_handler(inst_b, call_event_recv);
call_set_media_recv_cb(inst_b, call_media_recv, NULL);
uint64_t call_id = UINT64_C(0x123456789abcdef);
char group_id[32]; snprintf(group_id, sizeof(group_id), "%llu", (unsigned long long)TOPO_GROUP_UTUN);
call_start(inst_a, group_id, nid_b, call_id);
if (!poll_until(call_incoming_ready, 3000) || incoming_call != call_id) { fail("CALL PATH: no incoming call"); return -1; }
call_accept(inst_b, call_id);
if (!poll_until(call_active, 3000) || connection_call != call_id || !call_is_indirect() || call_next_hop != nid_c) {
fail("CALL PATH: relay status missing"); return -1;
}
uint8_t frame[] = {0xf8, 0xff, 0xfe};
if (call_send_media(inst_a, call_id, frame, sizeof(frame)) || !poll_until(call_media_delivered, 3000)) {
fail("CALL PATH: relay media delivery failed"); return -1;
}
// Появился пригодный адрес B. Обновление дополняет существующий NCD.
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 2));
if (node_conn_direct_update_node(inst_a, nid_b) < 0 || !poll_until(direct_a_ready, 4000)) {
fail("UPGRADE: no late direct READY path"); return -1;
}
if (entry->ncd_handle != original || up_count != 1 || down_count || timeout_count || !entry->intermediariy_count) {
fail("UPGRADE: duplicate UP/lost probe or reserve"); return -1;
}
if (send_data(3, nid_a)) return -1;
if (!poll_until(call_is_direct, 1400) || call_next_hop != nid_b) { fail("CALL PATH: late direct status missing"); return -1; }
// Без новых кадров/CM UP статус не повторяется на очередном тике.
int events = call_connection_events;
poll_ms(1100);
if (call_connection_events != events) { fail("CALL PATH: duplicate unchanged status"); return -1; }
// Потеря прямого транспорта должна вернуть передачу на проверенный резерв без DOWN.
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000));
mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000));
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;
}
/* Реальный обрыв обоих плеч прямого транспорта без закрытия CM-владения. */
static int drop_direct(struct UTUN_INSTANCE* inst, uint64_t nid) {
struct ETCP_CONN* conn = instance_find_conn(inst, nid);
if (!conn || etcp_conn_ref_take(conn)) { fail("RECOVERY: missing direct transport"); return -1; }
while (conn->links) etcp_link_close(conn->links);
etcp_conn_ref_free(conn);
return 0;
}
/* Поздний обрыв, несколько неудачных циклов, свежие адреса и возврат на DIRECT. */
static int run_recovery(void) {
fprintf(stderr, "\n=== Scenario 8: persistent recovery and address change ===\n");
if (create_configs("nat", "nat") || scenario_setup()) return -1;
if (!poll_until(candidates_a_and_b, 8000)) return -1;
uint16_t bad_port = (uint16_t)(base_port + 1000);
mangle_addrs(inst_a, nid_b, bad_port); mangle_addrs(inst_b, nid_a, bad_port);
if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a) || !poll_until(ev_a_up, 3000)) return -1;
struct CONN_MGR_ENTRY* entry = h_a->entry;
uint32_t stale_request = entry->exchange_request_id;
struct NODE_CONN_DIRECT* original = entry->ncd_handle;
if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1;
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 2));
node_conn_direct_update_node(inst_a, nid_b);
if (!poll_until(direct_a_ready, 4000)) { fail("RECOVERY: no initial direct path"); return -1; }
// Истёк первоначальный бюджет. Работающее соединение не требует новых проб.
poll_ms(CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS + 100);
if (entry->attempt_active || entry->timer || timeout_count) { fail("RECOVERY: direct path is still probing"); return -1; }
mangle_addrs(inst_a, nid_b, bad_port); mangle_addrs(inst_b, nid_a, bad_port);
if (drop_direct(inst_a, nid_b) || drop_direct(inst_b, nid_a)) return -1;
poll_ms(20);
if (!entry->attempt_active || entry->start_tb + 1000 < get_time_tb() || entry->conn_type != CONN_TYPE_INDIRECT ||
entry->ncd_handle != original || down_count || up_count != 1) {
fail("RECOVERY: late loss did not start recovery with reserve"); return -1;
}
if (send_data(2, nid_c)) return -1;
// Сокращаем только срок тестовой пробы; интервалы backoff проверяем настоящими часами.
for (int cycle = 0; cycle < 3; cycle++) {
uint64_t start = entry->start_tb;
unsigned delay = entry->retry_delay_ms;
entry->deadline_tb = get_time_tb() + 100;
cm_arm_timer(entry); poll_ms(30);
if (entry->attempt_active || !entry->retry_tb || entry->retry_tb <= get_time_tb()) {
fail("RECOVERY: exhausted round did not enter backoff"); return -1;
}
if (send_data((uint8_t)(3 + cycle), nid_c)) return -1;
while (entry->start_tb == start && get_time_tb() < entry->retry_tb + 3000) uasync_poll(ua, POLL_MS);
if (entry->start_tb == start || !entry->attempt_active || entry->start_tb - start < delay * 10 || timeout_count) {
fail("RECOVERY: retry/backoff lost demand or repeated TIMEOUT"); return -1;
}
}
entry->deadline_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30);
uint64_t start = entry->start_tb;
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 2));
conn_mgr_routes_changed(defgrp(inst_a)->conn_mgr);
if (!poll_until(direct_a_ready, 2000) || entry->start_tb == start || entry->ncd_handle != original ||
up_count != 1 || down_count || timeout_count || entry->attempt_active || entry->timer) {
fail("RECOVERY: address update did not bypass backoff and restore DIRECT"); return -1;
}
struct CM_INTERM_SEL stale = {0}; stale.request_id = stale_request; stale.count = 1;
stale.selected[0] = (struct CONN_MGR_CANDIDATE){nid_c, 1};
cm_handle_interm_ack(defgrp(inst_a)->conn_mgr, nid_b, (const uint8_t*)&stale, sizeof(stale));
if (send_data(6, nid_a) || entry->conn_type != CONN_TYPE_DIRECT) return -1;
// Смена локальных сокетов тоже должна обходить длинный backoff.
mangle_addrs(inst_a, nid_b, bad_port); mangle_addrs(inst_b, nid_a, bad_port);
if (drop_direct(inst_a, nid_b) || drop_direct(inst_b, nid_a)) return -1;
poll_ms(20);
entry->deadline_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30);
entry->retry_tb = get_time_tb() + 150000; cm_arm_timer(entry);
start = entry->start_tb;
mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 2));
entry->node_timestamp = topo_node_registry_find(inst_a->topo_groups, nid_b)->timestamp;
conn_mgr_network_changed(defgrp(inst_a)->conn_mgr);
if (!poll_until(direct_a_ready, 2000) || entry->start_tb == start) { fail("RECOVERY: local network change did not retry"); return -1; }
if (send_data(7, nid_a)) return -1;
// Закрытие последнего handle отменяет восстановление даже во время backoff.
mangle_addrs(inst_a, nid_b, bad_port); mangle_addrs(inst_b, nid_a, bad_port);
if (drop_direct(inst_a, nid_b) || drop_direct(inst_b, nid_a)) return -1;
poll_ms(20);
entry->deadline_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30);
int ups = up_count, downs = down_count, timeouts = timeout_count;
conn_mgr_close(h_a); h_a = NULL;
poll_ms(1200);
if (cm_find_entry(defgrp(inst_a)->conn_mgr, nid_b) || up_count != ups || down_count != downs || timeout_count != timeouts) {
fail("RECOVERY: callback or retry after last close"); return -1;
}
fprintf(stderr, " OK: late loss, 1/2/4s retries, unchanged reserve, address/socket wakeup and close cancellation\n");
return 0;
}
/* WITHDRAW последнего маршрута убирает резерв; новый NODEINFO восстанавливает доставку. */
static int run_relay_recovery(void) {
fprintf(stderr, "\n=== Scenario 9: lost relay route and new exchange ===\n");
if (create_configs("nat", "nat") || scenario_setup() || !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) || !poll_until(ev_a_up, 3000)) return -1;
struct CONN_MGR_ENTRY* entry = h_a->entry;
uint32_t old_request = entry->exchange_request_id;
if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1;
// Входящий обмен уже истёк, а собственная попытка ещё в backoff.
struct CONN_MGR_ENTRY* inbound = cm_find_entry(defgrp(inst_b)->conn_mgr, nid_a);
if (!inbound) { fail("RELAY RECOVERY: missing inbound exchange"); return -1; }
inbound->peer_exchange_deadline_tb = get_time_tb() - 1;
entry->deadline_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30);
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); poll_ms(20);
if (down_count != 1 || up_count != 1 || timeout_count || entry->conn_type != CONN_TYPE_NONE ||
!entry->attempt_active || !entry->exchange_request_id || entry->exchange_request_id == old_request) {
fail("RELAY RECOVERY: lost route did not start a fresh exchange with DOWN"); return -1;
}
struct CM_INTERM_SEL stale = {0}; stale.request_id = old_request; stale.count = 1;
stale.selected[0] = (struct CONN_MGR_CANDIDATE){nid_c, 1};
cm_handle_interm_ack(defgrp(inst_a)->conn_mgr, nid_b, (const uint8_t*)&stale, sizeof(stale));
if (entry->indirect_confirmed || entry->intermediariy_count != 1 || up_count != 1) {
fail("RELAY RECOVERY: old ACK confirmed a new attempt"); return -1;
}
struct ETCP_CONN* to_a = instance_find_conn(inst_c, nid_a);
struct TOPO_GROUP_NODE* b_on_c = topo_node_find_by_id(defgrp(inst_c), nid_b);
// Получатель потерял анонс: очищаем кэш отправителя, чтобы NODEINFO действительно ушёл по сети.
for (struct ll_entry* e = defgrp(inst_c)->senders_list->head; e; e = e->next) {
struct TOPO_GROUP_CONN_ITEM* peer = (struct TOPO_GROUP_CONN_ITEM*)e->data;
struct ll_entry* advert = peer->node_id == nid_a && peer->advertised ? queue_find_data_by_index(peer->advertised, &nid_b) : NULL;
if (advert) { queue_remove_data(peer->advertised, advert); queue_entry_free(advert); }
}
if (!to_a || !b_on_c || topo_group_send_nodeinfo(defgrp(inst_c), b_on_c, to_a) < 0) return -1;
ev_a = -1;
if (!poll_until(ev_a_up, 3000) || up_count != 2 || down_count != 1 || timeout_count ||
entry->conn_type != CONN_TYPE_INDIRECT || inbound->peer_exchange_request_id != entry->exchange_request_id ||
inbound->peer_exchange_deadline_tb <= get_time_tb()) {
fail("RELAY RECOVERY: fresh exchange did not restore UP"); return -1;
}
if (send_data(2, nid_c)) return -1;
fprintf(stderr, " OK: one DOWN/UP, immediate exchange, stale ACK ignored and expired incoming deadline renewed\n");
return 0;
}
/* Начальный TIMEOUT не снимает потребность; callback может безопасно закрыть последний handle. */
static int run_initial_timeout(void) {
fprintf(stderr, "\n=== Scenario 10: initial TIMEOUT keeps demand and callback close ===\n");
if (create_configs("nat", "nat") || scenario_setup() || !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));
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);
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;
entry->initial_deadline_tb = entry->deadline_tb = get_time_tb() + 100;
cm_arm_timer(entry); poll_ms(30);
if (timeout_count != 1 || up_count || down_count || h_a->entry != entry || entry->attempt_active || !entry->retry_tb) {
fail("INITIAL TIMEOUT: demand was lost or wrong events"); return -1;
}
uint64_t start = entry->start_tb;
entry->retry_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30);
if (!entry->attempt_active || entry->start_tb == start || timeout_count != 1) {
fail("INITIAL TIMEOUT: no subsequent retry"); return -1;
}
struct ETCP_CONN* to_a = instance_find_conn(inst_c, nid_a);
struct TOPO_GROUP_NODE* b_on_c = topo_node_find_by_id(defgrp(inst_c), nid_b);
for (struct ll_entry* e = defgrp(inst_c)->senders_list->head; e; e = e->next) {
struct TOPO_GROUP_CONN_ITEM* peer = (struct TOPO_GROUP_CONN_ITEM*)e->data;
struct ll_entry* advert = peer->node_id == nid_a && peer->advertised ? queue_find_data_by_index(peer->advertised, &nid_b) : NULL;
if (advert) { queue_remove_data(peer->advertised, advert); queue_entry_free(advert); }
}
if (!to_a || !b_on_c || topo_group_send_nodeinfo(defgrp(inst_c), b_on_c, to_a) < 0) return -1;
ev_a = -1;
if (!poll_until(ev_a_up, 3000) || up_count != 1 || timeout_count != 1 || down_count) {
fail("INITIAL TIMEOUT: late success did not deliver UP"); return -1;
}
if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1;
conn_mgr_close(h_a); h_a = NULL; poll_ms(30);
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);
close_on_timeout = 1;
if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1;
entry = h_a->entry;
entry->initial_deadline_tb = entry->deadline_tb = get_time_tb() + 100;
cm_arm_timer(entry); poll_ms(30);
if (h_a || cm_find_entry(defgrp(inst_a)->conn_mgr, nid_b) || timeout_count != 2) {
fail("INITIAL TIMEOUT: last close inside callback did not cancel recovery"); return -1;
}
poll_ms(1100);
if (up_count != 1 || down_count || timeout_count != 2) { fail("INITIAL TIMEOUT: callback after close"); return -1; }
fprintf(stderr, " OK: one initial TIMEOUT, continuing retries, late UP and safe close inside TIMEOUT callback\n");
return 0;
}
/* ─── main ─── */
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_INFO);
}
utun_instance_set_tun_init_enabled(0);
route_connectivity_set_probe_timeout_ms(10);
ua = uasync_create();
if (!ua) { fprintf(stderr, "uasync_create failed\n"); return 1; }
if (run_direct() != 0) g_failed = 1;
scenario_teardown();
ev_a = -1; ev_b = -1; h_a = NULL; h_b = NULL;
if (run_reverse() != 0) g_failed = 1;
scenario_teardown();
ev_a = -1; ev_b = -1; h_a = NULL; h_b = NULL;
if (run_indirect() != 0) g_failed = 1;
scenario_teardown();
if (run_schedule() != 0) g_failed = 1;
scenario_teardown();
if (run_upgrade() != 0) g_failed = 1;
scenario_teardown();
if (run_local() != 0) g_failed = 1;
scenario_teardown();
if (run_invalid_candidates() != 0) g_failed = 1;
scenario_teardown();
if (run_recovery() != 0) g_failed = 1;
scenario_teardown();
if (run_relay_recovery() != 0) g_failed = 1;
scenario_teardown();
if (run_initial_timeout() != 0) g_failed = 1;
scenario_teardown();
if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }
fprintf(stderr, "%s\n", g_failed ? "RESULT: FAIL" : "RESULT: PASS"); fflush(stderr);
return g_failed ? 1 : 0;
}