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.
393 lines
15 KiB
393 lines
15 KiB
/** |
|
* @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 <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdarg.h> |
|
#include <time.h> |
|
#include <sys/stat.h> |
|
#include "../lib/platform_compat.h" |
|
#include "test_utils.h" |
|
|
|
#ifdef _WIN32 |
|
#include <windows.h> |
|
#include <direct.h> |
|
#else |
|
#include <unistd.h> |
|
#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; |
|
}
|
|
|