Prepare local topology replacements before retiring old state

This commit is contained in:
evgeny committed 2026-10-24 14:16:39 +02:00
1 parent 42a58881ca
commit 1e0281d4d8
2 files changed
+156 -102

No files matched your search

+112 -102
View File
@@ -1107,114 +1107,124 @@ int topo_node_init_self(struct UTUN_INSTANCE* instance) {
return 0;
}
/* Пересоздаёт local_node группы при изменении имени/подсетей/ключей, инкрементируя версию. */
/* Готовит local_node целиком; отказ подготовки/публикации сохраняет прежний узел и маршруты. */
int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group) {
if (!instance || !group) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return -1; }
size_t name_len = 0;
if (instance->name[0]) { name_len = strlen(instance->name); if (name_len > 63) name_len = 63; }
int vc = 0, vc6 = 0;
struct CFG_ROUTE_ENTRY* s = instance->config->my_subnets;
while (s) { if (s->ip.family == AF_INET) vc++; else if (s->ip.family == AF_INET6) vc6++; s = s->next; }
// Для chat групп подсети не передаём
if (group->group_type == TOPO_GROUP_TYPE_CHAT) { vc = 0; vc6 = 0; }
int changed = 1; uint8_t old_ver = 0;
if (group->local_node) {
struct TOPO_NODE* oni = topo_node_registry_find(instance->topo_groups, group->local_node->node_id);
if (oni) {
old_ver = oni->ver;
changed = (vc != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v4_subnets) : 0))
|| (name_len != (oni->node_name ? strlen(oni->node_name) : 0))
|| (name_len && strncmp(instance->name, oni->node_name ? oni->node_name : "", name_len))
|| (memcmp(oni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) != 0)
|| (vc6 != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v6_subnets) : 0))
|| (oni->client_type != instance->client_type)
|| (oni->client_activity != instance->client_activity)
|| (group->local_node->radio != group->radio_active)
|| (group->local_node->phone_status != call_phone_status(instance))
|| (memcmp(oni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE) != 0);
}
if (!instance || !group || group->instance != instance || !instance->config || !instance->topo_groups) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: invalid instance/group/config"); return -1;
}
if (!changed && group->local_node)
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my nodeinfo unchanged (identity), ver=%d grp=%016llx",
group->local_node->last_ver, (unsigned long long)group->group_id);
if (changed) {
struct TOPO_NODE* ni = topo_node_registry_find(instance->topo_groups, instance->node_id);
if (ni) topo_node_ref(ni); /* новая группа удерживает запись до освобождения старой */
if (group->local_node) {
if (instance->rt) route_delete(instance->rt, group->local_node);
topo_nodeq_free_group_fields(instance->topo_groups, group->local_node);
u_free(group->local_node);
group->local_node = NULL;
}
group->local_node = u_calloc(1, sizeof(struct TOPO_GROUP_NODE));
if (!group->local_node) {
if (ni) topo_node_registry_unref(instance->topo_groups, instance->node_id);
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local group node allocation failed"); return -1;
}
struct TOPO_GROUP_NODE* lq = group->local_node;
int new_identity = ni == NULL;
if (!ni) ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) { u_free(lq); group->local_node = NULL; return -1; }
struct TOPO_NODE old_identity = *ni;
ni->node_id = instance->node_id;
ni->ver = (old_ver % 255) + 1;
ni->client_type = instance->client_type;
ni->client_activity = instance->client_activity;
lq->radio = group->radio_active;
lq->phone_status = call_phone_status(instance);
memcpy(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE);
memcpy(ni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE);
ni->node_name = u_strdup(instance->name);
if (!ni->node_name || topo_node_sign_self(instance, ni) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot publish local identity");
u_free(ni->node_name); *ni = old_identity;
if (new_identity) topo_node_free_raw(instance->topo_groups, ni);
else topo_node_registry_unref(instance->topo_groups, instance->node_id);
u_free(lq); group->local_node = NULL; return -1;
}
u_free(old_identity.node_name);
if (new_identity) ni = topo_node_registry_store(instance->topo_groups, ni);
if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot register local identity"); return -1; }
lq->node_id = ni->node_id;
lq->last_ver = ni->ver;
lq->last_timestamp = ni->timestamp;
if (group->group_type != TOPO_GROUP_TYPE_CHAT && (vc || vc6)) {
struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS));
s = instance->config->my_subnets;
while (s) {
if (s->ip.family == AF_INET) {
struct TOPO_SUBNET4* sub = memory_pool_alloc(group->instance->topo_groups->v4_subnet_pool);
memcpy(sub->addr, &s->ip.addr.v4, 4); sub->prefix_length = s->netmask;
sub->next = r->v4_subnets; r->v4_subnets = sub;
} else if (s->ip.family == AF_INET6) {
struct TOPO_SUBNET6* sub6 = memory_pool_alloc(group->instance->topo_groups->v6_subnet_pool);
memcpy(sub6->addr, &s->ip.addr.v6, 16); sub6->prefix_length = s->netmask;
sub6->next = r->v6_subnets; r->v6_subnets = sub6;
}
s = s->next;
struct TOPO_GROUPS* groups = instance->topo_groups;
struct TOPO_GROUP_NODE* old = group->local_node;
struct TOPO_GROUP_NODE* lq = u_calloc(1, sizeof(*lq));
if (!lq) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: allocation failed"); return -1; }
struct TOPO_NODE* ni = NULL;
char* name = NULL;
int new_identity = 0, retained = 0, routes_changed = 0, vc = 0, vc6 = 0;
lq->node_id = instance->node_id; lq->radio = group->radio_active; lq->phone_status = call_phone_status(instance);
if (group->group_type != TOPO_GROUP_TYPE_CHAT) {
struct TOPO_SUBNET4** tail4 = NULL; struct TOPO_SUBNET6** tail6 = NULL;
for (struct CFG_ROUTE_ENTRY* s = instance->config->my_subnets; s; s = s->next) {
if ((s->ip.family != AF_INET && s->ip.family != AF_INET6) ||
s->netmask > (s->ip.family == AF_INET ? 32 : 128)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: invalid subnet family=%d prefix=%u", s->ip.family, s->netmask);
goto fail;
}
if (!lq->subnets) {
lq->subnets = u_calloc(1, sizeof(*lq->subnets));
if (!lq->subnets) goto allocation_failed;
tail4 = &lq->subnets->v4_subnets; tail6 = &lq->subnets->v6_subnets;
}
if (s->ip.family == AF_INET) {
struct TOPO_SUBNET4* sub = memory_pool_alloc(groups->v4_subnet_pool);
if (!sub) goto allocation_failed;
*sub = (struct TOPO_SUBNET4){.prefix_length = s->netmask}; memcpy(sub->addr, &s->ip.addr.v4, 4);
*tail4 = sub; tail4 = &sub->next; vc++;
} else {
struct TOPO_SUBNET6* sub = memory_pool_alloc(groups->v6_subnet_pool);
if (!sub) goto allocation_failed;
*sub = (struct TOPO_SUBNET6){.prefix_length = s->netmask}; memcpy(sub->addr, &s->ip.addr.v6, 16);
*tail6 = sub; tail6 = &sub->next; vc6++;
}
lq->subnets = r;
}
if (group->group_type == TOPO_GROUP_TYPE_CHAT)
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my_nodeinfo updated: v4sub=%d v6sub=%d ver=%d grp=%016llx",
vc, vc6, ni->ver, (unsigned long long)group->group_id);
else
DEBUG_INFO(DEBUG_CATEGORY_BGP, "my_nodeinfo updated: v4sub=%d v6sub=%d ver=%d grp=%016llx",
vc, vc6, ni->ver, (unsigned long long)group->group_id);
if (group->group_type != TOPO_GROUP_TYPE_CHAT && instance->rt && !route_insert(instance->rt, lq))
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "failed to insert local routes");
} else {
struct TOPO_NODE* lni = topo_node_registry_find(instance->topo_groups, group->local_node->node_id);
if (lni) group->local_node->last_ver = lni->ver;
if (vc > UINT8_MAX || vc6 > UINT8_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: subnet counters exceed wire limit v4=%d v6=%d", vc, vc6); goto fail;
}
}
ni = topo_node_registry_find(groups, instance->node_id);
int changed = !old || !ni || strcmp(ni->node_name ? ni->node_name : "", instance->name) ||
memcmp(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) ||
memcmp(ni->ed25519_public_key, instance->my_ed25519_pubkey, SC_PUBKEY_SIZE) ||
ni->client_type != instance->client_type || ni->client_activity != instance->client_activity ||
old->radio != lq->radio || old->phone_status != lq->phone_status;
if (!changed) {
struct TOPO_SUBNET4* a4 = old->subnets ? old->subnets->v4_subnets : NULL;
struct TOPO_SUBNET4* b4 = lq->subnets ? lq->subnets->v4_subnets : NULL;
while (a4 && b4 && a4->prefix_length == b4->prefix_length && !memcmp(a4->addr, b4->addr, 4)) { a4 = a4->next; b4 = b4->next; }
struct TOPO_SUBNET6* a6 = old->subnets ? old->subnets->v6_subnets : NULL;
struct TOPO_SUBNET6* b6 = lq->subnets ? lq->subnets->v6_subnets : NULL;
while (a6 && b6 && a6->prefix_length == b6->prefix_length && !memcmp(a6->addr, b6->addr, 16)) { a6 = a6->next; b6 = b6->next; }
changed = a4 || b4 || a6 || b6;
}
if (!changed) {
old->last_ver = ni->ver; old->last_timestamp = ni->timestamp;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "local node unchanged: group=%016llx ver=%u", (unsigned long long)group->group_id, ni->ver);
goto unchanged;
}
name = u_strdup(instance->name);
if (!name) goto allocation_failed;
if (!ni) {
ni = u_calloc(1, sizeof(*ni)); new_identity = 1;
if (!ni) goto allocation_failed;
} else { topo_node_registry_ref(groups, instance->node_id); retained = 1; }
struct TOPO_NODE candidate = *ni;
candidate.node_id = instance->node_id; candidate.ver = (ni->ver % 255) + 1;
candidate.node_name = name; candidate.client_type = instance->client_type; candidate.client_activity = instance->client_activity;
memcpy(candidate.public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE);
memcpy(candidate.ed25519_public_key, instance->my_ed25519_pubkey, SC_PUBKEY_SIZE);
if (group->group_type != TOPO_GROUP_TYPE_CHAT && instance->rt) {
routes_changed = 1;
route_delete(instance->rt, old);
if (vc && !route_insert(instance->rt, lq)) {
DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "local node update: route conflict/allocation group=%016llx",
(unsigned long long)group->group_id); goto fail;
}
}
if (topo_node_sign_self(instance, &candidate) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: cannot publish group=%016llx", (unsigned long long)group->group_id); goto fail;
}
char* old_name = ni->node_name;
*ni = candidate; name = NULL; u_free(old_name);
if (new_identity) {
struct TOPO_NODE* stored = topo_node_registry_store(groups, ni);
if (!stored) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: cannot register identity"); goto fail; }
ni = stored; new_identity = 0; retained = 1;
}
lq->last_ver = ni->ver; lq->last_timestamp = ni->timestamp;
if (old) { topo_nodeq_clear_paths(old); topo_nodeq_free_group_fields(groups, old); u_free(old); }
group->local_node = lq;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "local node published: group=%016llx v4=%d v6=%d ver=%u timestamp=%llu",
(unsigned long long)group->group_id, vc, vc6, ni->ver, (unsigned long long)ni->timestamp);
return vc;
allocation_failed:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: preparation allocation failed group=%016llx",
(unsigned long long)group->group_id);
fail:
if (routes_changed) {
route_delete(instance->rt, lq);
if (old && old->subnets && old->subnets->v4_subnets && !route_insert(instance->rt, old))
DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "local node update: previous routes restoration failed group=%016llx",
(unsigned long long)group->group_id);
}
if (new_identity) topo_node_destroy(groups, ni);
if (retained) topo_node_registry_unref(groups, instance->node_id);
u_free(name);
unchanged:
if (lq->subnets) {
free_v4_sub_list(groups->v4_subnet_pool, lq->subnets->v4_subnets);
free_v6_sub_list(groups->v6_subnet_pool, lq->subnets->v6_subnets);
u_free(lq->subnets);
}
u_free(lq);
return changed ? -1 : vc;
}
/* Пересобирает списки адресов своего узла из ETCP-сокетов; при изменении — подпись, БД, рассылка. */
+44
View File
@@ -4,6 +4,7 @@
#include "utun_instance.h"
#include "routing_layer/topo_group.h"
#include "routing_layer/topo_node_sqlite.h"
#include "routing_layer/route_lib.h"
#include "transport_layer/secure_channel.h"
#include "transport_layer/etcp.h"
#include "config_parser.h"
@@ -292,6 +293,44 @@ static void test_bgp(struct UTUN_INSTANCE* signer) {
queue_free(group.nodes);
}
/* Замена подсети при том же количестве и откат ошибок сохраняют согласованные local_node/route table. */
static void test_local_group_update(struct UTUN_INSTANCE* inst) {
sqlite3* saved_db = inst->topo_sqlite_db;
assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK && !topo_node_sqlite_init(inst->topo_sqlite_db));
struct CFG_ROUTE_ENTRY subnet = {.ip.family = AF_INET, .netmask = 24};
subnet.ip.addr.v4.s_addr = htonl(0x0a010000);
struct utun_config config = {.my_subnets = &subnet};
inst->config = &config; inst->rt = route_table_create(inst->node_id); assert(inst->rt);
struct TOPO_GROUP group = {.instance = inst, .group_type = TOPO_GROUP_TYPE_UTUN, .group_id = TOPO_GROUP_UTUN};
memcpy(group.ed25519_public_key, inst->my_ed25519_pubkey, 32);
assert(topo_group_update_my_nodeinfo(inst, &group) == 1 && route_lookup(inst->rt, 0x0a010001));
subnet.ip.addr.v4.s_addr = htonl(0x0a020000);
assert(topo_group_update_my_nodeinfo(inst, &group) == 1 && route_lookup(inst->rt, 0x0a020001));
assert(!route_lookup(inst->rt, 0x0a010001));
struct TOPO_GROUP_NODE* before = group.local_node;
struct TOPO_NODE* identity = topo_node_registry_find(inst->topo_groups, inst->node_id);
uint64_t timestamp = identity->timestamp; uint32_t refs = identity->group_ref_count;
assert(sqlite3_exec(inst->topo_sqlite_db, "CREATE TRIGGER reject_local BEFORE UPDATE ON node_snapshots "
"BEGIN SELECT RAISE(ABORT,'test local identity rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
subnet.ip.addr.v4.s_addr = htonl(0x0a030000);
assert(topo_group_update_my_nodeinfo(inst, &group) < 0 && group.local_node == before);
assert(route_lookup(inst->rt, 0x0a020001) && !route_lookup(inst->rt, 0x0a030001));
assert(identity->timestamp == timestamp && identity->group_ref_count == refs && !topo_node_verify(identity));
assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_local", NULL, NULL, NULL) == SQLITE_OK);
struct TOPO_SUBNET4 remote_sub = {.addr = {10, 3, 0, 0}, .prefix_length = 24};
struct TOPO_NODESUBNETS remote_subnets = {.v4_subnets = &remote_sub};
struct TOPO_GROUP_NODE remote = {.node_id = 999, .subnets = &remote_subnets};
assert(route_insert(inst->rt, &remote));
assert(topo_group_update_my_nodeinfo(inst, &group) < 0 && group.local_node == before);
assert(route_lookup(inst->rt, 0x0a020001)->v_node_info == before && route_lookup(inst->rt, 0x0a030001)->v_node_info == &remote);
assert(identity->timestamp == timestamp && identity->group_ref_count == refs && !topo_node_verify(identity));
route_table_destroy(inst->rt); inst->rt = NULL;
topo_nodeq_free_group_fields(inst->topo_groups, group.local_node); u_free(group.local_node);
assert(!inst->topo_groups->node_registry->head);
sqlite3_close(inst->topo_sqlite_db); inst->topo_sqlite_db = saved_db; inst->config = NULL;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] local group: same-count subnet replacement, signature failure and route conflict rollback");
}
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_DEBUG);
@@ -305,6 +344,8 @@ int main(void) {
groups.v4_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA4), "snapshot_meta4");
groups.v6_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA6), "snapshot_meta6");
groups.v6_addr_pool = memory_pool_init(sizeof(struct TOPO_ADDR6), "snapshot_v6");
groups.v4_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET4), "snapshot_subnet4");
groups.v6_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET6), "snapshot_subnet6");
assert(groups.v4_addr_pool && groups.v4_sock_meta_pool && groups.v6_sock_meta_pool && groups.v6_addr_pool);
assert(sc_generate_keypair(&inst.my_keys) == SC_OK);
memcpy(inst.my_ed25519_privkey, inst.my_keys.private_key, 32);
@@ -317,6 +358,7 @@ int main(void) {
assert(topo_node_sqlite_init(inst.topo_sqlite_db) == 0);
test_verified_write();
test_compact_addresses(&inst);
test_local_group_update(&inst);
struct TOPO_NODE* first = record(&inst, 100, "old", 1);
assert(topo_node_verify(first) == 0);
@@ -379,6 +421,8 @@ int main(void) {
memory_pool_destroy(groups.v4_sock_meta_pool);
memory_pool_destroy(groups.v6_sock_meta_pool);
memory_pool_destroy(groups.v6_addr_pool);
memory_pool_destroy(groups.v4_subnet_pool);
memory_pool_destroy(groups.v6_subnet_pool);
sqlite3_close(inst.topo_sqlite_db);
uasync_destroy(inst.ua, 0);
return 0;