Files
utun2/tests/test_bgp_triangle.c
T
Evgeny c5123e4baf bgp: prevent sending NODEINFO to self + own NODEINFO early return
- route_bgp_process_nodeinfo: skip own node_id early, don't add to bgp->nodes
- broadcast loop: skip sending NODEINFO to connection where peer == node_id
- route_bgp_should_send_to: don't send node's NODEINFO to itself
- tests: lower debug level to ERROR, fix keepalive adaptive config
2026-07-01 17:49:27 +03:00

486 lines
21 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file test_bgp_triangle.c
* @brief Тест треугольной BGP-маршрутизации с цепочкой D и листом E (5 узлов)
*
* Топология:
* E
* │
* A ── B ── C ── D
* │ │
* └─────┘
*
* A↔B, A↔C, B↔C — треугольник (проверка fix#1: альтернативные пути при старой версии)
* C↔D — транзитивная цепочка
* B↔E — лист (проверка каскадного удаления)
*
* Фазы:
* 1. Все линки up — BGP-обмен, проверка полного состояния
* 2. Kill A–C — C/D/E только через B
* 3. Restore A–C — C шлёт NODEINFO напрямую (та же версия). Fix#1: путь [C] добавлен
* 4. Kill A–B — C/D выживают через A–C, B/E живут через C
* 5. Kill A–C — A изолирован, все узлы удалены
* 6. Restore A–C + A–B — полное восстановление
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>
#include "../lib/platform_compat.h"
#include "test_utils.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "../src/etcp.h"
#include "../src/etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "../src/routing.h"
#include "../src/route_lib.h"
#include "../src/route_bgp.h"
#include "../src/route_node.h"
#include "../src/dummynet.h"
#include "../src/tun_if.h"
#include "../src/secure_channel.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define TEST_TIMEOUT_TB 200000
#define PHASE_TIMEOUT_TB 100000
#define POLL_INTERVAL_MS 5
#define NODE_ID_A 0x1111111111111111ULL
#define NODE_ID_B 0x2222222222222222ULL
#define NODE_ID_C 0x3333333333333333ULL
#define NODE_ID_D 0x4444444444444444ULL
#define NODE_ID_E 0x5555555555555555ULL
static struct UTUN_INSTANCE* inst_a = NULL;
static struct UTUN_INSTANCE* inst_b = NULL;
static struct UTUN_INSTANCE* inst_c = NULL;
static struct UTUN_INSTANCE* inst_d = NULL;
static struct UTUN_INSTANCE* inst_e = NULL;
static struct UASYNC* ua = NULL;
static int test_phase = 0;
static void* timeout_id = NULL;
static struct dummynet_filter* df_ab = NULL;
static struct dummynet_filter* df_ac = NULL;
/* ================================================================
* Config generator
* ================================================================ */
struct lnk { const char* local_srv; int remote_port; };
struct cli { const char* name; const char* peer_pubkey_hex; int lnk_cnt; struct lnk lnks[2]; };
struct srv { const char* name; int port; };
struct ncfg {
uint64_t node_id; const char* tun_ip; const char* my_subnet;
const char* priv_hex; const char* pub_hex;
int srv_cnt; struct srv srvs[4];
int cli_cnt; struct cli clis[3];
};
static void build_node_config(char* buf, size_t size, const struct ncfg* c) {
int off = snprintf(buf, size,
"[global]\n"
"my_node_id=0x%llx\n"
"my_private_key=%s\n"
"my_public_key=%s\n"
"tun_ip=%s\n"
"tun_ifname=tun99\n"
"keepalive_adaptive=0\n"
"\n"
"[routing]\n"
"my_subnet=%s\n"
"\n",
(unsigned long long)c->node_id, c->priv_hex, c->pub_hex, c->tun_ip, c->my_subnet);
for (int i = 0; i < c->srv_cnt; i++)
off += snprintf(buf + off, size - off,
"[server: %s]\naddr=127.0.0.1:%d\ntype=public\n\n", c->srvs[i].name, c->srvs[i].port);
for (int i = 0; i < c->cli_cnt; i++) {
off += snprintf(buf + off, size - off,
"[client: %s]\nkeepalive=1\npeer_public_key=%s\n", c->clis[i].name, c->clis[i].peer_pubkey_hex);
for (int j = 0; j < c->clis[i].lnk_cnt; j++)
off += snprintf(buf + off, size - off,
"link=%s:127.0.0.1:%d\n", c->clis[i].lnks[j].local_srv, c->clis[i].lnks[j].remote_port);
off += snprintf(buf + off, size - off, "\n");
}
off += snprintf(buf + off, size - off, "[allowed_keys]\nallow_all=1\n");
}
/* ================================================================
* Verification helpers — проверка внутренних структур
* ================================================================ */
static int peer_in_nodes(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!inst || !inst->bgp || !inst->bgp->nodes) return 0;
return nodeinfo_find_by_id(inst->bgp, node_id) != NULL;
}
static int node_path_count(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct NODEINFO_Q* nq = nodeinfo_find_by_id(inst->bgp, node_id);
if (!nq || !nq->paths) return 0;
return queue_entry_count(nq->paths);
}
static int node_path_has_peer(struct UTUN_INSTANCE* inst, uint64_t node_id, uint64_t peer_id) {
struct NODEINFO_Q* nq = nodeinfo_find_by_id(inst->bgp, node_id);
if (!nq || !nq->paths) return 0;
struct ll_entry* e = nq->paths->head;
while (e) {
struct NODEINFO_PATH* path = (struct NODEINFO_PATH*)e;
uint64_t* hop = (uint64_t*)((uint8_t*)path + sizeof(struct NODEINFO_PATH));
for (uint8_t i = 0; i < path->hop_count; i++)
if (hop[i] == peer_id) return 1;
e = e->next;
}
return 0;
}
static int node_version(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct NODEINFO_Q* nq = nodeinfo_find_by_id(inst->bgp, node_id);
return nq ? nq->last_ver : -1;
}
static int route_count_for_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!inst || !inst->rt) return 0;
uint64_t expected_be = htobe64(node_id);
int cnt = 0;
for (size_t i = 0; i < inst->rt->count; i++)
if (inst->rt->entries[i].v_node_info &&
inst->rt->entries[i].v_node_info->node.node_id == expected_be) cnt++;
return cnt;
}
static int count_initialized_links(struct UTUN_INSTANCE* inst) {
int n = 0;
if (!inst) return 0;
struct ETCP_CONN* conn = inst->connections;
while (conn) {
struct ETCP_LINK* l = conn->links;
while (l) { if (l->initialized) n++; l = l->next; }
conn = conn->next;
}
return n;
}
static struct ETCP_LINK* find_client_link(struct UTUN_INSTANCE* inst, int idx) {
if (!inst) return NULL;
struct ETCP_CONN* conn = inst->connections;
while (conn) {
struct ETCP_LINK* l = conn->links;
while (l) {
if (l->is_server == 0) { if (idx == 0) return l; idx--; }
l = l->next;
}
conn = conn->next;
}
return NULL;
}
static void test_timeout_cb(void* arg) {
(void)arg;
test_phase = 2;
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "test_bgp_triangle: 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); goto cleanup; } } while(0)
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_links_init(void) {
return count_initialized_links(inst_a) >= 2 &&
count_initialized_links(inst_b) >= 2 &&
count_initialized_links(inst_c) >= 1 &&
count_initialized_links(inst_e) >= 1;
}
static int cond_d_in_a(void) { return peer_in_nodes(inst_a, NODE_ID_D); }
static int cond_d_gone_a(void) { return !peer_in_nodes(inst_a, NODE_ID_D); }
static int cond_e_in_a(void) { return peer_in_nodes(inst_a, NODE_ID_E); }
static int cond_e_gone_a(void) { return !peer_in_nodes(inst_a, NODE_ID_E); }
static int cond_c_in_a(void) { return peer_in_nodes(inst_a, NODE_ID_C); }
static int cond_c_gone_a(void) { return !peer_in_nodes(inst_a, NODE_ID_C); }
static int cond_c_two_paths(void) {
return node_path_count(inst_a, NODE_ID_C) >= 2 &&
node_path_has_peer(inst_a, NODE_ID_C, NODE_ID_C) &&
node_path_has_peer(inst_a, NODE_ID_C, NODE_ID_B);
}
static int cond_c_one_path_B(void) {
return node_path_count(inst_a, NODE_ID_C) == 1 &&
node_path_has_peer(inst_a, NODE_ID_C, NODE_ID_B);
}
static int cond_b_gone_a(void) { return !peer_in_nodes(inst_a, NODE_ID_B); }
static int cond_c_one_path(void) { return node_path_count(inst_a, NODE_ID_C) == 1; }
/* ================================================================
* 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);
/* ---------- Pre-generate 5 keypairs ---------- */
struct SC_MYKEYS keys[5];
char pub_hex[5][SC_PUBKEY_SIZE * 2 + 1];
char priv_hex[5][SC_PRIVKEY_SIZE * 2 + 1];
for (int i = 0; i < 5; i++) {
sc_generate_keypair(&keys[i]);
bytes_to_hex(keys[i].public_key, SC_PUBKEY_SIZE, pub_hex[i], sizeof(pub_hex[i]));
bytes_to_hex(keys[i].private_key, SC_PRIVKEY_SIZE, priv_hex[i], sizeof(priv_hex[i]));
}
/* ---------- Ports ---------- */
int base = 41000 + (getpid() % 15000);
int p_ab_a = base++, p_ab_b = base++; // A↔B
int p_ac_a = base++, p_ac_c = base++; // A↔C
int p_bc_b = base++, p_bc_c = base++; // B↔C (C's second server for B)
int p_be_b = base++, p_be_e = base++; // B↔E
int p_cd_c = base++, p_cd_d = base++; // C↔D
/* ---------- Build config strings ---------- */
char cfg_a[2048], cfg_b[2048], cfg_c[2048], cfg_d[2048], cfg_e[2048];
build_node_config(cfg_a, sizeof(cfg_a), &(struct ncfg){
.node_id = NODE_ID_A, .tun_ip = "10.100.0.1/24", .my_subnet = "192.168.10.0/24",
.priv_hex = priv_hex[0], .pub_hex = pub_hex[0],
.srv_cnt = 2, .srvs = {{"a_srv_b", p_ab_a}, {"a_srv_c", p_ac_a}},
.cli_cnt = 2, .clis = {
{"to_b", pub_hex[1], 1, {{"a_srv_b", p_ab_b}}},
{"to_c", pub_hex[2], 1, {{"a_srv_c", p_ac_c}}},
}
});
build_node_config(cfg_b, sizeof(cfg_b), &(struct ncfg){
.node_id = NODE_ID_B, .tun_ip = "10.100.0.2/24", .my_subnet = "192.168.20.0/24",
.priv_hex = priv_hex[1], .pub_hex = pub_hex[1],
.srv_cnt = 3, .srvs = {{"b_srv", p_ab_b}, {"b_cli_c", p_bc_b}, {"b_cli_e", p_be_b}},
.cli_cnt = 2, .clis = {
{"to_c", pub_hex[2], 1, {{"b_cli_c", p_bc_c}}},
{"to_e", pub_hex[4], 1, {{"b_cli_e", p_be_e}}},
}
});
build_node_config(cfg_c, sizeof(cfg_c), &(struct ncfg){
.node_id = NODE_ID_C, .tun_ip = "10.100.0.3/24", .my_subnet = "192.168.30.0/24",
.priv_hex = priv_hex[2], .pub_hex = pub_hex[2],
.srv_cnt = 3, .srvs = {{"c_srv_a", p_ac_c}, {"c_srv_b", p_bc_c}, {"c_cli_d", p_cd_c}},
.cli_cnt = 1, .clis = {
{"to_d", pub_hex[3], 1, {{"c_cli_d", p_cd_d}}},
}
});
build_node_config(cfg_d, sizeof(cfg_d), &(struct ncfg){
.node_id = NODE_ID_D, .tun_ip = "10.100.0.4/24", .my_subnet = "192.168.40.0/24",
.priv_hex = priv_hex[3], .pub_hex = pub_hex[3],
.srv_cnt = 1, .srvs = {{"d_srv", p_cd_d}},
.cli_cnt = 0, .clis = {},
});
build_node_config(cfg_e, sizeof(cfg_e), &(struct ncfg){
.node_id = NODE_ID_E, .tun_ip = "10.100.0.5/24", .my_subnet = "192.168.50.0/24",
.priv_hex = priv_hex[4], .pub_hex = pub_hex[4],
.srv_cnt = 1, .srvs = {{"e_srv", p_be_e}},
.cli_cnt = 0, .clis = {},
});
/* ---------- Create instances ---------- */
ua = uasync_create();
ASSERT(ua, "uasync_create");
inst_a = utun_instance_create_from_str(ua, cfg_a); ASSERT(inst_a, "inst_a");
inst_b = utun_instance_create_from_str(ua, cfg_b); ASSERT(inst_b, "inst_b");
inst_c = utun_instance_create_from_str(ua, cfg_c); ASSERT(inst_c, "inst_c");
inst_d = utun_instance_create_from_str(ua, cfg_d); ASSERT(inst_d, "inst_d");
inst_e = utun_instance_create_from_str(ua, cfg_e); ASSERT(inst_e, "inst_e");
ASSERT(utun_instance_init(inst_a) == 0, "init a");
ASSERT(utun_instance_init(inst_b) == 0, "init b");
ASSERT(utun_instance_init(inst_c) == 0, "init c");
ASSERT(utun_instance_init(inst_d) == 0, "init d");
ASSERT(utun_instance_init(inst_e) == 0, "init e");
/* ---------- Dummynet filters (2 managed links) ---------- */
struct ETCP_LINK* ab_link = find_client_link(inst_a, 0);
struct ETCP_LINK* ac_link = find_client_link(inst_a, 1);
ASSERT(ab_link && ac_link, "A links not found");
df_ab = dummynet_filter_create(ua); ASSERT(df_ab, "df_ab");
df_ac = dummynet_filter_create(ua); ASSERT(df_ac, "df_ac");
timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_TB, NULL, test_timeout_cb, "test_timeout");
/* ================================================================
* Phase 1: Initial BGP exchange — all 5 nodes visible
* ================================================================ */
PHASE("1: Initial BGP — all nodes visible");
ASSERT(wait_for("links init", cond_links_init, PHASE_TIMEOUT_TB), "links init timeout");
ASSERT(wait_for("D in A nodes", cond_d_in_a, PHASE_TIMEOUT_TB), "D not in A");
ASSERT(wait_for("E in A nodes", cond_e_in_a, PHASE_TIMEOUT_TB), "E not in A");
ASSERT(peer_in_nodes(inst_a, NODE_ID_B), "B missing");
ASSERT(peer_in_nodes(inst_a, NODE_ID_C), "C missing");
ASSERT(peer_in_nodes(inst_a, NODE_ID_D), "D missing");
ASSERT(peer_in_nodes(inst_a, NODE_ID_E), "E missing");
{
int pb = node_path_count(inst_a, NODE_ID_B);
int pc = node_path_count(inst_a, NODE_ID_C);
int pd = node_path_count(inst_a, NODE_ID_D);
int pe = node_path_count(inst_a, NODE_ID_E);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 1: B_paths=%d C_paths=%d D_paths=%d E_paths=%d", pb, pc, pd, pe);
ASSERT(pb >= 1, "B: expected >=1");
ASSERT(pc >= 1, "C: expected >=1");
ASSERT(pd >= 1, "D: expected >=1");
ASSERT(pe >= 1, "E: expected >=1");
}
ASSERT(route_count_for_node(inst_a, NODE_ID_B) > 0, "B routes missing");
ASSERT(route_count_for_node(inst_a, NODE_ID_C) > 0, "C routes missing");
ASSERT(route_count_for_node(inst_a, NODE_ID_D) > 0, "D routes missing");
ASSERT(route_count_for_node(inst_a, NODE_ID_E) > 0, "E routes missing");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 1 PASSED");
/* ================================================================
* Phase 2: Kill A–C — C/D/E only through B
* ================================================================ */
PHASE("2: Kill A–C — C/D/E via B only");
dummynet_filter_attach(df_ac, ac_link);
dummynet_filter_set_loss(df_ac, 1000);
ASSERT(wait_for("C: 1 path via B after A-C kill", cond_c_one_path_B, PHASE_TIMEOUT_TB), "C paths not 1");
{
int pc = node_path_count(inst_a, NODE_ID_C);
int pd = node_path_count(inst_a, NODE_ID_D);
int pe = node_path_count(inst_a, NODE_ID_E);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 2: C_paths=%d D_paths=%d E_paths=%d", pc, pd, pe);
ASSERT(cond_c_in_a(), "C missing after A-C kill");
ASSERT(cond_d_in_a(), "D missing after A-C kill");
ASSERT(cond_e_in_a(), "E missing after A-C kill");
ASSERT(pd == 1, "D: expected 1 path");
ASSERT(pe >= 1, "E: expected >=1");
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 2 PASSED");
/* ================================================================
* Phase 3: Restore A–C — C sends NODEINFO directly (same ver).
* Fix#1: path [C] added despite old version.
* ================================================================ */
PHASE("3: Restore A–C");
dummynet_filter_set_loss(df_ac, 0);
ASSERT(wait_for("C: >=2 paths after A-C restore", cond_c_two_paths, PHASE_TIMEOUT_TB), "C paths not >=2");
{
int cp = node_path_count(inst_a, NODE_ID_C);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 3 PASSED (C paths=%d)", cp);
}
/* ================================================================
* Phase 4: Kill A–B — B/E survive via C relay, C/D via A-C
* ================================================================ */
PHASE("4: Kill A–B — B/E survive via C");
dummynet_filter_attach(df_ab, ab_link);
dummynet_filter_set_loss(df_ab, 1000);
/* After A-B kill: direct paths removed, B/C/E survive via C relay */
ASSERT(wait_for("A-B paths removed", cond_c_one_path, PHASE_TIMEOUT_TB), "A-B paths not removed");
{
int pb = node_path_count(inst_a, NODE_ID_B);
int pe = node_path_count(inst_a, NODE_ID_E);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 4: B_paths=%d E_paths=%d", pb, pe);
ASSERT(peer_in_nodes(inst_a, NODE_ID_B), "B missing — should survive via C");
ASSERT(peer_in_nodes(inst_a, NODE_ID_E), "E missing — should survive via C");
ASSERT(cond_c_in_a(), "C missing");
ASSERT(cond_d_in_a(), "D missing");
ASSERT(node_path_count(inst_a, NODE_ID_C) == 1, "C: expected 1 path via A-C");
ASSERT(node_path_has_peer(inst_a, NODE_ID_C, NODE_ID_C), "C: missing direct path");
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 4 PASSED");
/* ================================================================
* Phase 5: Kill A–C — A fully isolated (A-B already dead)
* ================================================================ */
PHASE("5: Kill A–C — A isolated");
dummynet_filter_set_loss(df_ac, 1000);
ASSERT(wait_for("C gone from A", cond_c_gone_a, PHASE_TIMEOUT_TB), "C still in A");
ASSERT(wait_for("D gone from A", cond_d_gone_a, PHASE_TIMEOUT_TB), "D still in A");
ASSERT(!peer_in_nodes(inst_a, NODE_ID_B), "B should be gone");
ASSERT(!peer_in_nodes(inst_a, NODE_ID_C), "C should be gone");
ASSERT(!peer_in_nodes(inst_a, NODE_ID_D), "D should be gone");
ASSERT(!peer_in_nodes(inst_a, NODE_ID_E), "E should be gone");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 5 PASSED");
/* ================================================================
* Phase 6: Restore A–C + A–B — full recovery
* ================================================================ */
PHASE("6: Full restore — both links back");
dummynet_filter_set_loss(df_ac, 0);
dummynet_filter_set_loss(df_ab, 0);
ASSERT(wait_for("D in A after restore", cond_d_in_a, 2 * PHASE_TIMEOUT_TB), "D not back");
ASSERT(wait_for("E in A after restore", cond_e_in_a, PHASE_TIMEOUT_TB), "E not back");
ASSERT(wait_for("C: >=2 paths after restore", cond_c_two_paths, PHASE_TIMEOUT_TB), "C paths not >=2");
ASSERT(peer_in_nodes(inst_a, NODE_ID_B), "B missing after restore");
ASSERT(peer_in_nodes(inst_a, NODE_ID_C), "C missing after restore");
ASSERT(peer_in_nodes(inst_a, NODE_ID_D), "D missing after restore");
ASSERT(peer_in_nodes(inst_a, NODE_ID_E), "E missing after restore");
ASSERT(node_path_count(inst_a, NODE_ID_C) >= 2, "C: expected >=2 paths");
ASSERT(node_path_count(inst_a, NODE_ID_D) >= 1, "D: expected >=1");
ASSERT(node_path_count(inst_a, NODE_ID_B) >= 1, "B: expected >=1");
ASSERT(node_path_count(inst_a, NODE_ID_E) >= 1, "E: expected >=1");
ASSERT(route_count_for_node(inst_a, NODE_ID_B) > 0, "B routes");
ASSERT(route_count_for_node(inst_a, NODE_ID_C) > 0, "C routes");
ASSERT(route_count_for_node(inst_a, NODE_ID_D) > 0, "D routes");
ASSERT(route_count_for_node(inst_a, NODE_ID_E) > 0, "E routes");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 6 PASSED");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== ALL PHASES PASSED ===");
test_phase = 1;
cleanup:
if (df_ab) { dummynet_filter_detach(df_ab); dummynet_filter_destroy(df_ab); }
if (df_ac) { dummynet_filter_detach(df_ac); dummynet_filter_destroy(df_ac); }
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 (inst_d) { inst_d->running = 0; utun_instance_destroy(inst_d); }
if (inst_e) { inst_e->running = 0; utun_instance_destroy(inst_e); }
if (ua) { uasync_destroy(ua, 0); ua = NULL; }
printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED");
return test_phase == 1 ? 0 : 1;
}