From cc04f1c0fab034ba406a766a4b23178fdb23d562 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 19:33:26 +0100 Subject: [PATCH] Test admin import rollback, commit notification and safe retry --- tests/Makefile.am | 7 ++ tests/test_chat_admin.c | 217 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 tests/test_chat_admin.c diff --git a/tests/Makefile.am b/tests/Makefile.am index a593f6cd..8022b889 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -113,6 +113,7 @@ check_PROGRAMS += \ test_mem \ test_config_updater \ test_stun_nat \ + test_chat_admin \ test_db_sync_integrity \ test_reality_fallback \ test_etcp_padding \ @@ -844,6 +845,12 @@ test_db_sync_schema_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_db_sync_schema_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) if OS_LINUX +test_chat_admin_SOURCES = test_chat_admin.c +test_chat_admin_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_chat_admin_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_chat_admin_LDFLAGS = -Wl,--wrap=sqlite3_exec -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_blob \ + -Wl,--wrap=sqlite3_bind_text -Wl,--wrap=sqlite3_step -Wl,--wrap=sqlite3_finalize -Wl,--wrap=EVP_PKEY_new_raw_private_key + test_db_sync_integrity_SOURCES = test_db_sync_integrity.c test_db_sync_integrity_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_db_sync_integrity_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_chat_admin.c b/tests/test_chat_admin.c new file mode 100644 index 00000000..b0f06db2 --- /dev/null +++ b/tests/test_chat_admin.c @@ -0,0 +1,217 @@ +/* Настоящие core/chat/router callback/SQLite/crypto: атомарность импорта и момент события успеха. */ +#include "utun_instance.h" +#include "chat/chat_core.h" +#include "chat/chat_event.h" +#include "chat/chat_admin.h" +#include "routing_layer/topo_node_sqlite.h" +#include "routing_layer/etcp_router.h" +#include "transport_layer/etcp.h" +#include "transport_layer/secure_channel.h" +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include +#include + +enum fault { NONE, SAVEPOINT, PREPARE, BIND_X, BIND_ED, BIND_CHANNEL, STEP, FINALIZE, RELEASE, ED_CRYPTO, X_CRYPTO, FAULTS }; +static enum fault fault; +static unsigned hits[FAULTS], rollback_hits; +static int fail_rollback; +static const char* import_sql = "UPDATE channels SET x25519_privkey=?, ed25519_privkey=? WHERE channel_id=?"; + +static int take(enum fault target) { + if (fault != target) return 0; + fault = NONE; hits[target]++; + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "admin test: injected fault=%u", target); return 1; +} +static int is_import(sqlite3_stmt* st) { return st && !strcmp(sqlite3_sql(st), import_sql); } + +extern int __real_sqlite3_exec(sqlite3*, const char*, int (*)(void*, int, char**, char**), void*, char**); +int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char**, char**), void* arg, char** error) { + if (!strcmp(sql, "SAVEPOINT channel_private_keys") && take(SAVEPOINT)) return SQLITE_NOMEM; + if (!strcmp(sql, "RELEASE channel_private_keys") && take(RELEASE)) return SQLITE_BUSY; + if (fail_rollback && strstr(sql, "ROLLBACK TO channel_private_keys")) { + fail_rollback = 0; rollback_hits++; + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "admin test: injected rollback failure"); return SQLITE_IOERR; + } + return __real_sqlite3_exec(db, sql, cb, arg, error); +} +extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**); +int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int len, sqlite3_stmt** st, const char** tail) { + if (!strcmp(sql, import_sql) && take(PREPARE)) { *st = NULL; return SQLITE_IOERR; } + return __real_sqlite3_prepare_v2(db, sql, len, st, tail); +} +extern int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*)); +int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* data, int len, void (*destroy)(void*)) { + if (is_import(st) && ((index == 1 && take(BIND_X)) || (index == 2 && take(BIND_ED)))) return SQLITE_NOMEM; + return __real_sqlite3_bind_blob(st, index, data, len, destroy); +} +extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*)); +int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* data, int len, void (*destroy)(void*)) { + if (is_import(st) && index == 3 && take(BIND_CHANNEL)) return SQLITE_NOMEM; + return __real_sqlite3_bind_text(st, index, data, len, destroy); +} +extern int __real_sqlite3_step(sqlite3_stmt*); +int __wrap_sqlite3_step(sqlite3_stmt* st) { + if (is_import(st) && take(STEP)) return SQLITE_IOERR; + return __real_sqlite3_step(st); +} +extern int __real_sqlite3_finalize(sqlite3_stmt*); +int __wrap_sqlite3_finalize(sqlite3_stmt* st) { + int fail = is_import(st) && take(FINALIZE); + int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; +} +extern EVP_PKEY* __real_EVP_PKEY_new_raw_private_key(int, ENGINE*, const unsigned char*, size_t); +EVP_PKEY* __wrap_EVP_PKEY_new_raw_private_key(int type, ENGINE* engine, const unsigned char* data, size_t len) { + if ((type == EVP_PKEY_ED25519 && take(ED_CRYPTO)) || (type == EVP_PKEY_X25519 && take(X_CRYPTO))) return NULL; + return __real_EVP_PKEY_new_raw_private_key(type, engine, data, len); +} + +struct fixture { + struct UASYNC* ua; + struct UTUN_INSTANCE* inst; + struct SC_MYKEYS channel; + uint8_t ed[32], public_ed[32], other[32]; + unsigned events; +}; +static struct fixture* active; + +static void sql(struct fixture* f, const char* query) { + int rc = sqlite3_exec(f->inst->topo_sqlite_db, query, NULL, NULL, NULL); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "admin test SQL rc=%d: %s", rc, sqlite3_errmsg(f->inst->topo_sqlite_db)); + assert(rc == SQLITE_OK); +} +/* Независимая проверка обеих колонок; NULL означает ожидаемое отсутствие приватного ключа. */ +static void keys(struct fixture* f, const char* channel, const uint8_t* x, const uint8_t* ed) { + sqlite3_stmt* st = NULL; sqlite3* db = f->inst->topo_sqlite_db; + assert(sqlite3_prepare_v2(db, "SELECT x25519_privkey,ed25519_privkey FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); + const uint8_t* expected[] = {x, ed}; + for (int i = 0; i < 2; i++) { + if (expected[i]) assert(sqlite3_column_bytes(st, i) == 32 && !memcmp(sqlite3_column_blob(st, i), expected[i], 32)); + else assert(sqlite3_column_type(st, i) == SQLITE_NULL); + } + assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK); +} +static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type != CHAT_EVT_ADMIN_KEY_RECEIVED) return; + struct fixture* f = active; + assert(f && inst == f->inst && sqlite3_get_autocommit(inst->topo_sqlite_db)); + assert(len == 12 && data[0] == 3 && !memcmp(data + 1, "901", 3)); + uint64_t source; memcpy(&source, data + 4, 8); assert(source == 123); + keys(f, "901", f->channel.private_key, f->ed); f->events++; +} +static void setup(struct fixture* f) { + memset(f, 0, sizeof(*f)); f->ua = uasync_create(); assert(f->ua); + f->inst = utun_instance_create_from_str(f->ua, + "[global]\nmy_node_name=admin test\n[server:udp]\naddr=127.0.0.1:0\ntype=public\n" + "[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nmember_files_enabled=0\n"); + assert(f->inst && utun_core_start(f->inst) == 0 && chat_service_start(f->inst) == 0); + assert(sc_generate_keypair(&f->channel) == SC_OK); memset(f->ed, 7, 32); memset(f->other, 5, 32); + EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, f->ed, 32); assert(key); + size_t len = 32; assert(EVP_PKEY_get_raw_public_key(key, f->public_ed, &len) == 1 && len == 32); EVP_PKEY_free(key); + uint8_t signature[64] = {0}; + assert(topo_node_sqlite_channel_put(f->inst->topo_sqlite_db, "901", "admin", f->inst->node_id, + f->channel.public_key, NULL, f->public_ed, NULL, signature) == 0); + assert(topo_node_sqlite_channel_put(f->inst->topo_sqlite_db, "902", "other", f->inst->node_id, + f->channel.public_key, f->other, f->public_ed, f->other, signature) == 0); + assert(chat_core_ensure_channel_ready(f->inst, "901") == 0 && chat_core_ensure_channel_ready(f->inst, "902") == 0); + active = f; chat_event_set_handler(f->inst, event); +} +static void cleanup(struct fixture* f) { + assert(fault == NONE && !fail_rollback && sqlite3_get_autocommit(f->inst->topo_sqlite_db)); + keys(f, "902", f->other, f->other); + chat_event_set_handler(f->inst, NULL); active = NULL; + utun_instance_destroy(f->inst); uasync_destroy(f->ua, 0); +} +/* Доставка собственной расшифрованной записи через зарегистрированный callback; сеть здесь не подменяется тестом отправки. */ +static void receive(struct fixture* f, uint64_t group, const uint8_t ed[32], const uint8_t x[32]) { + struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_HDR_SIZE + 72); assert(e); + e->len = ROUTER_SVC_HDR_SIZE + 72; memset(e->dgram, 0, e->len); + uint64_t source = 123; + e->dgram[0] = ETCP_RT_ID_ADMIN_KEY; + e->dgram[ROUTER_SVC_FLAGS_OFF] = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED; + memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &source, 8); + memcpy(e->dgram + ROUTER_SVC_DST_OFF, &f->inst->node_id, 8); + memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8); + memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF, &group, 8); + memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 8, ed, 32); + memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 40, x, 32); + struct ETCP_CONN conn = {.instance = f->inst}; + assert(f->inst->router_bindings.callbacks[ETCP_RT_ID_ADMIN_KEY]); + f->inst->router_bindings.callbacks[ETCP_RT_ID_ADMIN_KEY](&conn, e); +} +static void test_faults(void) { + struct fixture f; setup(&f); + for (int existing = 0; existing < 2; existing++) for (enum fault error = SAVEPOINT; error < FAULTS; error++) { + if (existing) assert(topo_node_sqlite_channel_set_privs(f.inst->topo_sqlite_db, "901", f.channel.private_key, f.ed) == 0); + else sql(&f, "UPDATE channels SET x25519_privkey=NULL,ed25519_privkey=NULL WHERE channel_id='901'"); + unsigned before = f.events; fault = error; + receive(&f, 901, f.ed, f.channel.private_key); + assert(fault == NONE && f.events == before && sqlite3_get_autocommit(f.inst->topo_sqlite_db)); + keys(&f, "901", existing ? f.channel.private_key : NULL, existing ? f.ed : NULL); + keys(&f, "902", f.other, f.other); + receive(&f, 901, f.ed, f.channel.private_key); assert(f.events == before + 1); + } + cleanup(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: every SQL/crypto failure preserves absent/existing keys; retry confirms only committed pair"); +} +static int refuse_commit(void* arg) { (*(unsigned*)arg)++; return 1; } +static void test_sql_and_replay(void) { + struct fixture f; setup(&f); sqlite3* db = f.inst->topo_sqlite_db; + unsigned commits = 0; sqlite3_commit_hook(db, refuse_commit, &commits); + receive(&f, 901, f.ed, f.channel.private_key); + sqlite3_commit_hook(db, NULL, NULL); + assert(commits == 1 && !f.events && sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL); + const char* triggers[] = { + "CREATE TRIGGER deny_admin BEFORE UPDATE ON channels BEGIN SELECT RAISE(ABORT,'admin denied');END", + "CREATE TRIGGER deny_admin BEFORE UPDATE ON channels BEGIN SELECT RAISE(IGNORE);END" + }; + for (unsigned i = 0; i < 2; i++) { + sql(&f, triggers[i]); receive(&f, 901, f.ed, f.channel.private_key); + assert(!f.events && sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL); sql(&f, "DROP TRIGGER deny_admin"); + } + assert(topo_node_sqlite_channel_set_privs(db, "903", f.channel.private_key, f.ed) < 0 && sqlite3_get_autocommit(db)); + receive(&f, 903, f.ed, f.channel.private_key); assert(!f.events); + uint8_t bad[32] = {1}; + receive(&f, 901, bad, f.channel.private_key); receive(&f, 901, f.ed, bad); + assert(!f.events); keys(&f, "901", NULL, NULL); + receive(&f, 901, f.ed, f.channel.private_key); receive(&f, 901, f.ed, f.channel.private_key); + assert(f.events == 2); cleanup(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: actual commit veto, UPDATE error/skip, missing channel, wrong keys and duplicate delivery"); +} +static void test_outer_transaction(void) { + struct fixture f; setup(&f); sqlite3* db = f.inst->topo_sqlite_db; + sql(&f, "BEGIN;UPDATE channels SET name='outer' WHERE channel_id='902'"); + receive(&f, 901, f.ed, f.channel.private_key); assert(!f.events && !sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL); + fault = FINALIZE; + assert(topo_node_sqlite_channel_set_privs(db, "901", f.channel.private_key, f.ed) < 0 && fault == NONE); + assert(!sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL); + char name[32]; + assert(topo_node_sqlite_channel_get(db, "902", name, sizeof(name), NULL, NULL, NULL, NULL) == 0 && !strcmp(name, "outer")); + assert(topo_node_sqlite_channel_set_privs(db, "901", f.channel.private_key, f.ed) == 0); + assert(!sqlite3_get_autocommit(db) && !f.events); keys(&f, "901", f.channel.private_key, f.ed); + sql(&f, "ROLLBACK"); keys(&f, "901", NULL, NULL); + receive(&f, 901, f.ed, f.channel.private_key); assert(f.events == 1); cleanup(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: receiver rejects external transaction; helper preserves caller ownership and rollback"); +} +static void test_rollback_failure(void) { + struct fixture f; setup(&f); int running = f.inst->running; f.inst->running = 1; + fault = RELEASE; fail_rollback = 1; receive(&f, 901, f.ed, f.channel.private_key); + assert(fault == NONE && !fail_rollback && !f.events && !f.inst->running && !sqlite3_get_autocommit(f.inst->topo_sqlite_db)); + sql(&f, "ROLLBACK"); keys(&f, "901", NULL, NULL); f.inst->running = running; + receive(&f, 901, f.ed, f.channel.private_key); assert(f.events == 1); cleanup(&f); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: rollback failure never confirms import and stops the instance"); +} +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); + size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0); + chat_core_transfer_admin(NULL, "901", 123); + struct UTUN_INSTANCE empty = {0}; chat_core_transfer_admin(&empty, "901", 123); + test_faults(); test_sql_and_replay(); test_outer_transaction(); test_rollback_failure(); + for (unsigned i = SAVEPOINT; i < FAULTS; i++) assert(hits[i] == (i == FINALIZE || i == RELEASE ? 3 : 2)); + assert(rollback_hits == 1 && u_get_allocated_count() == baseline); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: NULL context, 22 injected failures, no tracked allocations remain"); + return 0; +}