Enforce disjoint IPv4 route insertion atomically

This commit is contained in:
evgeny committed 2026-10-24 12:15:29 +02:00
1 parent 34d1dcb7c8
commit 0bc0d6cc62
3 files changed
+133 -87

No files matched your search

+30 -54
View File
@@ -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++;
+14 -12
View File
@@ -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 адрес НЕ является публичным (роутится в интернете)
*
+89 -21
View File
@@ -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 <stdio.h>
@@ -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();