Files
utun2/tests/test_node_conn_direct.c
T
evgeny 38c9f229bc stcp: ed25519 pubkey exchange in handshake, fix instance destroy segfault, fix chat_tcp_test
- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll
- stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close
- utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP),
  add deferred drain in both Phase J and L
- stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39,
  exchange ed25519_pubkey in both handshake directions
- etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in
  etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb
- chat_sync.c: fix lk->conn NULL dereference for TCP links
- test_stcp.c: update stcp_server_create/stcp_client_connect callsites
- tools/chat_tcp_test: change transport udp→tcp
2026-08-07 00:48:27 +03:00

341 lines
16 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.
/**
* @file test_node_conn_direct.c
* @brief Тест node_conn_direct с 2 инстансами.
*
* Фазы:
* 1 — два open ДО eventloop → NCD_NEW + NCD_REUSED(pending)
* 2 — poll → init_cb → NCD_EVENT_UP на обоих
* 3 — open на ready conn → NCD_REUSED + async cb
* 4 — ещё open → NCD_REUSED
* 5 — close h3,h2 → conn жив (h0,h1 остаются)
* 6a — bidirectional: inject A→B, открыть B→A
* 6b — poll: A и B UP. A закрывает новый handle → CLOSE к B
* 6c — B имеет handle → KEEP_ALIVE. A переоткрывает → NCD_REUSED
* 6d — poll: A get_conn OK после KEEP_ALIVE. Cleanup B-side.
* 6e — open во время fin_wait: A закрывает h1, сразу же переоткрывает → NCD_REUSED
* 6f — poll: UP на переоткрытом handle. Cleanup.
* 6g — CLOSE от peer когда нет handle'ов: A закрывает → CLOSE к B, B без handles → conn dead → entry удалён → NCD_NEW при переоткрытии
* 6 — close h1,h0 (last) → conn закрыт (fin_wait → CLOSE → reinit)
* 7 — NCD_ERR unknown node
* 8 — inject unreachable → NCD_NEW → TIMEOUT
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "../lib/platform_compat.h"
#include "test_utils.h"
#ifndef _WIN32
#include <unistd.h>
#endif
#include "etcp.h"
#include "etcp_connections.h"
#include "node_conn_direct.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "topo_group.h"
#include "topo_node.h"
#include "secure_channel.h"
#include "../lib/u_async.h"
#include "../lib/ll_queue.h"
#include "../lib/memory_pool.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define TIMEOUT_TB 600000
#define SHORT_TO_TB 50000
#define POLL_MS 5
static struct UTUN_INSTANCE *g_a = NULL, *g_b = NULL;
static struct UASYNC *ua = NULL;
static volatile int g_result = 0;
static int g_phase = 0;
static void *g_ttimer = NULL;
static char tdir[] = "/tmp/utun_ncd_XXXXXX";
static char ca[256], cb[256];
static int pa = 0, pb = 0;
static uint64_t nid_a = 0, nid_b = 0;
static uint8_t pubkey_a[SC_PUBKEY_SIZE], pubkey_b[SC_PUBKEY_SIZE];
static struct NODE_CONN_DIRECT *gh[8];
static volatile int g_up[8], g_down[8], g_tout[8];
static struct NODE_CONN_DIRECT *gh_b[2];
static volatile int g_up_b[2], g_down_b[2];
static void ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
int idx = (int)(intptr_t)arg;
if (event == NCD_EVENT_UP) g_up[idx]++;
else if (event == NCD_EVENT_DOWN) g_down[idx]++;
else if (event == NCD_EVENT_TIMEOUT) g_tout[idx]++;
fprintf(stderr, " cb[%d]: event=%d (up=%d down=%d tout=%d)\n", idx, (int)event, g_up[idx], g_down[idx], g_tout[idx]); fflush(stderr);
}
static void ncd_cb_b(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
int idx = (int)(intptr_t)arg;
if (event == NCD_EVENT_UP) g_up_b[idx]++;
else if (event == NCD_EVENT_DOWN) g_down_b[idx]++;
fprintf(stderr, " cb_b[%d]: event=%d (up=%d down=%d)\n", idx, (int)event, g_up_b[idx], g_down_b[idx]); fflush(stderr);
}
static void to_cb(void* arg) { (void)arg; fprintf(stderr, "GLOBAL TIMEOUT phase=%d\n", g_phase); g_result = 2; }
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 fail(const char* msg) { fprintf(stderr, "FAIL[%d]: %s\n", g_phase, msg); fflush(stderr); g_result = 2; }
static int inject_node(struct UTUN_INSTANCE* inst, uint64_t nid, const uint8_t pk[SC_PUBKEY_SIZE], uint16_t port);
static void t1(void* arg); static void t2(void* arg); static void t3(void* arg);
static void t4(void* arg); static void t5(void* arg);
static void t6a(void* arg); static void t6b(void* arg); static void t6c(void* arg);
static void t6d(void* arg); static void t6e(void* arg); static void t6f(void* arg);
static void t6g(void* arg);
static void t6(void* arg); static void t7(void* arg);
static void t8(void* arg); static void t9(void* arg);
static void t_done(void* arg);
static void t1(void* arg) {
(void)arg; g_phase = 1;
fprintf(stderr, "\n=== P1: two opens before poll ===\n"); fflush(stderr);
g_a->etcp_connect_timeout_tb = TIMEOUT_TB;
int r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)0, &gh[0], NULL);
fprintf(stderr, " open#0 → %s\n", r == NCD_NEW ? "NCD_NEW" : r == NCD_REUSED ? "NCD_REUSED" : "ERR");
if (r != NCD_NEW) { fail("expected NCD_NEW"); return; }
r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)1, &gh[1], NULL);
fprintf(stderr, " open#1 → %s\n", r == NCD_NEW ? "NCD_NEW" : r == NCD_REUSED ? "NCD_REUSED" : "ERR");
if (r != NCD_REUSED) { fail("expected NCD_REUSED"); return; }
uasync_call_soon(ua, NULL, t2);
}
static void t2(void* arg) {
(void)arg; g_phase = 2;
if (g_up[0] > 0 && g_up[1] > 0) {
if (node_conn_direct_get_conn(gh[0]) == NULL) { fail("get_conn h0 NULL"); return; }
if (node_conn_direct_get_conn(gh[1]) == NULL) { fail("get_conn h1 NULL"); return; }
fprintf(stderr, "\n=== P2: both up, get_conn OK ===\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t3); return;
}
uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t2, "t2");
}
static void t3(void* arg) {
(void)arg; g_phase = 3;
fprintf(stderr, "\n=== P3: open on ready conn ===\n"); fflush(stderr);
int r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)2, &gh[2], NULL);
fprintf(stderr, " open#2 → %s\n", r == NCD_REUSED ? "NCD_REUSED" : "?");
if (r != NCD_REUSED) { fail("expected NCD_REUSED"); return; }
uasync_call_soon(ua, NULL, t4);
}
static void t4(void* arg) {
(void)arg; g_phase = 4;
if (g_up[2] == 0) { uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t4, "t4"); return; }
fprintf(stderr, "\n=== P4: async cb OK, open h3 ===\n"); fflush(stderr);
int r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)3, &gh[3], NULL);
fprintf(stderr, " open#3 → %s\n", r == NCD_REUSED ? "NCD_REUSED" : "?");
if (r != NCD_REUSED) { fail("expected NCD_REUSED"); return; }
uasync_call_soon(ua, NULL, t5);
}
static void t5(void* arg) {
(void)arg; g_phase = 5;
if (g_up[3] == 0) { uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t5, "t5"); return; }
fprintf(stderr, "\n=== P5: close h3, h2 → conn stays ===\n"); fflush(stderr);
node_conn_direct_close(gh[3]); gh[3] = NULL;
if (node_conn_direct_get_conn(gh[2]) == NULL) { fail("conn died after h3 close"); return; }
node_conn_direct_close(gh[2]); gh[2] = NULL;
if (node_conn_direct_get_conn(gh[1]) == NULL) { fail("conn died after h2 close"); return; }
fprintf(stderr, " OK\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t6a);
}
/* ═══════════ Scenario 1: KEEP_ALIVE handshake (conn ещё жив после P1-P5) ═══════════ */
static void t6a(void* arg) {
(void)arg; g_phase = 16;
fprintf(stderr, "\n=== P6a: bidirectional — B opens handle to A ===\n"); fflush(stderr);
int r = node_conn_direct_open(g_b, nid_a, ncd_cb_b, (void*)0, &gh_b[0], NULL);
if (r != NCD_REUSED) { fail("P6a: B open expected NCD_REUSED (incoming conn exists)"); return; }
fprintf(stderr, " B open → NCD_REUSED\n");
r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)4, &gh[4], NULL);
if (r != NCD_REUSED) { fail("P6a: A open(4) expected NCD_REUSED (conn alive)"); return; }
fprintf(stderr, " A open(4) → NCD_REUSED\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t6b);
}
static void t6b(void* arg) {
(void)arg; g_phase = 17;
if (g_up[4] > 0 && g_up_b[0] > 0) {
fprintf(stderr, "\n=== P6b: both UP, A closes handle 4 → CLOSE to B ===\n"); fflush(stderr);
node_conn_direct_close(gh[4]); gh[4] = NULL;
uasync_set_timeout(ua, 200, NULL, (timeout_callback_t)t6c, "t6b_d");
return;
}
uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t6b, "t6b");
}
static void t6c(void* arg) {
(void)arg; g_phase = 18;
fprintf(stderr, "\n=== P6c: KEEP_ALIVE round-trip, A re-opens ===\n"); fflush(stderr);
int r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)5, &gh[5], NULL);
if (r != NCD_REUSED) { fail("P6c: expected NCD_REUSED (KEEP_ALIVE saved conn)"); return; }
fprintf(stderr, " A re-open(5) → NCD_REUSED\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t6d);
}
static void t6d(void* arg) {
(void)arg; g_phase = 19;
if (g_up[5] == 0) { uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t6d, "t6d"); return; }
if (node_conn_direct_get_conn(gh[5]) == NULL) { fail("P6d: get_conn NULL after KEEP_ALIVE"); return; }
fprintf(stderr, "\n=== P6d: KEEP_ALIVE works, get_conn OK ===\n"); fflush(stderr);
node_conn_direct_close(gh[5]); gh[5] = NULL;
node_conn_direct_close(gh_b[0]); gh_b[0] = NULL;
uasync_set_timeout(ua, 300, NULL, (timeout_callback_t)t6e, "t6d_d");
}
/* ═══════════ Scenario 2: Open during fin_wait ═══════════ */
static void t6e(void* arg) {
(void)arg; g_phase = 20;
fprintf(stderr, "\n=== P6e: close h1 → fin_wait, immediately re-open ===\n"); fflush(stderr);
node_conn_direct_close(gh[1]); gh[1] = NULL;
int r = node_conn_direct_open(g_a, nid_b, ncd_cb, (void*)4, &gh[4], NULL);
if (r != NCD_REUSED) { fail("P6e: expected NCD_REUSED (entry found, fin_wait cleared)"); return; }
fprintf(stderr, " re-open → NCD_REUSED\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t6f);
}
static void t6f(void* arg) {
(void)arg; g_phase = 21;
if (g_up[4] == 0) { uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t6f, "t6f"); return; }
fprintf(stderr, "\n=== P6f: re-opened handle got UP, Scenario 2 OK ===\n"); fflush(stderr);
node_conn_direct_close(gh[4]); gh[4] = NULL;
uasync_set_timeout(ua, 300, NULL, (timeout_callback_t)t6, "t6f_d");
}
static void t6(void* arg) {
(void)arg; g_phase = 6;
fprintf(stderr, "\n=== P6: close last → conn closed ===\n"); fflush(stderr);
node_conn_direct_close(gh[1]); gh[1] = NULL;
if (node_conn_direct_get_conn(gh[0]) == NULL) { fail("conn died before last handle"); return; }
node_conn_direct_close(gh[0]); gh[0] = NULL;
fprintf(stderr, " OK\n"); fflush(stderr);
uasync_set_timeout(ua, 3000, NULL, (timeout_callback_t)t7, "t6d");
}
static void t7(void* arg) {
(void)arg; g_phase = 7;
fprintf(stderr, "\n=== P7: NCD_ERR unknown node ===\n"); fflush(stderr);
struct NODE_CONN_DIRECT* hx = NULL;
int r = node_conn_direct_open(g_a, 0xDEADBEEF00000001ULL, ncd_cb, (void*)99, &hx, NULL);
fprintf(stderr, " open(unknown) → %s\n", r == NCD_ERR ? "NCD_ERR" : "?");
if (r != NCD_ERR) { fail("expected NCD_ERR"); return; }
if (hx != NULL) { fail("handle not NULL"); return; }
fprintf(stderr, " OK\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t8);
}
static void t8(void* arg) {
(void)arg; g_phase = 8;
fprintf(stderr, "\n=== P8: unreachable node → timeout ===\n"); fflush(stderr);
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) { fail("alloc ni"); return; }
uint64_t fake_id = 0xF000000000000001ULL;
ni->group_ref_count = 0; ni->node_id = fake_id; ni->ver = 0;
memcpy(ni->public_key, pubkey_b, SC_PUBKEY_SIZE);
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(g_a->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(g_a->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0]=127; addr->addr[1]=0; addr->addr[2]=0; addr->addr[3]=1; addr->port=1; addr->type=TOPO_ADDR_INTERFACE; addr->socket_id=0; addr->protocol=TOPO_PROTO_UDP; addr->next=ni->v4_addrs; ni->v4_addrs=addr; }
if (!topo_node_registry_store(g_a->topo_groups, ni)) { fail("registry acquire"); return; }
memset((void*)g_up, 0, sizeof(g_up));
memset((void*)g_tout, 0, sizeof(g_tout));
g_a->etcp_connect_timeout_tb = SHORT_TO_TB;
int r = node_conn_direct_open(g_a, fake_id, ncd_cb, (void*)0, &gh[0], NULL);
fprintf(stderr, " open(unreachable) → %s\n", r == NCD_NEW ? "NCD_NEW" : "?");
if (r != NCD_NEW) { fail("expected NCD_NEW"); return; }
uasync_call_soon(ua, NULL, t9);
}
static void t9(void* arg) {
(void)arg; g_phase = 9;
if (g_tout[0] == 0) { uasync_set_timeout(ua, POLL_MS, NULL, (timeout_callback_t)t9, "t9"); return; }
if (g_up[0] > 0) { fail("up fired, expected only timeout"); return; }
fprintf(stderr, "\n=== P9: timeout_cb fired, no up ===\n"); fflush(stderr);
node_conn_direct_close(gh[0]); gh[0] = NULL;
uasync_call_soon(ua, NULL, t_done);
}
static void t_done(void* arg) {
(void)arg; g_phase = 99;
fprintf(stderr, "\n=== ALL PASSED ===\n"); fflush(stderr);
g_result = 1;
}
static int inject_node(struct UTUN_INSTANCE* inst, uint64_t nid, const uint8_t pk[SC_PUBKEY_SIZE], uint16_t port) {
if (!inst->topo_groups) return -1;
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) return -1;
ni->group_ref_count = 0; ni->node_id = nid; ni->ver = 0;
memcpy(ni->public_key, pk, SC_PUBKEY_SIZE);
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(inst->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(inst->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0]=127; addr->addr[1]=0; addr->addr[2]=0; addr->addr[3]=1; addr->port=port; addr->type=TOPO_ADDR_INTERFACE; addr->socket_id=0; addr->protocol=TOPO_PROTO_UDP; addr->next=ni->v4_addrs; ni->v4_addrs=addr; }
if (!topo_node_registry_store(inst->topo_groups, ni)) { u_free(ni); return -1; }
return 0;
}
static int setup(void) {
test_mkdtemp(tdir);
int base = 49000 + (getpid() % 10000); pa = base; pb = base + 1;
snprintf(ca, sizeof(ca), "%s/a.conf", tdir); snprintf(cb, sizeof(cb), "%s/b.conf", tdir);
wf(ca, "[global]\ntun_ip=10.97.0.1/24\ntun_ifname=tun95\nkeepalive_timeout=100\nkeepalive_interval=10\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", pa);
wf(cb, "[global]\ntun_ip=10.97.0.2/24\ntun_ifname=tun94\nkeepalive_timeout=100\nkeepalive_interval=10\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", pb);
config_ensure_keys_and_node_id(ca); config_ensure_keys_and_node_id(cb);
return 0;
}
static void cleanup(void) { test_unlink(ca); test_unlink(cb); test_rmdir(tdir); }
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_TRACE);
utun_instance_set_tun_init_enabled(0);
if (setup() != 0) return 1;
ua = uasync_create();
g_a = utun_instance_create(ua, ca); g_b = utun_instance_create(ua, cb);
if (!g_a || !g_b) { g_result = 2; goto done; }
nid_b = g_b->node_id; memcpy(pubkey_b, g_b->my_keys.public_key, SC_PUBKEY_SIZE);
nid_a = g_a->node_id; memcpy(pubkey_a, g_a->my_keys.public_key, SC_PUBKEY_SIZE);
fprintf(stderr, "A: 0x%016llx port=%d B: 0x%016llx port=%d\n",
(unsigned long long)nid_a, pa, (unsigned long long)nid_b, pb); fflush(stderr);
utun_instance_init(g_a); utun_instance_init(g_b);
if (inject_node(g_a, nid_b, pubkey_b, pb) != 0) { fail("inject_node"); goto done; }
if (inject_node(g_b, nid_a, pubkey_a, pa) != 0) { fail("inject_node A→B"); goto done; }
fprintf(stderr, "injected B→A and A→B registries\n"); fflush(stderr);
uasync_call_soon(ua, NULL, t1);
g_ttimer = uasync_set_timeout(ua, TIMEOUT_TB, NULL, to_cb, "to");
{ uint64_t st = get_time_tb(); while (!g_result && (int)(get_time_tb() - st) < TIMEOUT_TB + 50000) uasync_poll(ua, POLL_MS); }
if (g_result == 0) g_result = 2;
done:
if (g_ttimer && ua) uasync_cancel_timeout(ua, g_ttimer);
for (int i = 0; i < 8; i++) if (gh[i]) node_conn_direct_close(gh[i]);
for (int i = 0; i < 2; i++) if (gh_b[i]) node_conn_direct_close(gh_b[i]);
if (g_a) { g_a->running = 0; utun_instance_destroy(g_a); }
if (g_b) { g_b->running = 0; utun_instance_destroy(g_b); }
if (ua) uasync_destroy(ua, 0);
cleanup();
fprintf(stderr, "\n%s\n", g_result == 1 ? "=== TEST PASSED ===" : "=== TEST FAILED ==="); fflush(stderr);
return (g_result == 1) ? 0 : 1;
}