/** * @file test_bgp_route_exchange.c * @brief Тест цепочной BGP-маршрутизации A↔B↔C с эмуляцией потерь через dummynet_filter * * Топология: A (2 линка) ↔ B (2 линка к A, 1 к C) ↔ C (1 линк) * Потери имитируются через dummynet_filter, встроенный в send-путь линка. * * Фазы: * 0. Инициализация соединений * 1. Ожидание BGP-обмена, проверка маршрутов A↔C * 2. Потеря одного линка A↔B (loss=100%) → маршруты НЕ пропадают * 3. Восстановление линка (loss=0%) * 4. Потеря линка B↔C → маршруты удалены на A и C * 5. Восстановление B↔C → маршруты вернулись * 6. Потеря обоих линков A↔B → маршруты удалены * 7. Восстановление одного линка A↔B → маршруты вернулись */ #include #include #include #include #include #include #include "../lib/platform_compat.h" #include "test_utils.h" #ifdef _WIN32 #include #include #else #include #endif #include "etcp.h" #include "etcp_connections.h" #include "../src/config_parser.h" #include "../src/config_updater.h" #include "../src/utun_instance.h" #include "routing.h" #include "route_lib.h" #include "topo_group.h" #include "dummynet.h" #include "../src/tun_if.h" #include "secure_channel.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #define TEST_TIMEOUT_TB 200000 // 20s in 0.1ms units #define PHASE_TIMEOUT_TB 100000 // 10s per phase #define POLL_INTERVAL_MS 5 static uint64_t g_node_id_a = 0; static uint64_t g_node_id_b = 0; static uint64_t g_node_id_c = 0; static struct UTUN_INSTANCE* inst_a = NULL; static struct UTUN_INSTANCE* inst_b = NULL; static struct UTUN_INSTANCE* inst_c = NULL; static struct UASYNC* ua = NULL; static int test_phase = 0; // 0=running, 1=success, 2=failure static void* timeout_id = NULL; // dummynet filters per link static struct dummynet_filter* df_ab1 = NULL; static struct dummynet_filter* df_ab2 = NULL; static struct dummynet_filter* df_bc = NULL; static char temp_dir[] = "/tmp/utun_bgp_mesh_XXXXXX"; static char config_a[256], config_b[256], config_c[256]; static int port_b_srv = 0, port_b_cli = 0, port_c_srv = 0; static int port_a_srv1 = 0, port_a_srv2 = 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 = strdup(cfg->global.my_public_key_hex); free_config(cfg); return pub; } static int create_temp_configs(void) { if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "mkdtemp failed\n"); return -1; } int base = 41000 + (getpid() % 15000); port_b_srv = base; port_b_cli = base + 1; port_c_srv = base + 2; port_a_srv1 = base + 10; port_a_srv2 = base + 11; snprintf(config_a, sizeof(config_a), "%s/a.conf", temp_dir); snprintf(config_b, sizeof(config_b), "%s/b.conf", temp_dir); snprintf(config_c, sizeof(config_c), "%s/c.conf", temp_dir); // Config C: server only, no keys (config_ensure_keys_and_node_id adds them) if (write_file(config_c, "[global]\n" "tun_ip=10.100.0.3/24\n" "tun_ifname=tun102\n" "keepalive_interval=50\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" "my_subnet=192.168.30.0/24\n" "\n" "[server: c_srv]\n" "addr=127.0.0.1:%d\n" "type=public\n" "\n" "[allowed_keys]\n" "allow_all=1\n", port_c_srv) != 0) return -1; if (config_ensure_keys_and_node_id(config_c) != 0) return -1; char* pub_c = get_pubkey(config_c); if (!pub_c) return -1; // Config B: server for A + client to C, needs C's pubkey if (write_file(config_b, "[global]\n" "tun_ip=10.100.0.2/24\n" "tun_ifname=tun101\n" "keepalive_interval=50\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" "my_subnet=192.168.20.0/24\n" "\n" "[server: b_srv]\n" "addr=127.0.0.1:%d\n" "type=public\n" "\n" "[server: b_cli_srv]\n" "addr=127.0.0.1:%d\n" "type=public\n" "\n" "[client: to_c]\n" "keepalive=1\n" "peer_public_key=%s\n" "link=b_cli_srv:127.0.0.1:%d\n" "\n" "[allowed_keys]\n" "allow_all=1\n", port_b_srv, port_b_cli, pub_c, port_c_srv) != 0) { free(pub_c); return -1; } free(pub_c); if (config_ensure_keys_and_node_id(config_b) != 0) return -1; char* pub_b = get_pubkey(config_b); if (!pub_b) return -1; // Config A: two servers + client to B with 2 links, needs B's pubkey if (write_file(config_a, "[global]\n" "tun_ip=10.100.0.1/24\n" "tun_ifname=tun100\n" "keepalive_interval=50\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" "my_subnet=192.168.10.0/24\n" "\n" "[server: a_srv1]\n" "addr=127.0.0.1:%d\n" "type=public\n" "\n" "[server: a_srv2]\n" "addr=127.0.0.1:%d\n" "type=public\n" "\n" "[client: to_b]\n" "keepalive=1\n" "peer_public_key=%s\n" "link=a_srv1:127.0.0.1:%d\n" "link=a_srv2:127.0.0.1:%d\n" "\n" "[allowed_keys]\n" "allow_all=1\n", port_a_srv1, port_a_srv2, pub_b, port_b_srv, port_b_srv) != 0) { free(pub_b); return -1; } free(pub_b); if (config_ensure_keys_and_node_id(config_a) != 0) return -1; return 0; } static void cleanup_temp_configs(void) { test_unlink(config_a); test_unlink(config_b); test_unlink(config_c); test_rmdir(temp_dir); } static int has_initialized_link(struct UTUN_INSTANCE* inst) { if (!inst || !inst->connections) return 0; struct ll_entry* entry = inst->connections->head; while (entry) { struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; struct ETCP_LINK* l = ce->conn->links; while (l) { if (l->initialized) return 1; l = l->next; } entry = entry->next; } return 0; } static int count_initialized_links(struct UTUN_INSTANCE* inst) { int n = 0; if (!inst || !inst->connections) return 0; struct ll_entry* entry = inst->connections->head; while (entry) { struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; struct ETCP_LINK* l = ce->conn->links; while (l) { if (l->initialized) n++; l = l->next; } entry = entry->next; } return n; } static int check_learned_route(struct UTUN_INSTANCE* inst, uint32_t network, uint8_t prefix_len, uint64_t node_id) { if (!inst || !inst->rt) return 0; for (size_t i = 0; i < inst->rt->count; i++) { struct ROUTE_ENTRY* e = &inst->rt->entries[i]; if (e->network == network && e->prefix_length == prefix_len && e->v_node_info && e->v_node_info->node_id == node_id) return 1; } return 0; } static struct ETCP_LINK* find_client_link(struct UTUN_INSTANCE* inst, int idx) { if (!inst || !inst->connections) return NULL; struct ll_entry* entry = inst->connections->head; while (entry) { struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; struct ETCP_LINK* l = ce->conn->links; while (l) { if (l->is_server == 0) { if (idx == 0) return l; idx--; } l = l->next; } entry = entry->next; } return NULL; } static void test_timeout_cb(void* arg) { (void)arg; test_phase = 2; DEBUG_ERROR(DEBUG_CATEGORY_BGP, "test_bgp_route_exchange: overall timeout"); } static void fail(const char* msg) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: %s", msg); test_phase = 2; } #define PHASE(name) do { \ if (test_phase != 0) break; \ DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== PHASE: %s ===", name); \ } while(0) #define ASSERT(cond, msg) do { if (!(cond)) { fail(msg); return -1; } } while(0) // Poll until condition is met or timeout (clock-based) static int wait_for(const char* desc, int (*cond)(void), int timeout_tb) { uint64_t start = get_time_tb(); while (!cond() && (get_time_tb() - start) < (uint64_t)timeout_tb && test_phase == 0) uasync_poll(ua, POLL_INTERVAL_MS); if (!cond() && test_phase == 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "timeout waiting for: %s", desc); test_phase = 2; return 0; } return test_phase == 0; } // -------------------- Condition callbacks for wait_for -------------------- static int cond_all_links_init(void) { return count_initialized_links(inst_a) >= 2 && count_initialized_links(inst_b) >= 1 && count_initialized_links(inst_c) >= 1; } static int cond_bgp_a_to_c(void) { return check_learned_route(inst_a, ntohl(inet_addr("192.168.30.0")), 24, g_node_id_c); } static int cond_bgp_c_to_a(void) { return check_learned_route(inst_c, ntohl(inet_addr("192.168.10.0")), 24, g_node_id_a); } static int cond_bgp_a_to_c_gone(void) { return !check_learned_route(inst_a, ntohl(inet_addr("192.168.30.0")), 24, g_node_id_c); } static int cond_bgp_c_to_a_gone(void) { return !check_learned_route(inst_c, ntohl(inet_addr("192.168.10.0")), 24, g_node_id_a); } // -------------------- Main -------------------- int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_categories(DEBUG_CATEGORY_BGP); utun_instance_set_tun_init_enabled(0); if (create_temp_configs() != 0) return 1; ua = uasync_create(); ASSERT(ua, "uasync_create"); // Create instances inst_a = utun_instance_create(ua, config_a); ASSERT(inst_a, "inst_a create"); inst_b = utun_instance_create(ua, config_b); ASSERT(inst_b, "inst_b create"); inst_c = utun_instance_create(ua, config_c); ASSERT(inst_c, "inst_c create"); // Init connections ASSERT(utun_instance_init(inst_a) == 0, "inst_a init"); ASSERT(utun_instance_init(inst_b) == 0, "inst_b init"); ASSERT(utun_instance_init(inst_c) == 0, "inst_c init"); g_node_id_a = inst_a->node_id; g_node_id_b = inst_b->node_id; g_node_id_c = inst_c->node_id; // Find links for dummynet filters (client links: idx 0,1 on A, idx 0 on B) struct ETCP_LINK* ab_link1 = find_client_link(inst_a, 0); struct ETCP_LINK* ab_link2 = find_client_link(inst_a, 1); struct ETCP_LINK* bc_link = find_client_link(inst_b, 0); ASSERT(ab_link1 && ab_link2, "A↔B links not found"); ASSERT(bc_link, "B↔C link not found"); // Create dummynet filters df_ab1 = dummynet_filter_create(ua); ASSERT(df_ab1, "df_ab1"); df_ab2 = dummynet_filter_create(ua); ASSERT(df_ab2, "df_ab2"); df_bc = dummynet_filter_create(ua); ASSERT(df_bc, "df_bc"); timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_TB, NULL, test_timeout_cb, "test_timeout"); // --- Phase 1: Wait for all links and BGP exchange --- PHASE("1: Initial BGP exchange"); ASSERT(wait_for("all links initialized", cond_all_links_init, PHASE_TIMEOUT_TB), "links init"); ASSERT(wait_for("BGP A→C", cond_bgp_a_to_c, PHASE_TIMEOUT_TB), "bgp A→C"); ASSERT(wait_for("BGP C→A", cond_bgp_c_to_a, PHASE_TIMEOUT_TB), "bgp C→A"); // --- Phase 2: Kill one A↔B link, verify routes stay --- PHASE("2: Kill one A↔B link"); dummynet_filter_attach(df_ab1, ab_link1); dummynet_filter_set_loss(df_ab1, 1000); // Wait for link-down detection by keepalive, then verify routes survive (link2 still up) for (int i = 0; i < 200 && test_phase == 0 && ab_link1->link_status; i++) uasync_poll(ua, POLL_INTERVAL_MS); // Small margin after link-down before checks for (int i = 0; i < 40 && test_phase == 0; i++) uasync_poll(ua, POLL_INTERVAL_MS); ASSERT(check_learned_route(inst_a, ntohl(inet_addr("192.168.30.0")), 24, g_node_id_c), "routes lost after 1 link down"); ASSERT(check_learned_route(inst_c, ntohl(inet_addr("192.168.10.0")), 24, g_node_id_a), "routes lost C side"); // --- Phase 3: Restore the link --- PHASE("3: Restore one A↔B link"); dummynet_filter_set_loss(df_ab1, 0); ASSERT(wait_for("BGP recovery after ab1 restored", cond_bgp_a_to_c, PHASE_TIMEOUT_TB), "bgp A→C recovery"); // --- Phase 4: Kill B↔C link --- PHASE("4: Kill B↔C link"); dummynet_filter_attach(df_bc, bc_link); dummynet_filter_set_loss(df_bc, 1000); ASSERT(wait_for("A route to C removed", cond_bgp_a_to_c_gone, PHASE_TIMEOUT_TB), "bgp A→C gone"); ASSERT(wait_for("C route to A removed", cond_bgp_c_to_a_gone, PHASE_TIMEOUT_TB), "bgp C→A gone"); // --- Phase 5: Restore B↔C --- PHASE("5: Restore B↔C link"); dummynet_filter_set_loss(df_bc, 0); ASSERT(wait_for("BGP A→C restored", cond_bgp_a_to_c, PHASE_TIMEOUT_TB), "bgp A→C restored"); ASSERT(wait_for("BGP C→A restored", cond_bgp_c_to_a, PHASE_TIMEOUT_TB), "bgp C→A restored"); // --- Phase 6: Kill both A↔B links --- PHASE("6: Kill both A↔B links"); dummynet_filter_set_loss(df_ab1, 1000); dummynet_filter_attach(df_ab2, ab_link2); dummynet_filter_set_loss(df_ab2, 1000); ASSERT(wait_for("A route to C gone (both links)", cond_bgp_a_to_c_gone, PHASE_TIMEOUT_TB), "bgp A→C gone 2"); ASSERT(wait_for("C route to A gone (both links)", cond_bgp_c_to_a_gone, PHASE_TIMEOUT_TB), "bgp C→A gone 2"); // --- Phase 7: Restore one A↔B link (ab1 only, ab2 stays dead) --- PHASE("7: Restore one A↔B link"); dummynet_filter_set_loss(df_ab1, 0); ASSERT(wait_for("BGP A→C restored via one link", cond_bgp_a_to_c, PHASE_TIMEOUT_TB), "bgp A→C restored 2"); ASSERT(wait_for("BGP C→A restored via one link", cond_bgp_c_to_a, PHASE_TIMEOUT_TB), "bgp C→A restored 2"); DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== ALL PHASES PASSED ==="); test_phase = 1; // Cleanup dummynet_filter_detach(df_ab1); dummynet_filter_detach(df_ab2); dummynet_filter_detach(df_bc); dummynet_filter_destroy(df_ab1); dummynet_filter_destroy(df_ab2); dummynet_filter_destroy(df_bc); if (timeout_id) uasync_cancel_timeout(ua, timeout_id); if (inst_a) { inst_a->running = 0; utun_instance_destroy(inst_a); } if (inst_b) { inst_b->running = 0; utun_instance_destroy(inst_b); } if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); } if (ua) { uasync_destroy(ua, 0); ua = NULL; } cleanup_temp_configs(); printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED"); return test_phase == 1 ? 0 : 1; }