From 7c953c19293c071a50a53bfcf0693066b1177222 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 1 Jul 2026 14:22:59 +0300 Subject: [PATCH] bgp: add test_bgp_triangle + keepalive_adaptive config option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keepalive_adaptive=0 disables adaptive keepalive period growth, keeping it fixed at 200ms for faster timeout detection in tests - test_bgp_triangle: 5-node triangle topology test (6 phases): validates alternative path recording (fix#1), cascading node removal on full isolation, and full recovery - config generation via utun_instance_create_from_str() with pre-generated keys — no temp files needed --- src/config_parser.c | 7 +- src/config_parser.h | 1 + src/etcp_connections.c | 7 +- tests/Makefile.am | 5 + tests/test_bgp_route_exchange.c | 3 + tests/test_bgp_triangle.c | 485 ++++++++++++++++++++++++++++++++ 6 files changed, 504 insertions(+), 4 deletions(-) create mode 100644 tests/test_bgp_triangle.c diff --git a/src/config_parser.c b/src/config_parser.c index 47997807..4f781f2b 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -377,6 +377,10 @@ static int parse_global(const char *key, const char *value, struct global_config global->keepalive_interval = atoi(value); return 0; } + if (strcmp(key, "keepalive_adaptive") == 0) { + global->keepalive_adaptive = atoi(value); + return 0; + } if (strcmp(key, "bbr_max_cwnd") == 0) { global->bbr_max_cwnd = atoi(value); return 0; @@ -411,7 +415,7 @@ static int parse_global(const char *key, const char *value, struct global_config global->tun_test_mode = atoi(value); return 0; } - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown global option '%s'. Valid: my_node_name, my_private_key, my_public_key, my_node_id, tun_ifname, tun_ip, mtu, keepalive_timeout, keepalive_interval, bbr_max_cwnd, debug_level, log_file, db_path, enable_timestamp, enable_function_names, enable_file_lines, enable_colors, tun_test_mode", filename, line_num, key); + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown global option '%s'. Valid: my_node_name, my_private_key, my_public_key, my_node_id, tun_ifname, tun_ip, mtu, keepalive_timeout, keepalive_interval, keepalive_adaptive, bbr_max_cwnd, debug_level, log_file, db_path, enable_timestamp, enable_function_names, enable_file_lines, enable_colors, tun_test_mode", filename, line_num, key); return -1; } @@ -741,6 +745,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename) cfg->global.name[0] = '\0'; cfg->global.keepalive_timeout = 2000; // Default 2 seconds cfg->global.keepalive_interval = 200; // Default 0.2 s + cfg->global.keepalive_adaptive = 1; // Default: adaptive enabled cfg->global.bbr_max_cwnd = 1048576; // Default 1MB (was INFLIGHT_LIM_MAX) cfg->global.tcp_recv_buf = 0; // 0 = OS default cfg->global.firewall_rules = NULL; diff --git a/src/config_parser.h b/src/config_parser.h index dd9f62aa..ec520b84 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -121,6 +121,7 @@ struct global_config { int tun_test_mode; // test mode: 1 = don't open real TUN, queues only int keepalive_timeout; // keepalive timeout in ms (default: 2000) int keepalive_interval; // keepalive interval in ms (default: 200) + int keepalive_adaptive; // 1 = adaptive period (default), 0 = fixed interval int bbr_max_cwnd; // BBR cwnd cap in bytes (default: 100000) // Firewall configuration diff --git a/src/etcp_connections.c b/src/etcp_connections.c index cbb53a2b..a723e40f 100644 --- a/src/etcp_connections.c +++ b/src/etcp_connections.c @@ -324,10 +324,11 @@ static void keepalive_timer_cb(void* arg) { } } - // Adaptive keepalive period + // Adaptive keepalive period (only if adaptive enabled) if (link->pkt_sent_since_keepalive) - link->ka_period_ms = KA_PERIOD_MIN_MS; // data flowing → reset to 200ms - else { + link->ka_period_ms = KA_PERIOD_MIN_MS; + else if (link->etcp->instance && link->etcp->instance->config && + link->etcp->instance->config->global.keepalive_adaptive) { uint32_t next = (uint32_t)link->ka_period_ms * 105 / 100 + 1; link->ka_period_ms = next > KA_PERIOD_MAX_MS ? KA_PERIOD_MAX_MS : (uint16_t)next; } diff --git a/tests/Makefile.am b/tests/Makefile.am index d26a7a61..79df5c07 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -44,6 +44,7 @@ check_PROGRAMS = \ test_icmp_proxy \ test_tcp_proxy_client \ test_bgp_route_exchange \ + test_bgp_triangle \ test_conn_mgr \ test_bbr_integration \ test_intensive_memory_pool \ @@ -310,6 +311,10 @@ test_bgp_route_exchange_SOURCES = test_bgp_route_exchange.c test_bgp_route_exchange_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_bgp_route_exchange_LDADD = $(top_builddir)/src/utun-dummynet.o $(ETCP_FULL_OBJS) $(SECURE_CHANNEL_OBJS) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_bgp_triangle_SOURCES = test_bgp_triangle.c +test_bgp_triangle_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_bgp_triangle_LDADD = $(top_builddir)/src/utun-dummynet.o $(ETCP_FULL_OBJS) $(SECURE_CHANNEL_OBJS) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_conn_mgr_SOURCES = test_conn_mgr.c test_conn_mgr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_conn_mgr_LDADD = $(top_builddir)/src/utun-dummynet.o $(ETCP_FULL_OBJS) $(SECURE_CHANNEL_OBJS) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_bgp_route_exchange.c b/tests/test_bgp_route_exchange.c index 2aef4a06..ba341fb4 100644 --- a/tests/test_bgp_route_exchange.c +++ b/tests/test_bgp_route_exchange.c @@ -107,6 +107,7 @@ static int create_temp_configs(void) { "my_node_id=0x3333333333333333\n" "tun_ip=10.100.0.3/24\n" "tun_ifname=tun102\n" + "keepalive_adaptive=0\n" "\n" "[routing]\n" "my_subnet=192.168.30.0/24\n" @@ -128,6 +129,7 @@ static int create_temp_configs(void) { "my_node_id=0x2222222222222222\n" "tun_ip=10.100.0.2/24\n" "tun_ifname=tun101\n" + "keepalive_adaptive=0\n" "\n" "[routing]\n" "my_subnet=192.168.20.0/24\n" @@ -159,6 +161,7 @@ static int create_temp_configs(void) { "my_node_id=0x1111111111111111\n" "tun_ip=10.100.0.1/24\n" "tun_ifname=tun100\n" + "keepalive_adaptive=0\n" "\n" "[routing]\n" "my_subnet=192.168.10.0/24\n" diff --git a/tests/test_bgp_triangle.c b/tests/test_bgp_triangle.c new file mode 100644 index 00000000..d3ab1445 --- /dev/null +++ b/tests/test_bgp_triangle.c @@ -0,0 +1,485 @@ +/** + * @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 +#include +#include +#include +#include +#include "../lib/platform_compat.h" +#include "test_utils.h" + +#ifdef _WIN32 +#include +#else +#include +#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_INFO); + debug_set_categories(DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_CONNECTION); + 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; +}