Prevent invalid old versions from winning freshness comparisons

This commit is contained in:
evgeny committed 2026-11-04 00:48:23 +01:00
1 parent 7319f900c4
commit c02d5a6cfc
2 files changed
+28 -9

No files matched your search

+19
View File
@@ -948,6 +948,13 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const
MS_ID, (unsigned long long)m->node_id, ch_id);
return 0;
}
uint8_t trusted_ed[32];
int trusted = topo_node_sqlite_get_ed25519_pubkey(db, m->node_id, trusted_ed);
if (trusted == TOPO_SQLITE_LOOKUP_ERROR) return -1;
if (!trusted && memcmp(trusted_ed, m->ed25519, 32)) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "member identity conflicts with verified node ch=%s node=%016llx",
ch_id, (unsigned long long)m->node_id); return 0;
}
/* JOIN проверяется и при наличии профиля: профиль сам по себе не связывает запись с группой. */
if (m->join_sig && !_sig_is_zero64(m->join_sig)) {
@@ -1439,6 +1446,18 @@ static int member_commit_pull(void* ctx, void* state) {
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto fail;
struct member_wire record; size_t used;
if (member_decode(bytes, length, &record, &used) || used != length) { result = MT_ERR_DATA; goto fail; }
struct member_saved previous;
int found = member_read_saved(db, pull->channel, record.rec.node_id, 0, 0, &previous);
if (found < 0) goto fail;
if (found && !previous.source) {
int valid = previous.malformed ? 0 : member_sync_verify_local_record(db, pull->channel, &previous.rec);
if (valid < 0) goto fail;
if (!valid) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "member receive ignores invalid baseline ch=%s sync=%lld node=%016llx",
pull->channel, (long long)pull->id, (unsigned long long)record.rec.node_id);
if (topo_node_sqlite_member_del(db, pull->channel, record.rec.node_id)) goto fail;
}
}
if (!record.rec.join_sig && !topo_node_sqlite_member_get_join(db, pull->channel, record.rec.node_id, record.local_join, &record.rec.join_ts))
record.rec.join_sig = record.local_join;
rc = member_apply_record(inst, pull->channel, pull->peer, &record.rec, 1);
+9 -9
View File
@@ -57,7 +57,7 @@ static struct member_pull* receive(struct UTUN_INSTANCE* source, struct UTUN_INS
assert(!member_stage_page(target, state, prefix, 0, &after, &next, more, bytes, length)); return state;
}
static void accept(struct UTUN_INSTANCE* inst, struct member_pull* pull) {
static void finish_receive(struct UTUN_INSTANCE* inst, struct member_pull* pull) {
assert(!member_commit_pull(inst, pull)); member_publish_pull(inst, pull); member_abort_pull(inst, pull);
assert(sqlite3_get_autocommit(inst->topo_sqlite_db));
}
@@ -83,30 +83,30 @@ int main(void) {
profile(&source, 2, 3);
struct member_pull* b = receive(&source, &target, 2, identities[3].id);
assert(a->id != b->id && scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 4);
accept(&target, b); assert(version(&target) == 3);
finish_receive(&target, b); assert(version(&target) == 3);
assert(scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 2);
accept(&target, a); assert(version(&target) == 3);
finish_receive(&target, a); assert(version(&target) == 3);
assert(!scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state<>0"));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS two peers: isolated staging, no early profile/hash change, stale completion preserves newer commit");
profile(&source, 2, 4); a = receive(&source, &target, 2, identities[1].id); b = receive(&source, &target, 2, identities[3].id);
member_abort_pull(&target, a);
assert(scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 2);
accept(&target, b); assert(version(&target) == 4);
finish_receive(&target, b); assert(version(&target) == 4);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS cancelling one peer leaves the other exchange intact");
profile(&source, 2, 5);
char sql[256]; snprintf(sql, sizeof(sql), "UPDATE peers_1001 SET update_sig=randomblob(64) WHERE node_id=%lld AND record_state=0",
(long long)identities[2].id);
assert(sqlite3_exec(source.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
a = receive(&source, &target, 2, identities[1].id); accept(&target, a); assert(version(&target) == 4);
a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a); assert(version(&target) == 4);
assert(sqlite3_exec(target.topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
a = receive(&source, &target, 2, identities[1].id); accept(&target, a);
a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a);
assert(!topo_node_sqlite_member_in_channel(target.topo_sqlite_db, "1001", identities[2].id));
assert(member_sync_count(&target, "1001") == 2 && topo_node_sqlite_member_allowed(target.topo_sqlite_db, "1001", identities[0].id));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS bad new keeps good old; both invalid removes only that member");
profile(&source, 2, 6); a = receive(&source, &target, 2, identities[1].id); accept(&target, a); assert(version(&target) == 6);
profile(&source, 2, 6); a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a); assert(version(&target) == 6);
profile(&source, 2, 7); a = receive(&source, &target, 2, identities[1].id);
snprintf(sql, sizeof(sql), "CREATE TRIGGER fail_identity BEFORE INSERT ON nodes WHEN NEW.node_id=%lld"
" BEGIN SELECT RAISE(ABORT,'injected identity failure'); END", (long long)identities[2].id);
@@ -121,11 +121,11 @@ int main(void) {
member_sync_destroy(&target); assert(!member_sync_init(&target));
assert(version(&target) == 6 && !scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state<>0"));
b = receive(&source, &target, 2, identities[3].id); assert(b->id > previous);
member_abort_pull(&target, a); accept(&target, b); assert(version(&target) == 7);
member_abort_pull(&target, a); finish_receive(&target, b); assert(version(&target) == 7);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS restart removes abandoned versions without reusing exchange identifiers");
corrupt(&target); profile(&source, 2, 8);
a = receive(&source, &target, 2, identities[1].id); accept(&target, a); assert(version(&target) == 8);
a = receive(&source, &target, 2, identities[1].id); finish_receive(&target, a); assert(version(&target) == 8);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS valid new version repairs a malformed previous version");
member_sync_destroy(&source); member_sync_destroy(&target);
assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK);