From 0bc0d6cc62029b7c5ac94b08473e805962edd623 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 12:15:29 +0200 Subject: [PATCH] Enforce disjoint IPv4 route insertion atomically --- src/routing_layer/route_lib.c | 84 ++++++++++---------------- src/routing_layer/route_lib.h | 26 ++++---- tests/test_route_lib.c | 110 +++++++++++++++++++++++++++------- 3 files changed, 133 insertions(+), 87 deletions(-) diff --git a/src/routing_layer/route_lib.c b/src/routing_layer/route_lib.c index 90b3898b..dfcabb66 100644 --- a/src/routing_layer/route_lib.c +++ b/src/routing_layer/route_lib.c @@ -21,22 +21,12 @@ static uint32_t prefix_to_mask(uint8_t prefix) { } static bool routes_overlap(uint32_t net1, uint8_t pre1, uint32_t net2, uint8_t pre2) { - if (pre1 == pre2 && net1 == net2) return true; - return false; -} - -static bool check_route_overlap_in_table(uint32_t network, uint8_t prefix, - const struct ROUTE_ENTRY *entries, size_t count) { - for (size_t i = 0; i < count; i++) { - if (routes_overlap(network, prefix, entries[i].network, entries[i].prefix_length)) { - return true; - } - } - return false; + uint32_t mask = prefix_to_mask(pre1 < pre2 ? pre1 : pre2); + return (net1 & mask) == (net2 & mask); } // ============================================================================ -// Поиск (LPM) +// Поиск по непересекающимся диапазонам // ============================================================================ static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count, @@ -56,51 +46,19 @@ static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count, return left; } -static struct ROUTE_ENTRY* binary_search_lpm(struct ROUTE_TABLE *table, uint32_t dest_ip) { - if (table->count == 0) return NULL; - +/* В отсортированной таблице без пересечений подходящий диапазон может быть только один. */ +static struct ROUTE_ENTRY* binary_search_route(struct ROUTE_TABLE *table, uint32_t dest_ip) { int left = 0; int right = (int)table->count - 1; - struct ROUTE_ENTRY *best = NULL; - uint8_t best_prefix = 0; - while (left <= right) { int mid = left + (right - left) / 2; struct ROUTE_ENTRY *entry = &table->entries[mid]; uint32_t mask = prefix_to_mask(entry->prefix_length); - - if ((dest_ip & mask) == (entry->network & mask)) { - if (entry->prefix_length > best_prefix) { - best_prefix = entry->prefix_length; - best = entry; - } - if (mid + 1 < (int)table->count) { - struct ROUTE_ENTRY *next = &table->entries[mid + 1]; - uint32_t next_mask = prefix_to_mask(next->prefix_length); - if ((dest_ip & next_mask) == (next->network & next_mask)) { - left = mid + 1; - } else { - break; - } - } else { - break; - } - } else if (dest_ip > entry->network) { - left = mid + 1; - } else { - right = mid - 1; - } + if ((dest_ip & mask) == entry->network) return entry; + if (dest_ip > entry->network) left = mid + 1; + else right = mid - 1; } - - // fallback проверка предыдущей записи - if (!best && left > 0) { - struct ROUTE_ENTRY *entry = &table->entries[left - 1]; - uint32_t mask = prefix_to_mask(entry->prefix_length); - if ((dest_ip & mask) == (entry->network & mask)) { - best = entry; - } - } - return best; + return NULL; } // ============================================================================ @@ -160,8 +118,25 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) { for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) { uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network); - if (check_route_overlap_in_table(network, s->prefix_length, table->entries, table->count)) { - DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "overlap detected for %s/%d", ip_to_string(network).a, s->prefix_length); + if (s->prefix_length > 32) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: invalid IPv4 prefix", + (unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length); + return false; + } + for (size_t i = 0; i < table->count; i++) { + const struct ROUTE_ENTRY *e = &table->entries[i]; + if (!routes_overlap(network, s->prefix_length, e->network, e->prefix_length)) continue; + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps node=%016llx subnet=%s/%u", + (unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length, + (unsigned long long)e->v_node_info->node_id, ip_to_string(e->network).a, e->prefix_length); + return false; + } + for (const struct TOPO_SUBNET4 *previous = sub; previous != s; previous = previous->next) { + uint32_t other; memcpy(&other, previous->addr, 4); other = ntohl(other); + if (!routes_overlap(network, s->prefix_length, other, previous->prefix_length)) continue; + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps own subnet=%s/%u", + (unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length, + ip_to_string(other).a, previous->prefix_length); return false; } } @@ -176,6 +151,7 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) { for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) { uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network); + network &= prefix_to_mask(s->prefix_length); int pos = binary_search_insert_pos(table->entries, table->count, network, s->prefix_length); if (pos < (int)table->count) memmove(&table->entries[pos + 1], &table->entries[pos], (table->count - pos) * sizeof(struct ROUTE_ENTRY)); struct ROUTE_ENTRY* e = &table->entries[pos]; @@ -210,7 +186,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip) { DEBUG_TRACE(DEBUG_CATEGORY_ROUTING, "dest_ip=%s", ip_to_string(dest_ip).a); - struct ROUTE_ENTRY *result = binary_search_lpm(table, dest_ip); + struct ROUTE_ENTRY *result = binary_search_route(table, dest_ip); if (result) { table->stats.routes_lookup_hits++; diff --git a/src/routing_layer/route_lib.h b/src/routing_layer/route_lib.h index a9a2a57d..1a811a1b 100644 --- a/src/routing_layer/route_lib.h +++ b/src/routing_layer/route_lib.h @@ -1,5 +1,7 @@ /* route_lib — локальная IPv4-таблица подсетей UTUN, отсортированный массив без пересечения маршрутов. - * create → insert/add_local_subnet → lookup → delete/destroy; все операции в одном uasync-потоке. + * create → insert → lookup → delete/destroy; все операции в одном uasync-потоке. + * Пересечения и дубликаты запрещены, в том числе внутри одного набора подсетей; /0 занимает всю таблицу. + * Вставка нормализует адреса по маске; при отказе таблица и её статистика не меняются. * network/dest_ip/parse_subnet — host order; только is_local_subnet принимает network order. * TOPO_GROUP_NODE заимствован: удалить его маршруты до освобождения узла. * lookup возвращает элемент массива, действительный до следующего изменения таблицы. */ @@ -39,24 +41,24 @@ typedef enum { struct ROUTE_ENTRY { uint32_t network; // Сетевой адрес в host order uint8_t prefix_length; // Длина префикса подсети - struct TOPO_GROUP_NODE* v_node_info; // узел владелец этих маршрутов. null если - локальный маршрут. + struct TOPO_GROUP_NODE* v_node_info; // заимствованный владелец; локальный маршрут тоже содержит узел }; /** * @brief Таблица маршрутизации * * Структура представляет собой таблицу маршрутизации, содержащую записи маршрутов, подсети и статистику. - * маршруты не должны пересекаться. т.е. адрес 192.168.1.1/30 нельзя добавить если есть 192.168.1.2/32 или 192.168.1.1/24 + * Подсети не пересекаются: например, 192.168.1.0/24 исключает 192.168.1.0/30 и 192.168.1.2/32. */ struct ROUTE_TABLE { struct ROUTE_ENTRY *entries; /**< Массив записей маршрутов */ size_t count; /**< Текущее количество записей */ size_t capacity; /**< Максимальная емкость таблицы */ uint64_t my_node_id; /**< ID локального узла */ - uint32_t *dynamic_subnets; /**< Динамические подсети (массив пар: сеть, префикс) */ - size_t dynamic_subnet_count; /**< Количество динамических подсетей */ - uint32_t *local_subnets; /**< Локальные подсети (массив пар: сеть, префикс) */ - size_t local_subnet_count; /**< Количество локальных подсетей */ + uint32_t *dynamic_subnets; /**< Не используется текущей реализацией; destroy освобождает память */ + size_t dynamic_subnet_count; /**< Не используется текущей реализацией */ + uint32_t *local_subnets; /**< Не используется текущей реализацией; destroy освобождает память */ + size_t local_subnet_count; /**< Не используется текущей реализацией */ struct { uint64_t local_routes; /**< Количество локальных маршрутов */ uint64_t learned_routes; /**< Количество изученных маршрутов */ @@ -80,10 +82,12 @@ struct ROUTE_TABLE *route_table_create(uint64_t my_node_id); void route_table_destroy(struct ROUTE_TABLE *table); /** - * @brief Вставляет в таблицу маршрутизации все подсети для указанного узла + * @brief Атомарно вставляет все IPv4-подсети узла; /0..32, адреса нормализуются по маске + * @param table Таблица без пересечений; перед заменой маршрутов узла вызвать route_delete * @param node узел, все маршруты которого нужно добавить * - * @return true если подсети добавлены; false при отсутствии подсетей, пересечении или ошибке выделения памяти + * @return true если весь набор добавлен; false при неверных аргументах/префиксе, отсутствии подсетей, + * пересечении с таблицей/внутри набора или нехватке памяти. При false таблица не меняется. */ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node); @@ -112,7 +116,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip); void route_table_print(const struct ROUTE_TABLE *table); /** - * @brief Парсит строку подсети в сетевой адрес и длину префикса + * @brief Разбирает IPv4/префикс; host-биты адреса сохраняются, префикс читается через atoi * * @param subnet_str Строка подсети (например, "192.168.1.0/24") * @param network Указатель для сохранения адреса в host order @@ -121,8 +125,6 @@ void route_table_print(const struct ROUTE_TABLE *table); */ int parse_subnet(const char *subnet_str, uint32_t *network, uint8_t *prefix_length); -bool route_add_local_subnet(struct ROUTE_TABLE *table, uint32_t network, uint8_t prefix_length); - /** * @brief Проверяет что IP адрес НЕ является публичным (роутится в интернете) * diff --git a/tests/test_route_lib.c b/tests/test_route_lib.c index 812922ce..9e4f4b7b 100644 --- a/tests/test_route_lib.c +++ b/tests/test_route_lib.c @@ -1,16 +1,4 @@ -/** - * FULL REFACTORED TEST for new NODEINFO-based routing system - * - * Tests: - * - TOPO_GROUP_NODE creation and insertion - * - paths management (add/remove) - * - versioning (ver field) - * - WITHDRAW when no paths left - * - Performance with 1000 nodes - * - Overlap prevention and lookup - * - * Status: Complete coverage of current route_lib + route_bgp - */ +/* IPv4-таблица: владение, непересекающиеся подсети, атомарный отказ и поиск на границах. */ #include @@ -44,13 +32,8 @@ struct UTUN_INSTANCE { static struct TOPO_GROUP_NODE* create_test_node(uint64_t node_id, uint8_t ver, uint32_t subnet_net, uint8_t prefix) { struct TOPO_GROUP_NODE* nq = u_calloc(1, sizeof(struct TOPO_GROUP_NODE)); if (!nq) return NULL; - struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); - if (!ni) { u_free(nq); return NULL; } - ni->group_ref_count = 1; - ni->node_id = htobe64(node_id); - ni->ver = ver; - nq->node_id = ni->node_id; - nq->paths = queue_new(NULL, 16, 0, 0, "test_paths"); + (void)ver; + nq->node_id = node_id; struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); if (r) { struct TOPO_SUBNET4* sn = u_calloc(1, sizeof(struct TOPO_SUBNET4)); @@ -152,11 +135,12 @@ static void test_performance_1000_nodes(void) { struct ROUTE_TABLE *t = route_table_create(0); clock_t start = clock(); + struct TOPO_GROUP_NODE *nodes[1000]; for (int i = 0; i < 1000; i++) { uint32_t sub = 0x0a000000 + (i * 0x100); /* distinct 10.0.x.0/24 to avoid overlap */ struct TOPO_GROUP_NODE* nq = create_test_node(0x1000000ULL + i, 1, sub, 24); + nodes[i] = nq; route_insert(t, nq); - /* note: nq not freed here (table holds ref to it), freed in test cleanup if needed */ } clock_t end = clock(); @@ -166,6 +150,87 @@ static void test_performance_1000_nodes(void) { printf("(%.3fs for 1000 nodes) ", time); route_table_destroy(t); + for (int i = 0; i < 1000; i++) free_test_node(nodes[i]); + PASS(); +} + +/* Проверяет оба порядка вложенных подсетей, дубликаты, host-биты и соседние диапазоны. */ +static void test_overlap_and_lookup(void) { + TEST("overlap rejection and disjoint lookup"); + struct { uint32_t first, second; uint8_t p1, p2; bool accepted; } cases[] = { + {0x0a000000, 0x0a010000, 8, 24, false}, + {0x0a010000, 0x0a000000, 24, 8, false}, + {0x0a010001, 0x0a0100ff, 24, 24, false}, + {0x0a010001, 0x0a010001, 32, 32, false}, + {0x0a010000, 0x0a010002, 31, 31, true}, + {0x0a010001, 0x0a010002, 32, 32, true}, + {0, 0x0a000001, 0, 32, false}, + {0x0a000001, 0, 32, 0, false} + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + struct ROUTE_TABLE *t = route_table_create(1); + struct TOPO_GROUP_NODE *a = create_test_node(1, 0, cases[i].first, cases[i].p1); + struct TOPO_GROUP_NODE *b = create_test_node(2, 0, cases[i].second, cases[i].p2); + ASSERT(route_insert(t, a), "first insertion"); + ASSERT_EQ(route_insert(t, b), cases[i].accepted, "overlap decision"); + ASSERT_EQ(t->count, cases[i].accepted ? 2 : 1, "atomic table count"); + ASSERT_EQ(t->stats.local_routes, 1, "local route count"); + ASSERT_EQ(t->stats.learned_routes, cases[i].accepted ? 1 : 0, "learned route count"); + struct ROUTE_ENTRY *found = route_lookup(t, cases[i].first); + ASSERT(found && found->v_node_info == a, "first route lookup"); + if (cases[i].accepted) { + found = route_lookup(t, cases[i].second); + ASSERT(found && found->v_node_info == b, "second route lookup"); + } + route_delete(t, a); + route_delete(t, b); + ASSERT_EQ(t->count, 0, "delete clears routes"); + route_table_destroy(t); free_test_node(a); free_test_node(b); + } + PASS(); +} + +/* Ни поздний конфликт набора, ни неверный префикс не должны оставлять частичную вставку. */ +static void test_batch_atomicity(void) { + TEST("batch rejection leaves table unchanged"); + struct ROUTE_TABLE *t = route_table_create(1); + struct TOPO_GROUP_NODE *a = create_test_node(1, 0, 0x0a000000, 8); + struct TOPO_GROUP_NODE *b = create_test_node(2, 0, 0xc0a80000, 24); + ASSERT(route_insert(t, a), "base route"); + struct TOPO_SUBNET4 second = {0}; + uint32_t network = htonl(0x0a010000); + memcpy(second.addr, &network, 4); second.prefix_length = 24; + b->subnets->v4_subnets->next = &second; + ASSERT(!route_insert(t, b), "late overlap with table"); + ASSERT_EQ(t->count, 1, "no partial insert before table conflict"); + network = htonl(0xc0a80080); memcpy(second.addr, &network, 4); + ASSERT(!route_insert(t, b), "overlap inside batch"); + ASSERT_EQ(t->count, 1, "no partial insert before batch conflict"); + second.prefix_length = 33; + ASSERT(!route_insert(t, b), "invalid prefix rejected"); + ASSERT_EQ(t->count, 1, "invalid prefix preserves table"); + ASSERT_EQ(t->stats.learned_routes, 0, "rejection preserves statistics"); + b->subnets->v4_subnets->next = NULL; + route_table_destroy(t); free_test_node(a); free_test_node(b); + PASS(); +} + +static void test_lookup_boundaries(void) { + TEST("normalized subnet boundaries and default route"); + struct ROUTE_TABLE *t = route_table_create(1); + struct TOPO_GROUP_NODE *a = create_test_node(1, 0, 0x0a00007b, 24); + ASSERT(route_insert(t, a), "noncanonical address accepted"); + ASSERT_EQ(t->entries[0].network, 0x0a000000, "network normalized"); + ASSERT(route_lookup(t, 0x0a000000), "first address"); + ASSERT(route_lookup(t, 0x0a0000ff), "last address"); + ASSERT(!route_lookup(t, 0x09ffffff), "before range"); + ASSERT(!route_lookup(t, 0x0a000100), "after range"); + route_delete(t, a); + a->subnets->v4_subnets->prefix_length = 0; + ASSERT(route_insert(t, a), "default route"); + ASSERT(route_lookup(t, 0), "default first address"); + ASSERT(route_lookup(t, UINT32_MAX), "default last address"); + route_table_destroy(t); free_test_node(a); PASS(); } @@ -187,6 +252,9 @@ int main(void) { test_table_create_destroy(); test_nodeinfo_insert(); test_versioning(); + test_overlap_and_lookup(); + test_batch_atomicity(); + test_lookup_boundaries(); test_performance_1000_nodes(); test_destroy_refcount();