Files
utun2/tests/test_chat_join_e2e.c
T

716 lines
33 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test_chat_join_e2e.c — E2E тест join-протокола через реальные chat_core/chat_sync.
//
// Много-процессный: chat_core/chat_sync — глобальные синглтоны на процесс, поэтому
// роли A(инвайтер)/C(connection)/J(джойнер) разнесены по fork-процессам.
// Статические данные (ключи канала, node_id, pubkey) заполняются родителем ДО fork и
// наследуются детьми через память; динамический обмен — invite-ссылка через файл.
// Координация результата — коды возврата (0=OK, 1=FAIL).
//
// Сценарии:
// happy A≠C: A регистрирует ключ на C, J входит через C, у A и C мембер J с signed_by=A.
// degraded A==C: J входит напрямую в A (ключ локально, форварда нет).
// invalid неверный ключ: J отклонён, мембера J нет.
#include "chat_core.h"
#include "chat_core_priv.h"
#include "chat_sync.h"
#include "chat_join.h"
#include "chat_headless_control.h"
#include "../lib/socket_compat.h"
#include "../routing_layer/etcp_router.h"
#include "../transport_layer/etcp_api.h"
#include "invite_link.h"
#include "invite_build.h"
#include "member_sync.h"
#include "chat_event.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_node.h"
#include "../utun_instance.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/node_conn_direct.h"
#include "../transport_layer/secure_channel.h"
#include "../ntp_time.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/platform_compat.h"
#include <sqlite3.h>
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>
#ifndef _WIN32
#include <unistd.h>
#include <sys/wait.h>
#endif
#ifdef _WIN32
int main(void) {
printf("[SKIP] test_chat_join_e2e — fork() not available on Windows\n");
return 0;
}
#else
#define POLL_MS 5
#define CH_NAME "test-channel"
#define IDX_A 0
#define IDX_C 1
#define IDX_J 2
/* общие данные (заполняются родителем, наследуются детьми через fork) */
struct e2e_shared {
char ch_id[64];
uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32], ch_sig[64];
uint64_t owner;
uint64_t nid[3];
uint8_t x_pub[3][32], x_priv[3][32], ed_pub[3][32];
int port[3];
int degraded;
};
static struct e2e_shared g_sh;
static int early_bgp_membership;
/* Проверяем границу доступа на каждом шаге реального event loop. */
static void poll_checked(struct UTUN_INSTANCE* inst) {
uasync_poll(inst->ua, POLL_MS);
if (inst->node_id == g_sh.nid[IDX_J]) return;
if (topo_node_sqlite_member_in_channel(inst->topo_sqlite_db, g_sh.ch_id, g_sh.nid[IDX_J])) return;
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, strtoull(g_sh.ch_id, NULL, 10));
for (struct ll_entry* e = group && group->senders_list ? group->senders_list->head : NULL; e; e = e->next) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data;
if (item->conn && item->conn->peer_node_id == g_sh.nid[IDX_J]) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "test: J entered BGP before member commit group=%s", g_sh.ch_id);
early_bgp_membership = 1;
}
}
}
struct invite_result { int calls, result; char link[1024]; };
static void invite_ready(void* arg, int result, const char* link) {
struct invite_result* r = arg;
r->calls++; r->result = result;
if (link) snprintf(r->link, sizeof(r->link), "%s", link);
}
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)
/* ── крипто-хелперы ── */
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);
}
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);
}
static void to_hex(const uint8_t* bin, size_t n, char* out) {
for (size_t i = 0; i < n; i++) sprintf(out + i * 2, "%02x", bin[i]);
out[n * 2] = '\0';
}
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 void fill_shared(int base_port, int degraded) {
memset(&g_sh, 0, sizeof(g_sh));
g_sh.degraded = degraded;
snprintf(g_sh.ch_id, sizeof(g_sh.ch_id), "4242424242424242");
for (int i = 0; i < 3; i++) {
gen_x25519(g_sh.x_pub[i], g_sh.x_priv[i]);
uint8_t ed_priv[32];
x25519_to_ed25519(g_sh.x_priv[i], ed_priv, g_sh.ed_pub[i]);
g_sh.nid[i] = sc_derive_node_id_from_pubkey(g_sh.x_pub[i]);
g_sh.port[i] = base_port + i * 1000;
}
gen_x25519(g_sh.ch_x_pub, g_sh.ch_x_priv);
gen_ed25519(g_sh.ch_ed_pub, g_sh.ch_ed_priv);
g_sh.owner = g_sh.nid[IDX_A];
{
uint8_t msg[256]; size_t off = 0;
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", g_sh.ch_id) + 1;
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", CH_NAME) + 1;
memcpy(msg + off, &g_sh.owner, 8); off += 8;
memcpy(msg + off, g_sh.ch_x_pub, 32); off += 32;
memcpy(msg + off, g_sh.ch_ed_pub, 32); off += 32;
sc_ed25519_sign(g_sh.ch_ed_priv, msg, off, g_sh.ch_sig);
}
}
static void write_configs(const char* dir) {
for (int i = 0; i < 3; i++) {
char priv[65], pub[65], path[512], dbd[512];
to_hex(g_sh.x_priv[i], 32, priv);
to_hex(g_sh.x_pub[i], 32, pub);
snprintf(path, sizeof(path), "%s/%s.conf", dir, i == IDX_A ? "a" : i == IDX_C ? "c" : "j");
snprintf(dbd, sizeof(dbd), "%s/db%s", dir, i == IDX_A ? "a" : i == IDX_C ? "c" : "j");
utun_mkdir(dbd, 0755);
wf(path,
"[global]\ntun_ip=10.98.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s/db%s\n"
"my_private_key=%s\nmy_public_key=%s\n"
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n"
"[chatserver]\nstorage_autoload=0\n[allowed_keys]\nallow_all=1\n",
i, i, dir, i == IDX_A ? "a" : i == IDX_C ? "c" : "j",
priv, pub, g_sh.port[i]);
}
}
/* ── poll-хелперы (дети) ── */
static int wait_conn(struct UTUN_INSTANCE* inst, uint64_t nid, int max_iter) {
for (int a = 0; a < max_iter; a++) {
struct ETCP_CONN* c = instance_find_conn(inst, nid);
if (c && c->initialized && c->links_up) return 1;
poll_checked(inst);
}
{
struct ETCP_CONN* c = instance_find_conn(inst, nid);
fprintf(stderr, " [conn-fail] self=0x%016llx target=0x%016llx conn=%p init=%d links_up=%d\n",
(unsigned long long)inst->node_id, (unsigned long long)nid, (void*)c,
c ? c->initialized : -1, c ? c->links_up : -1);
}
return 0;
}
static int wait_bgp(struct UTUN_INSTANCE* inst, uint64_t nid, int max_iter) {
uint64_t gid = strtoull(g_sh.ch_id, NULL, 10);
for (int a = 0; a < max_iter; a++) {
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
if (g) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid);
if (nq && nq->paths && nq->paths->head) return 1;
}
poll_checked(inst);
}
{
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, nid) : NULL;
fprintf(stderr, " [bgp-fail] gid=%016llx group=%p nq=%p paths_head=%p self=0x%016llx target=0x%016llx\n",
(unsigned long long)gid, (void*)g, (void*)nq,
(void*)(nq && nq->paths ? nq->paths->head : NULL),
(unsigned long long)inst->node_id, (unsigned long long)nid);
}
return 0;
}
/* ждать, пока мембер получит подпись дерева от want_sb (не плейсхолдер) */
static int wait_signed(struct UTUN_INSTANCE* inst, uint64_t nid, uint64_t want_sb, int max_iter) {
for (int a = 0; a < max_iter; a++) {
uint64_t sb = 0; uint8_t sig[64];
if (topo_node_sqlite_member_get_sign(inst->topo_sqlite_db, g_sh.ch_id, nid, &sb, sig) == 0
&& sb == want_sb) return 1;
poll_checked(inst);
}
{
uint64_t sb = 0; uint8_t sig[64];
int gr = topo_node_sqlite_member_get_sign(inst->topo_sqlite_db, g_sh.ch_id, nid, &sb, sig);
fprintf(stderr, " [signed-fail] nid=0x%016llx want_sb=0x%016llx get_sign_rc=%d cur_sb=0x%016llx\n",
(unsigned long long)nid, (unsigned long long)want_sb, gr, (unsigned long long)sb);
}
return 0;
}
static int wait_not_member(struct UTUN_INSTANCE* inst, uint64_t nid, int max_iter) {
for (int a = 0; a < max_iter; a++) {
if (topo_node_sqlite_member_in_channel(inst->topo_sqlite_db, g_sh.ch_id, nid)) return 0;
poll_checked(inst);
}
return 1;
}
static int wait_file(struct UTUN_INSTANCE* inst, const char* path, int max_iter) {
for (int a = 0; a < max_iter; a++) {
if (access(path, F_OK) == 0) return 1;
poll_checked(inst);
}
return 0;
}
/* JOIN_READY (успех join) наблюдается через CHAT_EVT_CONNECT_RESULT(result=0):
* сигнал точный (в отличие от senders_list, заполняемого и ncd-подключением)
* и не зависит от того, успел ли connection-узел остаться в живых до синка мембера. */
static int g_j_join_ok = 0;
static void join_event_handler(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
(void)inst;
if (type == CHAT_EVT_CONNECT_RESULT && len >= 12) {
int r = 0; memcpy(&r, data + 8, 4);
if (r == 0) g_j_join_ok = 1;
}
}
static int wait_join_result(struct UTUN_INSTANCE* inst, int max_iter) {
for (int a = 0; a < max_iter && !g_j_join_ok; a++) poll_checked(inst);
return g_j_join_ok;
}
/* ── настройка канала и мемберов ── */
static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) {
topo_node_sqlite_channel_put(inst->topo_sqlite_db, g_sh.ch_id, CH_NAME, g_sh.owner,
g_sh.ch_x_pub, has_priv ? g_sh.ch_x_priv : NULL,
g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig);
chat_core_ensure_channel_ready(inst, g_sh.ch_id);
}
static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) {
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
uint8_t jm[128];
int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id,
inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm));
uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig);
uint8_t root_sig[64];
member_sync_sign_pubkey(g_sh.ch_ed_priv, inst->my_keys.public_key, root_sig);
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
member_sync_put(inst, g_sh.ch_id, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, root_sig);
}
static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) {
/* Ключи fixture известны родителю; плейсхолдер не создаёт общую запись узла сам. */
if (topo_node_sqlite_node_update_verified(inst->topo_sqlite_db, g_sh.nid[idx], name, g_sh.x_pub[idx], g_sh.ed_pub[idx],
(uint64_t)ntp_time_get_seconds(inst), 0) < 0)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "test: cannot initialize fixture node=%016llx", (unsigned long long)g_sh.nid[idx]);
if (topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, g_sh.ch_id, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx]) < 0)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "test: cannot create member placeholder node=%016llx", (unsigned long long)g_sh.nid[idx]);
}
/* C специально откладывает обработку KEY_REGISTER. Ссылки до этого момента быть не должно. */
static etcp_recv_fn connection_join_recv;
static struct ll_entry* delayed_registration;
static struct ETCP_CONN* delayed_conn;
static const char* scenario_dir;
static int early_link;
static void apply_registration(void* arg) {
(void)arg;
char path[512]; snprintf(path, sizeof(path), "%s/link.txt", scenario_dir);
FILE* f = fopen(path, "r");
if (f) { early_link = 1; fclose(f); }
struct ll_entry* e = delayed_registration;
delayed_registration = NULL;
connection_join_recv(delayed_conn, e);
etcp_conn_ref_free(delayed_conn);
delayed_conn = NULL;
}
static void delay_registration(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) {
if (delayed_registration || etcp_conn_ref_take(conn) != 0) abort();
delayed_registration = entry; delayed_conn = conn;
if (!uasync_set_timeout(conn->instance->ua, 2000, NULL, apply_registration, "test_delayed_registration")) abort();
} else connection_join_recv(conn, entry);
}
static int gui_results, gui_result_valid;
static void gui_invite_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (type != CHAT_EVT_INVITE_LINK_READY) return;
gui_results++;
if (len < 11 || len < 1 + data[0] + 10) return;
uint64_t request_id; uint16_t link_len;
memcpy(&request_id, data + 1 + data[0], 8);
memcpy(&link_len, data + 1 + data[0] + 8, 2);
if (len != 1 + data[0] + 10 + link_len || request_id != 102 || !link_len) return;
struct InviteData invite; char error[128];
gui_result_valid = invite_link_decode((const char*)data + 1 + data[0] + 10, link_len, &invite, error, sizeof(error)) == 0
&& chat_join_lookup_inviter(inst, invite.channelId, invite.join_key) == inst->node_id;
}
static int check_gui_superseded(struct UTUN_INSTANCE* inst) {
gui_results = gui_result_valid = 0;
chat_event_set_handler(inst, gui_invite_event);
for (uint64_t id = 101; id <= 102; id++) {
struct chat_invite_build_req* req = u_calloc(1, sizeof(*req));
if (!req) abort();
req->inst = inst; req->target_node_id = inst->node_id; req->request_id = id;
snprintf(req->ch_id, sizeof(req->ch_id), "%s", g_sh.ch_id);
chat_invite_build_link_trampoline(req);
}
int deferred = gui_results == 0;
uint64_t deadline = get_time_tb() + 10000;
while (!gui_results && get_time_tb() < deadline) poll_checked(inst);
chat_event_set_handler(inst, join_event_handler);
return deferred && gui_results == 1 && gui_result_valid && !inst->invite_gui_request;
}
/* Реальный JSON control socket: два параллельных invite, затем отмена при quit. */
static int check_headless_invites(struct UTUN_INSTANCE* inst) {
int port = g_sh.port[IDX_A] + 2100, ok = 0;
socket_t fd = SOCKET_INVALID;
if (chat_headless_control_init(inst->ua, inst, "127.0.0.1", port) != 0) return 0;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == SOCKET_INVALID) goto out;
struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) goto out;
socket_set_nonblocking(fd);
char command[1024], reply[4096] = {0};
int n = snprintf(command, sizeof(command),
"{\"id\":71,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n"
"{\"id\":72,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n",
g_sh.ch_id, (unsigned long long)inst->node_id, g_sh.ch_id, (unsigned long long)inst->node_id);
if (send(fd, command, n, 0) != n) goto out;
uint64_t deadline = get_time_tb() + 10000;
int used = 0, lines = 0;
while (lines < 2 && get_time_tb() < deadline) {
poll_checked(inst);
ssize_t r = recv(fd, reply + used, sizeof(reply) - 1 - used, 0);
if (r > 0) {
for (int i = 0; i < r; i++) if (reply[used + i] == '\n') lines++;
used += (int)r; reply[used] = 0;
}
}
if (lines != 2 || !strstr(reply, "\"id\":71,\"ok\":true") || !strstr(reply, "\"id\":72,\"ok\":true")) goto out;
uint64_t last_key = 0;
char* pos = reply;
for (int i = 0; i < 2; i++) {
pos = strstr(pos, "utun://"); if (!pos) goto out;
char* end = strchr(pos, '"'); if (!end) goto out;
struct InviteData data; char error[128];
if (invite_link_decode(pos, end - pos, &data, error, sizeof(error)) != 0
|| data.join_key == last_key || data.nodeId != inst->node_id
|| chat_join_lookup_inviter(inst, data.channelId, data.join_key) != inst->node_id) goto out;
last_key = data.join_key; pos = end;
}
n = snprintf(command, sizeof(command),
"{\"id\":73,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n"
"{\"id\":74,\"cmd\":\"quit\"}\n"
"{\"id\":75,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n",
g_sh.ch_id, (unsigned long long)inst->node_id, g_sh.ch_id, (unsigned long long)inst->node_id);
if (send(fd, command, n, 0) != n) goto out;
used = 0; reply[0] = 0; deadline = get_time_tb() + 10000;
while (get_time_tb() < deadline) {
poll_checked(inst);
ssize_t r = recv(fd, reply + used, sizeof(reply) - 1 - used, 0);
if (r == 0) break;
if (r > 0) { used += (int)r; reply[used] = 0; }
}
ok = strstr(reply, "\"id\":74") && !strstr(reply, "\"id\":73") && !strstr(reply, "\"id\":75") && !inst->join_registrations;
out:
if (fd != SOCKET_INVALID) socket_close_wrapper(fd);
chat_headless_control_destroy(inst);
chat_event_set_handler(inst, join_event_handler);
if (!ok) fprintf(stderr, "headless: concurrent invite/quit regression failed\n");
return ok;
}
/* ── child_main ── */
/* Проверяем выбор на реальных мемберах канала; изменения производных полей откатываем до следующего poll. */
static int check_invite_priority(struct UTUN_INSTANCE* inst) {
uint64_t channel_id = strtoull(g_sh.ch_id, NULL, 10), selected = 0;
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, channel_id);
struct TOPO_GROUP_NODE* peer = topo_node_find_by_id(group, g_sh.nid[IDX_C]);
if (!peer) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: missing peer"); return 0; }
uint64_t deadline = get_time_tb() + 10000;
while (!peer->conn_presence && get_time_tb() < deadline) poll_checked(inst);
if (!peer->conn_presence) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: peer unreachable"); return 0; }
char table[80], sql[256];
peers_table_name(g_sh.ch_id, table, sizeof(table));
sqlite3* db = inst->topo_sqlite_db;
if (sqlite3_exec(db, "SAVEPOINT invite_priority", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: savepoint failed: %s", sqlite3_errmsg(db)); return 0;
}
int ok = 0;
uint8_t presence = peer->conn_presence;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=1", table);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != g_sh.nid[IDX_C]) goto out;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=4 WHERE node_id=%lld", table, (long long)inst->node_id);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != inst->node_id) goto out;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=1 WHERE node_id=%lld", table, (long long)inst->node_id);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=4 WHERE node_id=%lld", table, (long long)g_sh.nid[IDX_C]);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != g_sh.nid[IDX_C]) goto out;
peer->conn_presence = 0;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != inst->node_id) goto out;
ok = 1;
out:
peer->conn_presence = presence;
if (sqlite3_exec(db, "ROLLBACK TO invite_priority; RELEASE invite_priority", NULL, NULL, NULL) != SQLITE_OK) ok = 0;
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority failed: selected=%016llx err=%s",
(unsigned long long)selected, sqlite3_errmsg(db));
else DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: regular, self-supernode, supernode, unreachable PASS");
return ok;
}
static int child_main(const char* role, const char* dir, int invalid_key) {
struct UASYNC* ua = uasync_create();
if (!ua) { fprintf(stderr, "%s: uasync_create failed\n", role); return 1; }
char cfg[512]; snprintf(cfg, sizeof(cfg), "%s/%s.conf", dir,
strcmp(role, "a") == 0 ? "a" : strcmp(role, "c") == 0 ? "c" : "j");
struct UTUN_INSTANCE* inst = utun_instance_create(ua, cfg);
if (!inst) { fprintf(stderr, "%s: create failed\n", role); uasync_destroy(ua, 0); return 1; }
if (utun_core_start(inst) < 0 || chat_service_start(inst) < 0) {
utun_instance_destroy(inst); uasync_destroy(ua, 0); return 1;
}
chat_event_set_handler(inst, join_event_handler);
if (strcmp(role, "c") == 0) {
scenario_dir = dir;
connection_join_recv = inst->router_bindings.callbacks[ETCP_RT_ID_JOIN];
etcp_router_bind(inst, ETCP_RT_ID_JOIN, delay_registration);
}
/* Fixture bootstrap between the existing members uses core NCD, without UTUN. */
struct NODE_CONN_DIRECT* bootstrap = NULL;
if (!strcmp(role, "c") && !g_sh.degraded) {
struct TOPO_ADDR4 address = { .addr = {127, 0, 0, 1}, .port = g_sh.port[IDX_A], .protocol = TOPO_PROTO_UDP };
struct TOPO_NODE node = { .node_id = g_sh.nid[IDX_A], .v4_addrs = &address };
memcpy(node.public_key, g_sh.x_pub[IDX_A], 32);
if (node_conn_direct_open_node(inst, node.node_id, NULL, NULL, &bootstrap, &node, NULL) == NCD_ERR) return 1;
}
int rc = 1;
uint64_t ch_num = strtoull(g_sh.ch_id, NULL, 10);
sqlite3* db = inst->topo_sqlite_db;
if (strcmp(role, "a") == 0) {
if (!g_sh.degraded && !wait_conn(inst, g_sh.nid[IDX_C], 4000)) {
fprintf(stderr, "A: no conn to C\n"); goto out;
}
channel_put_shared(inst, 1);
member_put_self(inst, "A");
if (!g_sh.degraded) {
member_put_placeholder(inst, IDX_C, "C");
/* канал добавлен уже после коннекта — явно запускаем merkle-сессию A↔C */
member_sync_start(inst, g_sh.nid[IDX_C], g_sh.ch_id, NULL, NULL);
}
if (!g_sh.degraded && !wait_bgp(inst, g_sh.nid[IDX_C], 8000)) {
fprintf(stderr, "A: no BGP to C\n"); goto out;
}
if (!g_sh.degraded && !check_invite_priority(inst)) goto out;
uint64_t target = g_sh.degraded ? g_sh.nid[IDX_A] : g_sh.nid[IDX_C];
struct invite_result result = {0};
struct chat_invite_build* pending = NULL;
/* conn_presence может отставать от paths; повторяем только ошибку запуска. */
for (int a = 0; a < 1000 && !pending; a++) {
pending = chat_invite_build_link(inst, ch_num, target, NULL, invite_ready, &result);
if (!pending) poll_checked(inst);
}
uint64_t deadline = get_time_tb() + JOIN_REGISTER_TIMEOUT_TB + 10000;
while (pending && !result.calls && get_time_tb() < deadline) poll_checked(inst);
if (!result.calls && pending) chat_invite_build_cancel(pending);
if (result.calls != 1 || result.result != CHAT_JOIN_OK || !result.link[0]) {
fprintf(stderr, "A: invite confirmation failed calls=%d result=%d\n", result.calls, result.result); goto out;
}
char lf[512];
if (!check_gui_superseded(inst)) { fprintf(stderr, "GUI: stale invite result\n"); goto out; }
if (!check_headless_invites(inst)) goto out;
snprintf(lf, sizeof(lf), "%s/link.txt", dir);
wf(lf, "%s", result.link);
if (invalid_key) {
/* J с неверным ключом — мембером стать не должен */
if (!wait_not_member(inst, g_sh.nid[IDX_J], 1500)) {
fprintf(stderr, "A: J unexpectedly became member\n"); goto out;
}
} else {
if (!wait_signed(inst, g_sh.nid[IDX_J], g_sh.nid[IDX_A], 8000)) {
fprintf(stderr, "A: J not signed by me\n"); goto out;
}
uint64_t sb = 0; uint8_t sig[64]; memset(sig, 0, sizeof(sig));
topo_node_sqlite_member_get_sign(db, g_sh.ch_id, g_sh.nid[IDX_J], &sb, sig);
struct ms_member_rec rec; memset(&rec, 0, sizeof(rec));
rec.x25519 = g_sh.x_pub[IDX_J]; rec.signature = sig; rec.signed_by = sb;
if (member_sync_verify_signature(db, g_sh.ch_id, &rec) != 1) {
fprintf(stderr, "A: J signature invalid\n"); goto out;
}
}
rc = 0;
} else if (strcmp(role, "c") == 0) {
if (!wait_conn(inst, g_sh.nid[IDX_A], 4000)) {
fprintf(stderr, "C: no conn to A\n"); goto out;
}
channel_put_shared(inst, 0);
member_put_placeholder(inst, IDX_C, "C");
member_put_placeholder(inst, IDX_A, "A");
/* явно запускаем merkle-сессию C↔A */
member_sync_start(inst, g_sh.nid[IDX_A], g_sh.ch_id, NULL, NULL);
if (!wait_bgp(inst, g_sh.nid[IDX_A], 8000)) {
fprintf(stderr, "C: no BGP to A\n"); goto out;
}
if (invalid_key) {
if (!wait_not_member(inst, g_sh.nid[IDX_J], 1500)) {
fprintf(stderr, "C: J unexpectedly became member\n"); goto out;
}
} else {
if (!wait_signed(inst, g_sh.nid[IDX_J], g_sh.nid[IDX_A], 8000)) {
fprintf(stderr, "C: J not signed by A\n"); goto out;
}
uint64_t sb = 0; uint8_t sig[64]; memset(sig, 0, sizeof(sig));
topo_node_sqlite_member_get_sign(db, g_sh.ch_id, g_sh.nid[IDX_J], &sb, sig);
struct ms_member_rec rec; memset(&rec, 0, sizeof(rec));
rec.x25519 = g_sh.x_pub[IDX_J]; rec.signature = sig; rec.signed_by = sb;
if (member_sync_verify_signature(db, g_sh.ch_id, &rec) != 1) {
fprintf(stderr, "C: J signature invalid\n"); goto out;
}
}
rc = 0;
} else {
char lf[512]; snprintf(lf, sizeof(lf), "%s/link.txt", dir);
if (!wait_file(inst, lf, 8000)) { fprintf(stderr, "J: no link file\n"); goto out; }
char link[1024];
{ FILE* f = fopen(lf, "r"); if (!f) { fprintf(stderr, "J: link open failed\n"); goto out; }
size_t n = fread(link, 1, sizeof(link) - 1, f); fclose(f); link[n] = '\0'; }
struct InviteData d; char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
fprintf(stderr, "J: decode failed: %s\n", err); goto out;
}
uint8_t addrs_buf[2048];
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
if (addrs_len <= 0) { fprintf(stderr, "J: serialize addrs failed\n"); goto out; }
uint64_t key = invalid_key ? (d.join_key ^ 0xDEADBEEFCAFEBABEULL) : d.join_key;
chat_sync_connect_from_invite(inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, key);
if (invalid_key) {
rc = wait_not_member(inst, g_sh.nid[IDX_J], 200) ? 0 : 1;
if (rc) fprintf(stderr, "J: unexpectedly became member with wrong key\n");
} else {
/* критерий J по ТЗ: получил JOIN_READY (CHAT_EVT_CONNECT_RESULT result=0) */
if (!wait_join_result(inst, 8000)) {
fprintf(stderr, "J: no JOIN_READY\n"); goto out;
}
uint64_t connection_node = g_sh.nid[g_sh.degraded ? IDX_A : IDX_C];
if (!wait_bgp(inst, connection_node, 8000)) {
fprintf(stderr, "J: JOIN_READY received but BGP did not recover\n"); goto out;
}
char ready[512]; snprintf(ready, sizeof(ready), "%s/join_ready", dir);
wf(ready, "ready");
rc = 0;
}
}
/* Локальная запись не означает доставку: участники обслуживают обмен до JOIN_READY у J. */
if (rc == 0 && !invalid_key && strcmp(role, "j") != 0) {
char ready[512]; snprintf(ready, sizeof(ready), "%s/join_ready", dir);
if (!wait_file(inst, ready, 8000)) { fprintf(stderr, "%s: join delivery not confirmed\n", role); rc = 1; }
}
out:
node_conn_direct_close(bootstrap);
if (early_link || delayed_registration || early_bgp_membership) rc = 1;
fprintf(stderr, "%s: %s\n", role, rc == 0 ? "OK" : "FAIL");
fflush(stdout); fflush(stderr);
_exit(rc);
}
/* ── parent ── */
static int run_scenario(const char* name, int degraded, int invalid_key) {
char dir[512]; snprintf(dir, sizeof(dir), "/tmp/utun_e2e_%s_XXXXXX", name);
(void)!mkdtemp(dir);
fill_shared(56000 + (getpid() % 2000), degraded);
write_configs(dir);
const char* roles[4];
if (degraded) { roles[0] = "a"; roles[1] = "j"; roles[2] = NULL; roles[3] = NULL; }
else { roles[0] = "a"; roles[1] = "c"; roles[2] = "j"; roles[3] = NULL; }
pid_t pids[3]; int n = 0;
for (int i = 0; roles[i]; i++) {
pid_t p = fork();
if (p == 0) {
int r = child_main(roles[i], dir, invalid_key);
_exit(r);
}
if (p > 0) pids[n++] = p;
}
int ok = 1;
for (int i = 0; i < n; i++) {
int st = 0; waitpid(pids[i], &st, 0);
if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) ok = 0;
}
char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd);
return ok;
}
static int parent_main(void) {
printf("=== test_chat_join_e2e ===\n"); fflush(stdout);
TEST("happy path A!=C: J joins via C (signed_by=A)"); {
if (run_scenario("happy", 0, 0)) OK(); else FAIL();
}
TEST("degraded A==C: J joins directly via A"); {
if (run_scenario("deg", 1, 0)) OK(); else FAIL();
}
TEST("invalid key: J rejected (no member)"); {
if (run_scenario("inv", 0, 1)) OK(); else FAIL();
}
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED);
return G_FAILED > 0 ? 1 : 0;
}
int main(int argc, char** argv) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level_by_name("chat", "debug");
debug_set_category_level_by_name("chat_sync", "info");
debug_set_category_level_by_name("member_sync", "trace");
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));
if (argc >= 3) {
int invalid = (argc >= 4 && strcmp(argv[3], "invalid") == 0);
return child_main(argv[1], argv[2], invalid);
}
return parent_main();
}
#endif /* !_WIN32 */