712 lines
32 KiB
C
712 lines
32 KiB
C
// 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) {
|
||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||
member_sync_put(inst, g_sh.ch_id, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx],
|
||
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||
}
|
||
|
||
/* 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_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 */
|