/** * @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 #include #include #include #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 "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 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; } 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; } /* Меняем порт адресов цели на «неверный» (закрытый) — DIRECT создаст линк, * но handshake не получит ответа и фаза провалится по таймауту NCD. */ 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); if (!ni) 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; } 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; } 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(); } /* ─── Сценарий 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; } if (conn_type_of(inst_a, nid_b) != CONN_TYPE_REVERSE) { fail("REVERSE: conn_type != REVERSE"); 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; } /* ─── main ─── */ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); if (getenv("UTUN_TEST_DEBUG")) { debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); } 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 (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; }