diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 00fbaa4a3..5ae028866 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -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); diff --git a/tests/test_member_versions.c b/tests/test_member_versions.c index 662a4e0f7..b8f286152 100644 --- a/tests/test_member_versions.c +++ b/tests/test_member_versions.c @@ -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);