// test_chat_join.c — компонентные + интеграционные тесты join-протокола. // // Задача 1 (компонентные): криптография дерева приглашений (build_sign_msg / sign_pubkey / // verify_signature), сериализация мембера, chat_join_process_request (инвайтер), хранилище // ключей (register_key self / lookup_inviter), member_sync_validate_pubkey. // // Задача 2 (интеграция): два инстанса A(инвайтер) и C(connection) — регистрация join-ключа // через etcp_router, после сходимости BGP lookup_inviter(C) == A.node_id. // // chat_core/chat_sync (глобальные синглтоны) НЕ используются: конфиг без секции [chatserver], // member_sync/chat_join инициализируются вручную. #include "chat_join.h" #include "../routing_layer/etcp_router.h" #include "../routing_layer/route_crypto.h" #include "../transport_layer/etcp_api.h" #include "member_sync.h" #include "../routing_layer/topo_node_sqlite.h" #include "../utun_instance.h" #include "../transport_layer/etcp.h" #include "../transport_layer/secure_channel.h" #include "../config_parser.h" #include "../config_updater.h" #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_node.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "test_utils.h" #include #define OPENSSL_API_COMPAT 0x10100000L #include #include #include #include #include #include #include static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; #define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0) #define OK() do { G_PASSED++; printf("OK\n"); } while(0) #define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0) #define POLL_MS 5 #define CH_COMP "42424242424" #define CH_INT "53535353535" #define JOIN_KEY 0x0BADCAFEF00DF00DULL #define MAX_NODES 2 static struct UASYNC* g_ua = NULL; static struct UTUN_INSTANCE* g_inst[MAX_NODES]; static uint64_t g_nid[MAX_NODES]; static char g_tdir[256] = "/tmp/utun_cj_XXXXXX"; static char g_cfg[MAX_NODES][256]; static char g_db_dir[MAX_NODES][320]; static int g_port[MAX_NODES]; static uint64_t g_group_id = 0; /* ── крипто-хелперы ── */ static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); EVP_PKEY* pkey = NULL; EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); EVP_PKEY_free(pkey); } static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); EVP_PKEY* pkey = NULL; EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); EVP_PKEY_free(pkey); } /* ed25519 из x25519: priv = SHA512(x_priv)[0:32], pub = sc_derive_ed25519_pubkey(x_priv) */ static void x25519_to_ed25519(const uint8_t* x_priv, uint8_t ed_priv[32], uint8_t ed_pub[32]) { EVP_MD_CTX* c = EVP_MD_CTX_new(); uint8_t h[64]; unsigned int hl = 0; EVP_DigestInit_ex(c, EVP_sha512(), NULL); EVP_DigestUpdate(c, x_priv, 32); EVP_DigestFinal_ex(c, h, &hl); EVP_MD_CTX_free(c); memcpy(ed_priv, h, 32); sc_derive_ed25519_pubkey(x_priv, ed_pub); } /* ── DB-хелперы ── */ static sqlite3* open_db(void) { sqlite3* db = NULL; sqlite3_open(":memory:", &db); topo_node_sqlite_init(db); return db; } static void make_channel(sqlite3* db, const char* ch_id, uint8_t ch_x_pub[32], uint8_t ch_x_priv[32], uint8_t ch_ed_pub[32], uint8_t ch_ed_priv[32]) { gen_x25519(ch_x_pub, ch_x_priv); gen_ed25519(ch_ed_pub, ch_ed_priv); uint8_t ch_sig[64]; sc_ed25519_sign(ch_ed_priv, (const uint8_t*)ch_id, strlen(ch_id), ch_sig); topo_node_sqlite_channel_put(db, ch_id, "test", 0, ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv, ch_sig); } /* ── джойнер: ключи + join_sig + сериализованный member-блок ── */ struct joiner { uint8_t x_pub[32], x_priv[32], ed_pub[32], ed_priv[32]; uint64_t node_id; uint8_t join_sig[64]; uint64_t join_ts; }; static void make_joiner(struct joiner* j, const uint8_t ch_x_pub[32], const uint8_t ch_ed_pub[32]) { gen_x25519(j->x_pub, j->x_priv); x25519_to_ed25519(j->x_priv, j->ed_priv, j->ed_pub); j->node_id = sc_derive_node_id_from_pubkey(j->x_pub); j->join_ts = (uint64_t)time(NULL); uint8_t jm[128]; int jl = member_sync_build_join_msg(ch_x_pub, ch_ed_pub, j->node_id, j->x_pub, j->join_ts, jm, (int)sizeof(jm)); sc_ed25519_sign(j->ed_priv, jm, (size_t)jl, j->join_sig); } static int serialize_member(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519, const uint8_t* join_sig, uint64_t join_ts, uint8_t* buf, size_t sz, size_t* len) { struct join_member_data m; memset(&m, 0, sizeof(m)); m.node_id = node_id; m.x25519 = x25519; m.ed25519 = ed25519; m.join_sig = join_sig; m.join_ts = join_ts; snprintf(m.userinfo, sizeof(m.userinfo), "{\"name\":\"J\"}"); return chat_join_serialize_member(&m, buf, sz, len); } /* ── 5.1.1 build_sign_msg ── */ static void t_build_sign_msg(void) { TEST("member_sync_build_sign_msg: 42 bytes"); { uint8_t x[32], priv[32], out[64]; gen_x25519(x, priv); int n = member_sync_build_sign_msg(x, out, (int)sizeof(out)); if (n == MEMBER_SIGN_MSG_SIZE && memcmp(out, x, 32) == 0 && memcmp(out + 32, MEMBER_SIGN_SUFFIX, MEMBER_SIGN_SUFFIX_LEN) == 0) OK(); else FAIL("n=%d", n); } TEST("member_sync_build_sign_msg: short buffer -> -1"); { uint8_t x[32], priv[32], out[41]; gen_x25519(x, priv); if (member_sync_build_sign_msg(x, out, (int)sizeof(out)) == -1) OK(); else FAIL(); } } /* ── 5.1.2 sign_pubkey / verify_signature ── */ static void t_sign_verify(void) { sqlite3* db = open_db(); uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; make_channel(db, CH_COMP, ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv); (void)ch_x_priv; /* joiner (подписываемый pubkey) и «другой» pubkey для подмены */ uint8_t jx[32], jpriv[32]; gen_x25519(jx, jpriv); uint8_t other_x[32], other_priv[32]; gen_x25519(other_x, other_priv); (void)jpriv; (void)other_priv; /* родитель (для ребёнка) */ uint8_t p_x_pub[32], p_x_priv[32], p_ed_pub[32], p_ed_priv[32]; gen_x25519(p_x_pub, p_x_priv); x25519_to_ed25519(p_x_priv, p_ed_priv, p_ed_pub); uint64_t parent_id = sc_derive_node_id_from_pubkey(p_x_pub); topo_node_sqlite_node_update_verified(db, parent_id, "parent", p_x_pub, p_ed_pub, (uint64_t)time(NULL), time(NULL)); struct ms_member_rec rec; uint8_t sig[64]; TEST("verify_signature: root (signed_by=0, channel ed25519) == 1"); { member_sync_sign_pubkey(ch_ed_priv, jx, sig); memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = 0; if (member_sync_verify_signature(db, CH_COMP, &rec) == 1) OK(); else FAIL(); } TEST("verify_signature: child (signed_by=parent) == 1"); { member_sync_sign_pubkey(p_ed_priv, jx, sig); memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = parent_id; if (member_sync_verify_signature(db, CH_COMP, &rec) == 1) OK(); else FAIL(); } TEST("verify_signature: tampered x25519 == 0"); { member_sync_sign_pubkey(ch_ed_priv, jx, sig); memset(&rec, 0, sizeof(rec)); rec.x25519 = other_x; rec.signature = sig; rec.signed_by = 0; if (member_sync_verify_signature(db, CH_COMP, &rec) == 0) OK(); else FAIL(); } TEST("verify_signature: parent not in nodes == -1"); { member_sync_sign_pubkey(ch_ed_priv, jx, sig); memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = 0x7777ULL; if (member_sync_verify_signature(db, CH_COMP, &rec) == -1) OK(); else FAIL(); } TEST("verify_signature: no channel == -1"); { member_sync_sign_pubkey(ch_ed_priv, jx, sig); memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = 0; if (member_sync_verify_signature(db, "99999999999", &rec) == -1) OK(); else FAIL(); } sqlite3_close(db); } /* ── 5.1.3 serialize_member / parse_member ── */ static void t_serialize_parse(void) { uint8_t x[32], xp[32], ed[32], edp[32], jsig[64], usig[64]; gen_x25519(x, xp); gen_ed25519(ed, edp); memset(jsig, 0x11, 64); memset(usig, 0x22, 64); struct join_member_data m; memset(&m, 0, sizeof(m)); m.node_id = 0x1234567890abcdefULL; m.x25519 = x; m.ed25519 = ed; m.join_sig = jsig; m.join_ts = 1234567ULL; m.update_sig = usig; m.update_ts = 7654321ULL; strcpy(m.userinfo, "{\"name\":\"J\"}"); uint8_t buf[512]; size_t len = 0; TEST("serialize_member + parse_member roundtrip"); { struct join_member_data p; memset(&p, 0, sizeof(p)); if (chat_join_serialize_member(&m, buf, sizeof(buf), &len) != 0 || chat_join_parse_member(buf, len, &p) != 0 || p.node_id != m.node_id || memcmp(p.x25519, x, 32) || memcmp(p.ed25519, ed, 32) || memcmp(p.join_sig, jsig, 64) || p.join_ts != m.join_ts || memcmp(p.update_sig, usig, 64) || p.update_ts != m.update_ts || strcmp(p.userinfo, m.userinfo) != 0) FAIL(); else OK(); } TEST("serialize_member: small buffer -> -1"); { if (chat_join_serialize_member(&m, buf, 10, &len) == -1) OK(); else FAIL(); } TEST("parse_member: short input -> -1"); { struct join_member_data p; if (chat_join_parse_member(buf, 10, &p) == -1) OK(); else FAIL(); } TEST("serialize_member: userinfo up to 255"); { char ui[256]; memset(ui, 'a', 255); ui[255] = '\0'; struct join_member_data ml = m; ml.userinfo[0] = '\0'; memcpy(ml.userinfo, ui, 256); uint8_t b2[512]; size_t l2 = 0; struct join_member_data p; memset(&p, 0, sizeof(p)); if (chat_join_serialize_member(&ml, b2, sizeof(b2), &l2) != 0 || chat_join_parse_member(b2, l2, &p) != 0 || strlen(p.userinfo) != 255 || memcmp(p.userinfo, ui, 255) != 0) FAIL(); else OK(); } } /* ── config/instance хелперы ── */ static int wf(const char* p, const char* f, ...) { va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1; va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0; } static char* gv(const char* p, const char* k) { struct utun_config* c = parse_config(p); if (!c) return NULL; char* r = (strcmp(k, "pub") == 0) ? u_strdup(c->global.my_public_key_hex) : u_strdup(c->global.my_private_key_hex); free_config(c); return r; } static void t_rmdir(const char* p) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", p); system(cmd); } /* пишет конфиг n0 (component) — без клиентов */ static void write_comp_config(void) { snprintf(g_cfg[0], sizeof(g_cfg[0]), "%s/comp.conf", g_tdir); snprintf(g_db_dir[0], sizeof(g_db_dir[0]), "%s/dbcomp", g_tdir); utun_mkdir(g_db_dir[0], 0755); g_port[0] = 54000 + (getpid() % 4000); wf(g_cfg[0], "[global]\ntun_ip=10.98.70.1/24\ntun_ifname=tun70\ndb_path=%s\n" "[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", g_db_dir[0], g_port[0]); config_ensure_keys_and_node_id(g_cfg[0]); struct utun_config* c = parse_config(g_cfg[0]); g_nid[0] = c->global.my_node_id; free_config(c); } /* пишет конфиги a.conf (инвайтер, сервер) и c.conf (connection, клиент → a) */ static void write_int_configs(void) { for (int i = 0; i < 2; i++) { snprintf(g_cfg[i], sizeof(g_cfg[i]), "%s/%s.conf", g_tdir, i ? "c" : "a"); snprintf(g_db_dir[i], sizeof(g_db_dir[i]), "%s/db%s", g_tdir, i ? "c" : "a"); utun_mkdir(g_db_dir[i], 0755); g_port[i] = 55000 + (getpid() % 3000) + i * 1000; } for (int i = 0; i < 2; i++) { wf(g_cfg[i], "[global]\ntun_ip=10.98.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s\n" "[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", i, i, g_db_dir[i], g_port[i]); config_ensure_keys_and_node_id(g_cfg[i]); struct utun_config* c = parse_config(g_cfg[i]); g_nid[i] = c->global.my_node_id; free_config(c); } /* c (i=1) — клиент к a (i=0) */ { char* pr = gv(g_cfg[1], "priv"); char* pu = gv(g_cfg[1], "pub"); char* p0_pu = gv(g_cfg[0], "pub"); wf(g_cfg[1], "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.98.1.1/24\ntun_ifname=tun10\n" "db_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n" "[client: to_a]\nkeepalive=1\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n[allowed_keys]\nallow_all=1\n", pr, pu, g_db_dir[1], g_port[1], p0_pu, g_port[0]); u_free(pr); u_free(pu); u_free(p0_pu); } } static int create_instances(int n) { for (int i = 0; i < n; i++) { g_inst[i] = utun_instance_create(g_ua, g_cfg[i]); if (!g_inst[i]) { printf("FAIL: create n%d\n", i); return -1; } utun_instance_init(g_inst[i]); member_sync_init(g_inst[i]); } return 0; } static void destroy_instances(int n) { for (int i = 0; i < n; i++) { if (g_inst[i]) { chat_join_destroy(g_inst[i]); member_sync_destroy(g_inst[i]); g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL; } } } static int wait_connected(int n) { if (n <= 1) return 1; int need = n * (n - 1); for (int a = 0; a < 2000; a++) { int ok = 0; for (int i = 0; i < n; i++) { struct ll_entry* e = g_inst[i]->connections->head; while (e) { struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; if (ce->conn && ce->conn->links) { struct ETCP_LINK* l; for (l = ce->conn->links; l; l = l->next) if (l->initialized && ce->conn->crypto_ctx.initialized) ok++; } e = e->next; } } if (ok >= need) return 1; uasync_poll(g_ua, POLL_MS); } return 0; } static int wait_chat_bgp(int n) { for (int a = 0; a < 8000; a++) { int ok = 1; for (int i = 0; i < n && ok; i++) { struct TOPO_GROUP* g = topo_groups_find(g_inst[i]->topo_groups, g_group_id); if (!g) { ok = 0; break; } for (int m = 0; m < n; m++) { if (m == i) continue; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, g_nid[m]); if (!nq || !nq->paths || !nq->paths->head) { ok = 0; break; } } } if (ok) return 1; uasync_poll(g_ua, POLL_MS); } return 0; } /* ── 5.1.4 chat_join_process_request (инвайтер) ── */ static void t_process_request(void) { struct UTUN_INSTANCE* inst = g_inst[0]; sqlite3* db = inst->topo_sqlite_db; uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; make_channel(db, CH_COMP, ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv); (void)ch_x_priv; (void)ch_ed_priv; /* инвайтер (self) в nodes — чтобы verify_signature резолвил signed_by */ topo_node_sqlite_node_update_verified(db, inst->node_id, "inviter", inst->my_keys.public_key, inst->my_ed25519_pubkey, (uint64_t)time(NULL), time(NULL)); uint64_t group_id = strtoull(CH_COMP, NULL, 10); /* 0. неизвестный join-ключ → отклонён (ключ ещё не зарегистрирован) */ TEST("process_request: unknown join_key rejected"); { struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); uint8_t buf[512]; size_t len = 0; serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); if (!topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); } /* регистрируем ключ локально — дальше process_request его принимает */ chat_join_store_local_key(inst, group_id, JOIN_KEY); /* 1. валидный join_sig → мембер добавлен, signed_by=self, подпись валидна */ TEST("process_request: valid join_sig accepted"); { struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); uint8_t buf[512]; size_t len = 0; serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); uint64_t sb = 0; uint8_t sig[64]; memset(sig, 0, sizeof(sig)); int in_ch = topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id); int gs = topo_node_sqlite_member_get_sign(db, CH_COMP, j.node_id, &sb, sig); struct ms_member_rec rec; memset(&rec, 0, sizeof(rec)); rec.x25519 = j.x_pub; rec.signature = sig; rec.signed_by = sb; int vs = (gs == 0) ? member_sync_verify_signature(db, CH_COMP, &rec) : -99; if (in_ch == 1 && gs == 0 && sb == inst->node_id && vs == 1) OK(); else FAIL("in_ch=%d gs=%d sb=%016llx vs=%d", in_ch, gs, (unsigned long long)sb, vs); } /* 2. невалидный join_sig → отклонён */ TEST("process_request: invalid join_sig rejected"); { struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); uint8_t bad_sig[64]; memcpy(bad_sig, j.join_sig, 64); bad_sig[0] ^= 0xFF; uint8_t buf[512]; size_t len = 0; serialize_member(j.node_id, j.x_pub, j.ed_pub, bad_sig, j.join_ts, buf, sizeof(buf), &len); chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); if (!topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); } /* 3. node_id != derive(x25519) → отклонён */ TEST("process_request: node_id mismatch rejected"); { struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); uint8_t buf[512]; size_t len = 0; serialize_member(j.node_id ^ 0x12345ULL, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); if (!topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); } } /* ── 5.1.5 chat_join_register_key(self) / lookup_inviter ── */ static void t_register_key_self(void) { struct UTUN_INSTANCE* inst = g_inst[0]; uint64_t group_id = strtoull(CH_COMP, NULL, 10); TEST("register_key(target=self) -> lookup_inviter == self"); { chat_join_store_local_key(inst, group_id, JOIN_KEY); uint64_t inv = chat_join_lookup_inviter(inst, group_id, JOIN_KEY); if (inv == inst->node_id) OK(); else FAIL("inv=%016llx", (unsigned long long)inv); } TEST("lookup_inviter: unknown key -> 0"); { if (chat_join_lookup_inviter(inst, group_id, JOIN_KEY + 1) == 0) OK(); else FAIL(); } } /* ── 5.1.6 member_sync_validate_pubkey ── */ static void t_validate_pubkey(void) { struct UTUN_INSTANCE* inst = g_inst[0]; sqlite3* db = inst->topo_sqlite_db; uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; make_channel(db, "61616161616", ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv); (void)ch_x_priv; (void)ch_ed_priv; struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); uint64_t gid = strtoull("61616161616", NULL, 10); uint8_t buf[512]; size_t len = 0; serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); chat_join_store_local_key(inst, gid, JOIN_KEY); chat_join_process_request(inst, gid, j.node_id, JOIN_KEY, buf, len); TEST("validate_pubkey: member -> 1"); { if (member_sync_validate_pubkey(inst, "61616161616", j.x_pub) == 1) OK(); else FAIL(); } TEST("validate_pubkey: non-member -> 0"); { uint8_t ox[32], op[32]; gen_x25519(ox, op); (void)op; if (member_sync_validate_pubkey(inst, "61616161616", ox) == 0) OK(); else FAIL(); } } struct registration_result { int calls, result; }; static void registered(void* arg, int result) { struct registration_result* r = arg; r->calls++; r->result = result; } static void poll_ms(int ms) { uint64_t until = get_time_tb() + (uint64_t)ms * 10; while (get_time_tb() < until) uasync_poll(g_ua, POLL_MS); } static int wait_registered(struct registration_result* r) { uint64_t until = get_time_tb() + JOIN_REGISTER_TIMEOUT_TB + 10000; while (!r->calls && get_time_tb() < until) uasync_poll(g_ua, POLL_MS); return r->calls == 1 && r->result == CHAT_JOIN_OK; } static etcp_recv_fn original_join_recv; static struct ll_entry* held_ack; static struct ETCP_CONN* held_conn; static void hold_ack(struct ETCP_CONN* conn, struct ll_entry* entry) { if (entry->len > ROUTER_SVC_PAYLOAD_OFF && entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == JOIN_SUBCMD_KEY_REGISTER_ACK) { if (held_ack) { queue_dgram_free(held_ack); queue_entry_free(held_ack); etcp_conn_ref_free(held_conn); } if (etcp_conn_ref_take(conn) != 0) abort(); held_ack = entry; held_conn = conn; } else original_join_recv(conn, entry); } static void release_ack(void) { struct ll_entry* entry = held_ack; held_ack = NULL; if (entry) { original_join_recv(held_conn, entry); etcp_conn_ref_free(held_conn); } } /* Инъекция после transport decode: проверяем корреляцию и malformed, без подделки криптографии. */ static void inject_ack(uint64_t peer, uint64_t group, uint64_t key, int status, int size, int flags) { struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + 10); e->len = ROUTER_SVC_PAYLOAD_OFF + size; memset(e->dgram, 0, e->memlen); e->dgram[ROUTER_SVC_FLAGS_OFF] = flags; memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &peer, 8); memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8); e->dgram[ROUTER_SVC_PAYLOAD_OFF] = JOIN_SUBCMD_KEY_REGISTER_ACK; memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, &key, 8); e->dgram[ROUTER_SVC_PAYLOAD_OFF + 9] = status; struct ETCP_CONN conn = {0}; conn.instance = g_inst[0]; original_join_recv(&conn, e); } static void t_registration_edges(void) { original_join_recv = g_inst[0]->router_bindings.callbacks[ETCP_RT_ID_JOIN]; etcp_router_bind(g_inst[0], ETCP_RT_ID_JOIN, hold_ack); TEST("registration: delayed ACK, wrong peer/channel/key, duplicate"); { struct registration_result r = {0}; uint64_t key = JOIN_KEY + 10; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); poll_ms(200); int ok = req && held_ack && !r.calls && chat_join_lookup_inviter(g_inst[1], g_group_id, key) == g_nid[0]; inject_ack(g_nid[1] ^ 1, g_group_id, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); inject_ack(g_nid[1], g_group_id + 1, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); inject_ack(g_nid[1], g_group_id, key + 1, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); inject_ack(g_nid[1], g_group_id, key, 0, 9, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); inject_ack(g_nid[1], g_group_id, key, 2, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); inject_ack(g_nid[1], g_group_id, key, 0, 10, 0); inject_ack(g_nid[1], g_group_id, key, 0, 10, ROUTER_FLAG_ENCRYPTED); ok = ok && !r.calls && !chat_join_lookup_inviter(g_inst[0], g_group_id, key); release_ack(); inject_ack(g_nid[1], g_group_id, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); if (ok && r.calls == 1 && r.result == CHAT_JOIN_OK && !g_inst[0]->join_registrations) OK(); else FAIL(); if (!r.calls && req) chat_join_cancel_registration(req); } TEST("registration: cancelled request ignores late ACK"); { struct registration_result r = {0}; uint64_t key = JOIN_KEY + 11; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); poll_ms(100); int ok = req && held_ack && !r.calls; if (req) chat_join_cancel_registration(req); release_ack(); poll_ms(30); if (ok && r.calls == 1 && r.result == CHAT_JOIN_CANCELLED && !g_inst[0]->join_registrations && !chat_join_lookup_inviter(g_inst[0], g_group_id, key)) OK(); else FAIL(); } TEST("registration: missing ACK times out once, late ACK ignored"); { struct registration_result r = {0}; uint64_t key = JOIN_KEY + 12; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); wait_registered(&r); int had_ack = held_ack != NULL; release_ack(); poll_ms(20); if (req && had_ack && r.calls == 1 && r.result == CHAT_JOIN_TIMEOUT && !g_inst[0]->join_registrations && !chat_join_lookup_inviter(g_inst[0], g_group_id, key)) OK(); else FAIL(); if (!r.calls && req) chat_join_cancel_registration(req); } etcp_router_bind(g_inst[0], ETCP_RT_ID_JOIN, original_join_recv); TEST("registration: concurrent keys and duplicate pending key"); { struct registration_result r[2] = {{0}, {0}}, duplicate = {0}; struct chat_join_registration* a = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 20, registered, &r[0]); struct chat_join_registration* b = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 21, registered, &r[1]); struct chat_join_registration* d = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 20, registered, &duplicate); int first = wait_registered(&r[0]), second = wait_registered(&r[1]); if (a && b && !d && !duplicate.calls && first && second && !g_inst[0]->join_registrations) OK(); else FAIL(); if (!r[0].calls && a) chat_join_cancel_registration(a); if (!r[1].calls && b) chat_join_cancel_registration(b); if (d) chat_join_cancel_registration(d); } TEST("registration: conflict is rejected without replacing inviter"); { uint64_t key = JOIN_KEY + 22; chat_join_store_local_key(g_inst[1], g_group_id, key); struct registration_result r = {0}; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); wait_registered(&r); if (req && r.calls == 1 && r.result == CHAT_JOIN_ERROR && chat_join_lookup_inviter(g_inst[1], g_group_id, key) == g_nid[1]) OK(); else FAIL(); if (!r.calls && req) chat_join_cancel_registration(req); } TEST("registration: send failure leaves no pending timer or callback"); { struct registration_result r = {0}; struct ll_queue* saved = g_inst[0]->router_conns; g_inst[0]->router_conns = NULL; /* inject router allocation/unavailability failure */ TimeoutEntry timer; timeout_heap_peek(g_ua->timeout_heap, &timer); size_t before = g_ua->timer_alloc_count - g_ua->timer_free_count; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 26, registered, &r); uint8_t key[32] = {0}; struct join_member_data member = {0}; member.node_id = g_nid[0]; member.x25519 = member.ed25519 = key; int forwarded = chat_join_forward_request(g_inst[0], g_group_id, g_nid[1], JOIN_KEY, &member); g_inst[0]->router_conns = saved; timeout_heap_peek(g_ua->timeout_heap, &timer); /* освобождает отменённые таймеры без вызова callback */ if (!req && !r.calls && forwarded == -1 && !g_inst[0]->join_registrations && before == g_ua->timer_alloc_count - g_ua->timer_free_count) OK(); else FAIL("req=%p calls=%d forward=%d timers=%zu/%zu", (void*)req, r.calls, forwarded, before, g_ua->timer_alloc_count - g_ua->timer_free_count); if (req) chat_join_cancel_registration(req); } TEST("registration: self callback deferred, cancellation, shutdown"); { struct registration_result r = {0}, cancelled = {0}, shutdown = {0}; struct chat_join_registration* a = chat_join_register_key(g_inst[0], g_group_id, 0, JOIN_KEY + 23, registered, &r); int deferred = !r.calls; int ok = wait_registered(&r); struct chat_join_registration* b = chat_join_register_key(g_inst[0], g_group_id, 0, JOIN_KEY + 24, registered, &cancelled); if (b) chat_join_cancel_registration(b); struct chat_join_registration* c = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 25, registered, &shutdown); chat_join_destroy(g_inst[0]); chat_join_init(g_inst[0]); poll_ms(100); if (a && b && c && deferred && ok && cancelled.calls == 1 && cancelled.result == CHAT_JOIN_CANCELLED && shutdown.calls == 1 && shutdown.result == CHAT_JOIN_CANCELLED && !g_inst[0]->join_registrations) OK(); else FAIL(); } TEST("registration: connection node shutdown times out without success"); { struct registration_result r = {0}; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 27, registered, &r); chat_join_destroy(g_inst[1]); member_sync_destroy(g_inst[1]); g_inst[1]->running = 0; utun_instance_destroy(g_inst[1]); g_inst[1] = NULL; wait_registered(&r); if (req && r.calls == 1 && r.result == CHAT_JOIN_TIMEOUT && !g_inst[0]->join_registrations && !chat_join_lookup_inviter(g_inst[0], g_group_id, JOIN_KEY + 27)) OK(); else FAIL(); if (!r.calls && req) chat_join_cancel_registration(req); } } /* ── 5.2 интеграция: register_key через etcp_router ── */ static void t_integration(void) { g_group_id = strtoull(CH_INT, NULL, 10); write_int_configs(); if (create_instances(2) != 0) return; TEST("integration: A<->C connected"); { if (wait_connected(2)) OK(); else FAIL(); } /* CHAT-группа + канал + мемберы (после connect — чтобы сработал passive topo_group_new_conn) */ { uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; gen_x25519(ch_x_pub, ch_x_priv); gen_ed25519(ch_ed_pub, ch_ed_priv); uint8_t ch_sig[64]; sc_ed25519_sign(ch_ed_priv, (const uint8_t*)CH_INT, strlen(CH_INT), ch_sig); for (int i = 0; i < 2; i++) { sqlite3* db = g_inst[i]->topo_sqlite_db; topo_node_sqlite_channel_put(db, CH_INT, "test", g_nid[0], ch_x_pub, NULL, ch_ed_pub, NULL, ch_sig); if (!topo_groups_find(g_inst[i]->topo_groups, g_group_id)) topo_groups_create_group(g_inst[i]->topo_groups, g_group_id, TOPO_GROUP_TYPE_CHAT, CH_INT); for (int m = 0; m < 2; m++) { member_sync_put(g_inst[i], CH_INT, g_nid[m], g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, NULL, 0, NULL, 0, "{\"name\":\"node\"}", NULL, NULL, 0, 0, NULL); topo_node_sqlite_node_update_verified(g_inst[i]->topo_sqlite_db, g_nid[m], "node", g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, (uint64_t)time(NULL), time(NULL)); } chat_join_init(g_inst[i]); } } TEST("integration: CHAT group BGP converged"); { if (wait_chat_bgp(2)) OK(); else FAIL(); } for (int repeat = 0; repeat < 2; repeat++) { TEST(repeat ? "integration: repeated registration confirmed" : "integration: registration confirmed after C stores key"); struct registration_result r = {0}; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY, registered, &r); int deferred = !r.calls, ok = wait_registered(&r); if (req && deferred && ok && chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY) == g_nid[0]) OK(); else FAIL(); if (!r.calls && req) chat_join_cancel_registration(req); } t_registration_edges(); destroy_instances(2); } /* ── main ── */ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); if (getenv("UTUN_TEST_DEBUG")) { debug_set_category_level_by_name("chat_sync", "info"); debug_set_category_level_by_name("member_sync", "info"); debug_set_category_level_by_name("etcp_route", "info"); debug_set_category_level_by_name("bgp", "trace"); } utun_instance_set_tun_init_enabled(0); srand((unsigned)time(NULL)); printf("=== test_chat_join ===\n"); { char t[256] = "/tmp/utun_cj_XXXXXX"; (void)test_mkdtemp(t); snprintf(g_tdir, sizeof(g_tdir), "%s", t); } g_ua = uasync_create(); if (!g_ua) { printf("FAIL: uasync_create\n"); return 1; } /* ── компонентные (без инстанса) ── */ t_build_sign_msg(); t_sign_verify(); t_serialize_parse(); /* ── компонентные (один инстанс, без [chatserver]) ── */ write_comp_config(); if (create_instances(1) == 0) { t_process_request(); t_register_key_self(); t_validate_pubkey(); destroy_instances(1); } /* ── интеграция (2 инстанса через etcp_router) ── */ t_integration(); fflush(stdout); fflush(stderr); printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); fflush(stdout); if (g_ua) { uasync_poll(g_ua, 0); if (g_ua->timer_alloc_count != g_ua->timer_free_count) { fprintf(stderr, "timer leak: allocated=%zu freed=%zu\n", g_ua->timer_alloc_count, g_ua->timer_free_count); G_FAILED++; } uasync_destroy(g_ua, 0); g_ua = NULL; } t_rmdir(g_tdir); return G_FAILED > 0 ? 1 : 0; }