Files
utun2/tests/test_bgp_paths.c
T
evgeny e3caf15f32 Guard routing loops and reconcile blocked transit against current paths
Track visited nodes outside the signed body for all routed packet types. Recheck queued transit routes without losing FIFO priority, reject unknown-group transit, and suppress BGP announcements caused only by RTT jitter.

Document routing rules and wire format. Validate graph churn, REINIT, encrypted transit, malformed paths, loop drops and queue cleanup; full suite: 106 passed, 1 skipped, plus proxy integration and sanitizer checks.
2026-09-28 15:36:47 +03:00

337 lines
18 KiB
C

/* Управляемая сеть: настоящий BGP receiver/sender, подписи и очереди, без UDP.
* Доставка по каждому ребру сохраняет FIFO, порядок между рёбрами меняется. */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "utun_instance.h"
#include "config_updater.h"
#include "topo_group.h"
#include "etcp.h"
#include "etcp_api.h"
#include "etcp_router.h"
#include "route_crypto.h"
#include "node_conn_direct.h"
#include "../lib/debug_config.h"
#include "test_utils.h"
#define N 4
#define GROUP 42
static struct UASYNC* ua;
static struct UTUN_INSTANCE* nodes[N];
static struct TOPO_GROUP* groups[N];
static struct ETCP_CONN* conns[N][N];
static struct NODE_CONN_DIRECT* owners[N][N];
static unsigned graph, delivered;
static uint32_t random_state = 1;
static int router_test, blocked_from = -1, blocked_to = -1;
static unsigned received, transit_packets;
static unsigned next_random(void) {
random_state = random_state * 1664525U + 1013904223U;
return random_state;
}
static unsigned edge(int a, int b) {
if (a > b) { int t = a; a = b; b = t; }
unsigned bit = 1;
for (int i = 0; i < N; i++) for (int j = i + 1; j < N; j++, bit <<= 1)
if (i == a && j == b) return bit;
return 0;
}
static void release(struct ll_entry* e) { queue_dgram_free(e); queue_entry_free(e); }
/* Единственный ручной потребитель send_input_q; штатный normalizer отключён. */
static int step(void) {
uasync_poll(ua, 0);
int work = 0, offset = next_random() % (N * N);
for (int k = 0; k < N * N; k++) {
int a = ((k + offset) % (N * N)) / N, b = (k + offset) % N;
if (a == b) continue;
if (a == blocked_from && b == blocked_to) continue;
struct ll_queue* q = conns[a][b]->send_input_q;
assert(queue_entry_count(q) <= (router_test ? 8 : 1));
struct ll_entry* e = queue_data_get(q);
if (!e) continue;
work++;
if ((graph & edge(a, b)) && e->len && e->dgram[0] == ETCP_ID_TOPO_ENTRY) {
delivered++;
nodes[b]->api_bindings.callbacks[ETCP_ID_TOPO_ENTRY](conns[b][a], e);
} else if ((graph & edge(a, b)) && e->len && e->dgram[0] == ETCP_RT_ID_SVC_ROUTE) {
unsigned count = e->dgram[e->len - 1];
assert(count && count <= ROUTER_MAX_VISITED);
uint64_t last; memcpy(&last, e->dgram + e->len - 9, 8); assert(last == nodes[a]->node_id);
if (count > 1) transit_packets++;
nodes[b]->api_bindings.callbacks[ETCP_RT_ID_SVC_ROUTE](conns[b][a], e);
} else release(e);
queue_resume_callback(q);
}
return work;
}
static void settle(void) {
unsigned before = delivered;
int idle = 0;
for (int i = 0; i < 4000 && idle < 16; i++) idle = step() ? 0 : idle + 1;
assert(idle == 16);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "model settled graph=%02x packets=%u", graph, delivered - before);
}
/* Смена графа без обработки очередей между событиями разрыва и восстановления. */
static void change(unsigned mask) {
unsigned old = graph; graph = mask;
for (int a = 0; a < N; a++) for (int b = a + 1; b < N; b++) {
if (!(old & edge(a, b)) || (mask & edge(a, b))) continue;
conns[a][b]->links_up = conns[b][a]->links_up = 0;
topo_group_remove_conn(groups[a], conns[a][b], TOPO_REMOVE_REMOTE_LEAVE);
topo_group_remove_conn(groups[b], conns[b][a], TOPO_REMOVE_REMOTE_LEAVE);
struct ll_entry* e;
while ((e = queue_data_get(conns[a][b]->send_input_q))) release(e);
while ((e = queue_data_get(conns[b][a]->send_input_q))) release(e);
}
for (int a = 0; a < N; a++) for (int b = a + 1; b < N; b++) {
if ((old & edge(a, b)) || !(mask & edge(a, b))) continue;
conns[a][b]->links_up = conns[b][a]->links_up = 1;
assert(topo_group_new_conn(groups[a], conns[a][b]) == 0);
assert(topo_group_new_conn(groups[b], conns[b][a]) == 0);
}
}
/* Независимый эталон — кратчайшие расстояния в графе реальных соединений. */
static void verify(void) {
int distance[N][N];
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++)
distance[a][b] = a == b ? 0 : (graph & edge(a, b)) ? 1 : 100;
for (int k = 0; k < N; k++) for (int a = 0; a < N; a++) for (int b = 0; b < N; b++)
if (distance[a][k] + distance[k][b] < distance[a][b]) distance[a][b] = distance[a][k] + distance[k][b];
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++) {
if (a == b) continue;
struct ETCP_CONN* conn = topo_group_find_conn_for_node(groups[a], nodes[b]->node_id);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "verify graph=%02x src=%d dst=%d expected=%d conn=%p", graph, a, b, distance[a][b], conn);
assert((conn != NULL) == (distance[a][b] < 100));
if (!conn) continue;
struct TOPO_GROUP_NODE* node = topo_node_find_by_id(groups[a], nodes[b]->node_id);
uint8_t count; uint64_t* hops = topo_node_best_hop_list(node, &count, NULL);
assert(count == distance[a][b]);
uint64_t best_neighbor = UINT64_MAX;
for (int n = 0; n < N; n++)
if ((graph & edge(a, n)) && distance[n][b] + 1 == distance[a][b] && nodes[n]->node_id < best_neighbor)
best_neighbor = nodes[n]->node_id;
assert(conn->peer_node_id == best_neighbor);
for (struct ll_entry* p = node->paths->head; p; p = p->next)
for (struct ll_entry* other = p->next; other; other = other->next)
assert(((struct TOPO_NODEPATH*)p)->conn != ((struct TOPO_NODEPATH*)other)->conn);
int previous = a;
for (int h = count - 1; h >= 0; h--) {
int current = 0;
while (current < N && nodes[current]->node_id != hops[h]) current++;
assert(current < N && (graph & edge(previous, current)));
previous = current;
}
assert(previous == b);
for (int h = 0; h < count; h++) for (int j = h + 1; j < count; j++) assert(hops[h] != hops[j]);
if (graph & edge(a, b)) assert(topo_group_peer_ready(groups[a], nodes[b]->node_id));
}
}
static void create(void) {
ua = uasync_create(); assert(ua);
for (int i = 0; i < N; i++) {
struct SC_MYKEYS keys; assert(sc_generate_keypair(&keys) == SC_OK);
char pub[65], priv[65], config[512];
bytes_to_hex(keys.public_key, 32, pub, sizeof(pub)); bytes_to_hex(keys.private_key, 32, priv, sizeof(priv));
snprintf(config, sizeof(config), "[global]\nmy_public_key=%s\nmy_private_key=%s\n", pub, priv);
nodes[i] = utun_instance_create_from_str(ua, config); assert(nodes[i]);
assert(utun_core_start(nodes[i]) == 0);
groups[i] = topo_groups_create_group(nodes[i]->topo_groups, GROUP, TOPO_GROUP_TYPE_UTUN, NULL); assert(groups[i]);
}
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++) {
if (a == b) continue;
struct ETCP_CONN* c = conns[a][b] = etcp_connection_create(nodes[a], "virtual_bgp"); assert(c);
c->peer_node_id = nodes[b]->node_id;
assert(sc_init_ctx(&c->crypto_ctx, &nodes[a]->my_keys) == SC_OK);
assert(sc_set_peer_public_key(&c->crypto_ctx, nodes[b]->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK);
queue_set_callback(c->send_input_q, NULL, NULL);
etcp_conn_ready(c);
assert(node_conn_direct_open(nodes[a], c->peer_node_id, NULL, NULL, &owners[a][b], NULL) >= 0);
}
}
static void metric_jitter(void) {
unsigned before = delivered;
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++) {
if (!(graph & edge(a, b))) continue;
conns[a][b]->rtt_last = 17 + 13 * a + b;
}
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++) {
if (!(graph & edge(a, b))) continue;
for (struct ll_entry* e = groups[a]->nodes->head; e; e = e->next)
assert(topo_group_send_nodeinfo(groups[a], (struct TOPO_GROUP_NODE*)e, conns[a][b]) == 0);
}
settle(); verify();
DEBUG_INFO(DEBUG_CATEGORY_BGP, "metric-only change: unexpected announcements=%u", delivered - before);
assert(delivered == before); /* RTT is telemetry, not routing state. */
}
static void service_receive(struct ETCP_CONN* conn, struct ll_entry* e) {
assert(conn->instance == nodes[3]);
if (e->len > ROUTER_SVC_HDR_SIZE) {
uint64_t source, group;
memcpy(&source, e->dgram + ROUTER_SVC_SRC_OFF, 8); memcpy(&group, e->dgram + ROUTER_SVC_GROUP_OFF, 8);
assert(source == nodes[0]->node_id && group == GROUP);
assert(e->dgram[ROUTER_SVC_FLAGS_OFF] == (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED));
assert(e->len == ROUTER_SVC_HDR_SIZE + 4 && !memcmp(e->dgram + ROUTER_SVC_HDR_SIZE, "test", 4));
received++;
}
release(e);
}
static struct ll_entry* route_packet(uint8_t flags, const uint64_t* ids, unsigned count) {
size_t body = SVC_ROUTE_HDR_SIZE + (flags ? 0 : 4);
struct ll_entry* e = ll_alloc_lldgram(body + count * 8 + 1); assert(e);
struct SVC_ROUTE_HDR h = { .cmd = ETCP_RT_ID_SVC_ROUTE, .group_id = GROUP,
.src_node_id = ids[0], .dst_node_id = nodes[3]->node_id, .svc_id = 0x41, .flags = flags, .reset_id = 1 };
memcpy(e->dgram, &h, sizeof(h));
if (!flags) memcpy(e->dgram + sizeof(h), "loop", 4);
memcpy(e->dgram + body, ids, count * 8); e->dgram[body + count * 8] = count;
e->len = body + count * 8 + 1;
return e;
}
static void inject_router(struct ll_entry* e) {
nodes[1]->api_bindings.callbacks[ETCP_RT_ID_SVC_ROUTE](conns[1][0], e);
}
static void blocked_ready(struct ll_queue* q, void* arg) {
(void)q; (void)arg;
assert(0 && "busy transport must not release a waiter");
}
static void router_paths(void) {
router_test = 1;
unsigned chain = edge(0, 1) | edge(1, 2) | edge(2, 3);
change(chain); settle(); verify();
assert(etcp_router_bind(nodes[3], 0x42, service_receive) == 0);
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(nodes[0], GROUP, nodes[3]->node_id, 0x42); assert(c);
assert(etcp_router_conn_send(c, (const uint8_t*)"test", 4, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0);
uint64_t deadline = get_time_tb() + 20000;
while ((!received || c->tx_acked != c->tx_seq) && get_time_tb() < deadline) { step(); uasync_poll(ua, 1); }
assert(received == 1 && c->tx_acked == c->tx_seq && transit_packets > 0);
settle();
/* Actual transient two-hop loop B -> C -> B. Each hop must append once,
* for DATA, ACK and every control flag, before B rejects its second visit. */
struct TOPO_GROUP_NODE* target = topo_node_find_by_id(groups[2], nodes[3]->node_id);
assert(topo_group_remove_path(target, conns[2][3]) == 1);
uint64_t cycle[] = { nodes[3]->node_id, nodes[1]->node_id };
assert(topo_group_add_path(target, conns[2][1], cycle, 2, 0) == 0);
uint8_t flags[] = { 0, 0, ROUTER_FLAG_START, ROUTER_FLAG_RST, ROUTER_FLAG_START | ROUTER_FLAG_RST, ROUTER_FLAG_CLOSE };
for (unsigned i = 0; i < sizeof(flags); i++) {
uint64_t id = nodes[0]->node_id, before = nodes[1]->router_loop_drops;
struct ll_entry* e = route_packet(flags[i], &id, 1);
if (i == 1) { /* Header-only ACK. */
memcpy(e->dgram + SVC_ROUTE_HDR_SIZE, &id, 8); e->dgram[SVC_ROUTE_HDR_SIZE + 8] = 1; e->len -= 4;
}
inject_router(e); settle();
assert(nodes[1]->router_loop_drops == before + 1);
}
assert(topo_group_remove_path(target, conns[2][1]) == 1);
uint64_t direct = nodes[3]->node_id;
assert(topo_group_add_path(target, conns[2][3], &direct, 1, 0) == 0);
/* Reject malformed metadata before touching a router session. */
uint64_t ids[ROUTER_MAX_VISITED + 1];
for (unsigned i = 0; i < ROUTER_MAX_VISITED + 1; i++) ids[i] = 100 + i;
ids[0] = nodes[0]->node_id;
uint64_t errors = nodes[1]->router_path_errors;
struct ll_entry* e = route_packet(ROUTER_FLAG_START, ids, 1); e->dgram[e->len - 1] = 0; inject_router(e);
e = route_packet(ROUTER_FLAG_START, ids, 1); e->dgram[e->len - 1] = 2; inject_router(e);
e = route_packet(ROUTER_FLAG_START, ids, 1); ((struct SVC_ROUTE_HDR*)e->dgram)->src_node_id++; inject_router(e);
ids[1] = ids[0]; inject_router(route_packet(ROUTER_FLAG_START, ids, 2)); /* repeated source */
ids[1] = 101; inject_router(route_packet(ROUTER_FLAG_START, ids, 2)); /* last hop != ETCP peer */
inject_router(route_packet(ROUTER_FLAG_START, ids, ROUTER_MAX_VISITED + 1));
ids[0] = 99; ids[ROUTER_MAX_VISITED - 1] = nodes[0]->node_id;
inject_router(route_packet(ROUTER_FLAG_START, ids, ROUTER_MAX_VISITED)); /* valid but cannot append */
assert(nodes[1]->router_path_errors == errors + 7);
e = route_packet(ROUTER_FLAG_START, ids, ROUTER_MAX_VISITED);
((struct SVC_ROUTE_HDR*)e->dgram)->dst_node_id = nodes[1]->node_id;
((struct SVC_ROUTE_HDR*)e->dgram)->flags = ROUTER_FLAG_CLOSE; /* valid max-sized list at destination */
inject_router(e); assert(nodes[1]->router_path_errors == errors + 7);
/* Queued transit must leave the old busy next hop even if it never drains. */
change(chain | edge(1, 3)); settle(); verify();
blocked_from = 1; blocked_to = 3;
struct ll_entry* blocker = queue_entry_new(0); assert(blocker); queue_data_put(conns[1][3]->send_input_q, blocker);
assert(etcp_router_conn_send(c, (const uint8_t*)"test", 4, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0);
settle();
assert(received == 1 && conns[1][3]->transit_queues && queue_entry_count(conns[1][3]->transit_queues) == 1);
struct ll_queue* busy = conns[1][3]->send_input_q;
struct TRANSIT_QUEUE* waiting = (struct TRANSIT_QUEUE*)conns[1][3]->transit_queues->head;
struct queue_waiter_handle other = {0};
assert(queue_waiter_wait(busy, &other, blocked_ready, NULL) == 0);
deadline = get_time_tb() + 600;
while (get_time_tb() < deadline) uasync_poll(ua, 1);
assert(busy->waiter_head == waiting->waiter.internal); /* polling must preserve FIFO priority */
queue_waiter_cancel(busy, &other);
target = topo_node_find_by_id(groups[1], nodes[3]->node_id);
assert(topo_group_remove_path(target, conns[1][3]) == 1);
deadline = get_time_tb() + 20000;
while ((received != 2 || c->tx_acked != c->tx_seq) && get_time_tb() < deadline) { step(); uasync_poll(ua, 1); }
assert(received == 2 && c->tx_acked == c->tx_seq);
assert(queue_entry_count(conns[1][3]->transit_queues) == 0 && !conns[1][3]->send_input_q->waiter_head);
assert(queue_data_get(conns[1][3]->send_input_q) == blocker); release(blocker);
blocked_from = blocked_to = -1;
queue_resume_callback(conns[1][3]->send_input_q);
/* Pending waiter + route timer must both disappear with their owning group. */
settle(); blocked_from = 1; blocked_to = 2;
blocker = queue_entry_new(0); assert(blocker); queue_data_put(conns[1][2]->send_input_q, blocker);
uint64_t source = nodes[0]->node_id; inject_router(route_packet(ROUTER_FLAG_START, &source, 1));
assert(conns[1][2]->transit_queues && queue_entry_count(conns[1][2]->transit_queues) == 1);
etcp_router_close_group(nodes[1], GROUP);
assert(queue_entry_count(conns[1][2]->transit_queues) == 0 && !conns[1][2]->send_input_q->waiter_head);
assert(queue_data_get(conns[1][2]->send_input_q) == blocker); release(blocker);
blocked_from = blocked_to = -1;
queue_resume_callback(conns[1][2]->send_input_q);
settle();
/* A direct physical connection does not authorize transit in an unknown group. */
e = route_packet(ROUTER_FLAG_START, &source, 1);
((struct SVC_ROUTE_HDR*)e->dgram)->group_id = GROUP + 1;
inject_router(e);
for (int b = 0; b < N; b++) if (b != 1) assert(queue_entry_count(conns[1][b]->send_input_q) == 0);
DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router paths: encrypted transit, loops, malformed paths and blocked-route switch passed");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
utun_instance_set_tun_init_enabled(0);
create();
unsigned triangle = edge(0, 1) | edge(1, 2) | edge(0, 2) | edge(2, 3);
change(triangle); settle(); verify();
metric_jitter();
/* One-sided REINIT with routes retained, then simultaneous REINIT at all peers. */
etcp_fire_conn_status(conns[0][1], ETCP_CONN_STATUS_REINIT); settle(); verify();
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++)
if (graph & edge(a, b)) etcp_fire_conn_status(conns[a][b], ETCP_CONN_STATUS_REINIT);
settle(); verify();
change(triangle & ~edge(0, 2)); settle(); verify();
change(triangle); settle(); verify();
change(triangle & ~edge(1, 2) & ~edge(0, 2)); settle(); verify();
change(triangle); settle(); verify();
/* Все графы, включая изолированные компоненты; затем churn без ожидания сходимости. */
for (unsigned mask = 0; mask < 64; mask++) { change(mask); settle(); verify(); }
for (int i = 0; i < 100; i++) {
change(next_random() >> 26);
for (unsigned n = next_random() % 5; n; n--) step();
}
settle(); verify();
router_paths();
change(0); settle(); verify();
for (int a = 0; a < N; a++) for (int b = 0; b < N; b++) if (a != b) node_conn_direct_close(owners[a][b]);
for (int a = 0; a < N; a++) utun_instance_destroy(nodes[a]);
uasync_poll(ua, 0);
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "BGP paths: triangle, partitions, all four-node graphs and churn passed packets=%u", delivered);
return 0;
}