You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

658 lines
28 KiB

#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>
#include <net/if.h>
#endif
#include "etcp.h"
#include "etcp_connections.h"
#include "etcp_api.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 "../src/transport_layer/node_conn_direct.h"
#include "../lib/u_async.h"
#include "../lib/ll_queue.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define TIMEOUT_TB 300000
#define POLL_MS 5
#define STEP_TB 3000 /* 300ms между фазами */
#define TRAF_SEND_TB 50 /* 5ms — отправка */
#define TRAF_MON_TB 1000 /* 100ms — мониторинг */
#define ETCP_RT_ID_TEST 0xF0
typedef void (*timeout_cb)(void*);
static struct test_ctx {
struct UTUN_INSTANCE *server, *client;
struct UASYNC* ua;
int round; /* 0..7 */
int step;
int ip_changes_on_last;
char cur_iface[IFNAMSIZ];
char prev_iface[IFNAMSIZ];
int connected; /* etcp_connect callback fired */
uint64_t srv_node_id;
uint8_t srv_pubkey[SC_PUBKEY_SIZE];
int result; /* 0=running, 1=fail, 2=pass */
uint32_t recv_at_ip_change; /* total_recv на момент смены IP */
/* traffic */
uint32_t send_seq, send_count, pong_count, total_recv;
uint32_t expected_rx_seq; /* следующий ожидаемый seq в ответе */
uint32_t rx_seq_gaps; /* счётчик пропусков в seq */
uint32_t rx_seq_dups; /* счётчик дубликатов */
/* connection health */
uint32_t conn_reinit_snapshot; /* reinit_count на начало текущей фазы */
uint32_t max_allowed_reinits; /* максимально допустимых reinits за тест */
/* full-disconnect phase */
int all_deleted;
uint32_t pre_delete_total_recv;
} ctx;
static void* timeout_handle;
static char tdir[] = "/tmp/utun_as_XXXXXX";
static char scf[256], ccf[256];
/* ── rounds ── */
static const struct {
const char* ifname, *ip1, *ip2;
} rounds[] = {
{"dummy_cli1", "10.90.0.2/16", "10.90.1.2/16"},
{"dummy_cli2", "10.90.0.3/16", "10.90.1.3/16"},
{"dummy_cli3", "10.90.0.4/16", "10.90.1.4/16"},
{"dummy_cli4", "10.90.0.5/16", "10.90.1.5/16"},
{"dummy_cli5", "10.90.0.6/16", "10.90.1.6/16"},
{"dummy_cli6", "10.90.0.7/16", "10.90.1.7/16"},
{"dummy_cli7", "10.90.0.8/16", "10.90.1.8/16"},
{"dummy_cli8", "10.90.0.9/16", "10.90.1.9/16"},
};
#define N_ROUNDS (int)(sizeof(rounds)/sizeof(rounds[0]))
/* ── helpers ── */
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 fail(const char* msg) {
fprintf(stderr, "FAIL r=%d s=%d: %s\n", ctx.round, ctx.step, msg); fflush(stderr);
ctx.result = 1;
}
static void to_cb(void* arg) { (void)arg; fprintf(stderr, "TIMEOUT\n"); ctx.result = 1; }
static int link_on_iface(const struct ETCP_LINK* l, const char* ifname) {
if (!l->conn || !l->conn->name || !ifname || !ifname[0]) return 0;
size_t ifl = strlen(ifname);
return strncmp(l->conn->name + 3, ifname, ifl) == 0 && l->conn->name[3 + ifl] == '_';
}
static int count_links_to_srv(const char* ifname) {
int n = 0; struct ll_entry* e = ctx.client->connections->head;
while (e) { struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (ce->conn->peer_node_id == ctx.srv_node_id) {
struct ETCP_LINK* l = ce->conn->links;
while (l) { if (l->initialized && l->link_status && link_on_iface(l, ifname)) n++; l = l->next; }
} e = e->next; }
return n;
}
/* ── assertions helpers ── */
static int count_all_client_udp(void) { int n = 0; struct ETCP_SOCKET* s = ctx.client->etcp_sockets; while (s) { if (!s->is_tcp) n++; s = s->next; } return n; }
static int count_all_client_tcp(void) { int n = 0; struct ETCP_SOCKET* s = ctx.client->etcp_sockets; while (s) { if (s->is_tcp) n++; s = s->next; } return n; }
static int iface_still_exists(const char* ifname) { return if_nametoindex(ifname) != 0; }
static int count_sockets_on_iface(const char* ifname) {
int n = 0; uint32_t idx = if_nametoindex(ifname); if (!idx) return 0;
for (struct ETCP_SOCKET* s = ctx.client->etcp_sockets; s; s = s->next) if (s->netif_index == idx) n++;
return n;
}
static int count_live_links_on_conn(struct ETCP_CONN* conn) {
int n = 0; struct ETCP_LINK* l = conn->links; while (l) { if (l->initialized && l->link_status) n++; l = l->next; } return n;
}
static int has_stale_links(struct ETCP_CONN* conn) {
struct ETCP_LINK* l = conn->links;
while (l) {
if (!l->initialized || !l->link_status) { l = l->next; continue; }
char b[IFNAMSIZ];
if (!l->conn || l->conn->netif_index == 0) { l = l->next; continue; }
if (!if_indextoname(l->conn->netif_index, b)) return 1; // netif не существует
if (!iface_still_exists(b)) return 1;
l = l->next;
}
return 0;
}
static struct ETCP_CONN* find_srv_conn(void) {
struct ll_entry* e = ctx.client->connections->head;
while (e) { struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (ce->conn->peer_node_id == ctx.srv_node_id && ce->conn->state != 2) return ce->conn; e = e->next; }
return NULL;
}
static void check_conn_health(struct ETCP_CONN* conn) {
if (!conn) return;
if (conn->state == 2) fail("conn state is deleted (2)");
if (conn->state != 1) { fprintf(stderr, "[warn] conn state=%d\n", conn->state); }
if (conn->reinit_count - ctx.conn_reinit_snapshot > ctx.max_allowed_reinits) fail("too many reinits");
}
/* ── server node (2 addrs: UDP + TCP) ── */
static struct TOPO_GROUP_NODE* mk_srv_node(void) {
struct TOPO_GROUP_NODE* nq = u_calloc(1, sizeof(struct TOPO_GROUP_NODE));
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!nq || !ni) { u_free(nq); u_free(ni); return NULL; }
ni->group_ref_count = 1; ni->node_id = ctx.srv_node_id;
memcpy(ni->public_key, ctx.srv_pubkey, SC_PUBKEY_SIZE);
nq->ll.size = sizeof(struct TOPO_GROUP_NODE) - sizeof(struct ll_entry);
nq->node_id = ctx.srv_node_id;
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(ctx.client->topo_groups->v4_sock_meta_pool);
if (!sm) { u_free(nq); u_free(ni); return NULL; }
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* a_udp = memory_pool_alloc(ctx.client->topo_groups->v4_addr_pool);
struct TOPO_ADDR4* a_tcp = memory_pool_alloc(ctx.client->topo_groups->v4_addr_pool);
if (!a_udp || !a_tcp) { u_free(nq); u_free(ni); return NULL; }
a_udp->addr[0] = 10; a_udp->addr[1] = 90; a_udp->addr[2] = 0; a_udp->addr[3] = 1;
a_udp->port = 9001; a_udp->protocol = TOPO_PROTO_UDP;
a_udp->type = TOPO_ADDR_INTERFACE; a_udp->socket_id = 0;
a_tcp->addr[0] = 10; a_tcp->addr[1] = 90; a_tcp->addr[2] = 0; a_tcp->addr[3] = 1;
a_tcp->port = 9001; a_tcp->protocol = TOPO_PROTO_TCP;
a_tcp->type = TOPO_ADDR_INTERFACE; a_tcp->socket_id = 0;
a_tcp->next = a_udp; ni->v4_addrs = a_tcp;
topo_node_registry_store(ctx.client->topo_groups, ni);
return nq;
}
static void srv_traffic_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || entry->len < 5) { if (entry) queue_entry_free(entry); return; }
struct ll_entry* reply = queue_entry_new(0);
if (!reply) { queue_entry_free(entry); return; }
reply->dgram = u_malloc(entry->len);
if (reply->dgram) { memcpy(reply->dgram, entry->dgram, entry->len); reply->len = entry->len; }
queue_entry_free(entry);
if (reply->dgram) etcp_send(conn, reply);
else queue_entry_free(reply);
}
static void cli_traffic_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
(void)conn;
if (!entry || entry->len < 5) { if (entry) queue_entry_free(entry); return; }
uint32_t seq;
memcpy(&seq, (uint8_t*)entry->dgram + 1, 4);
if (ctx.expected_rx_seq == 0) ctx.expected_rx_seq = seq;
if (seq == ctx.expected_rx_seq) {
ctx.expected_rx_seq++;
} else if (seq > ctx.expected_rx_seq) {
ctx.rx_seq_gaps++;
ctx.expected_rx_seq = seq + 1;
} else {
ctx.rx_seq_dups++;
}
ctx.pong_count++; ctx.total_recv++;
queue_entry_free(entry);
}
static void traffic_send_timer(void* arg) {
(void)arg;
if (ctx.result) return;
struct ETCP_CONN* conn = instance_find_conn(ctx.client, ctx.srv_node_id);
if (conn && conn->state != 2) {
uint8_t buf[5]; buf[0] = ETCP_RT_ID_TEST;
ctx.send_seq++; memcpy(buf + 1, &ctx.send_seq, 4);
struct ll_entry* e = queue_entry_new(0);
if (e) { e->dgram = u_malloc(5); if (e->dgram) { memcpy(e->dgram, buf, 5); e->len = 5;
etcp_send(conn, e); ctx.send_count++; } else queue_entry_free(e); }
}
if (!ctx.result) timeout_handle = uasync_set_timeout(ctx.ua, TRAF_SEND_TB, NULL, traffic_send_timer, "traf_snd");
}
static void traffic_monitor_timer(void* arg) {
(void)arg;
if (ctx.result) return;
uint32_t d = ctx.pong_count; ctx.pong_count = 0;
fprintf(stderr, " [traf] r=%d tx=%u rx=%u+d=%u rate=%u/s gaps=%u dups=%u\n",
ctx.round, ctx.send_count, ctx.total_recv, d, d * 10, ctx.rx_seq_gaps, ctx.rx_seq_dups);
fflush(stderr);
if (ctx.rx_seq_gaps > 100 || ctx.rx_seq_dups > 50) fail("too many seq anomalies");
if (!ctx.result) timeout_handle = uasync_set_timeout(ctx.ua, TRAF_MON_TB, NULL, traffic_monitor_timer, "traf_mon");
}
/* ── etcp_connect callback ── */
static void connect_cb(void* arg, struct ETCP_CONN* conn, int type) {
(void)arg; (void)conn;
if (type == ETCP_CONNECT_EARLY || type == ETCP_CONNECT_LATE) ctx.connected = 1;
}
static void start_traffic(void) {
timeout_handle = uasync_set_timeout(ctx.ua, TRAF_SEND_TB, NULL, traffic_send_timer, "traf_snd");
timeout_handle = uasync_set_timeout(ctx.ua, TRAF_MON_TB, NULL, traffic_monitor_timer, "traf_mon");
}
/* ── diagnostics ── */
static void diag_dump_state(const char* label) {
fprintf(stderr, "--- DIAG [%s] r=%d s=%d ---\n", label, ctx.round, ctx.step);
struct ll_entry* e = ctx.client->connections->head;
int conn_n = 0;
while (e) { conn_n++; e = e->next; }
fprintf(stderr, " connections=%d\n", conn_n);
fprintf(stderr, " current_iface=%s prev_iface=%s\n", ctx.cur_iface, ctx.prev_iface[0] ? ctx.prev_iface : "(none)");
/* list sockets */
fprintf(stderr, " sockets:\n");
struct ETCP_SOCKET* s = ctx.client->etcp_sockets;
while (s) {
char ifname[IFNAMSIZ] = "?";
if (s->netif_index) if_indextoname(s->netif_index, ifname);
fprintf(stderr, " %-30s fd=%d if=%s(%u) fam=%d tcp=%d\n",
s->name, (int)s->fd, ifname, s->netif_index,
s->local_addr.ss_family, s->is_tcp);
s = s->next;
}
/* list links for the server connection */
fprintf(stderr, " links to srv 0x%016llx:\n", (unsigned long long)ctx.srv_node_id);
e = ctx.client->connections->head;
while (e) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (ce->conn->peer_node_id == ctx.srv_node_id) {
int nlink = 0; for (struct ETCP_LINK* tl = ce->conn->links; tl; tl = tl->next) nlink++;
fprintf(stderr, " conn=%s state=%d links=%d\n",
ce->conn->log_name, ce->conn->state, nlink);
struct ETCP_LINK* l = ce->conn->links;
while (l) {
char ifname[IFNAMSIZ] = "?";
if (l->conn->netif_index) if_indextoname(l->conn->netif_index, ifname);
fprintf(stderr, " link=%d init=%d status=%d is_tcp=%d sock=%s if=%s\n",
l->local_link_id, l->initialized, l->link_status, l->is_tcp,
l->conn ? l->conn->name : "?", ifname);
l = l->next;
}
}
e = e->next;
}
fflush(stderr);
}
/* ── ip addr add/del via system ── */
static int ip_addr_add(const char* ifname, const char* cidr) {
char cmd[256]; snprintf(cmd, sizeof(cmd), "ip addr add %s dev %s 2>/dev/null", cidr, ifname);
return system(cmd);
}
static int ip_addr_del(const char* ifname, const char* cidr) {
char cmd[256]; snprintf(cmd, sizeof(cmd), "ip addr del %s dev %s 2>/dev/null", cidr, ifname);
return system(cmd);
}
static int ip_link_add(const char* ifname) {
char cmd[256]; snprintf(cmd, sizeof(cmd), "ip link add %s type dummy 2>/dev/null", ifname);
return system(cmd);
}
static int ip_link_del(const char* ifname) {
char cmd[256]; snprintf(cmd, sizeof(cmd), "ip link del %s 2>/dev/null", ifname);
return system(cmd);
}
static int ip_link_up(const char* ifname) {
char cmd[256]; snprintf(cmd, sizeof(cmd), "ip link set %s up 2>/dev/null", ifname);
return system(cmd);
}
/* ═══════════════════════════════════════════════════════════
* Phases
* ═══════════════════════════════════════════════════════════ */
static void phase_check_ip(void* arg);
static void phase_change_ip(void* arg);
static void phase_check_del(void* arg);
static void phase_del_prev(void* arg);
static void phase_check_add(void* arg);
static void phase_add(void* arg);
static void phase_del_last(void* arg);
static void phase_check_del_last(void* arg);
static void phase_full_disconnect(void* arg);
static void phase_reconnect(void* arg);
static void phase_check_reconnect(void* arg);
static void phase_done(void* arg);
static void phase_done(void* arg) {
(void)arg;
if (ctx.result) return;
struct ETCP_CONN* conn = find_srv_conn();
fprintf(stderr, "=== ALL PASSED === gaps=%u dups=%u reinit=%u\n",
ctx.rx_seq_gaps, ctx.rx_seq_dups, conn ? conn->reinit_count : 0);
fflush(stderr);
ctx.result = 2;
}
static void phase_del_last(void* arg) {
(void)arg;
if (ctx.result) return;
ctx.step = 11;
diag_dump_state("before del_last");
ip_link_del(rounds[N_ROUNDS-1].ifname);
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_check_del_last, "chk_del_last");
}
/* ══ check wrappers ══ */
static void do_check(void) {
int l = count_links_to_srv(ctx.cur_iface);
if (l < 1) { fail("no links"); return; }
if (l > 1) { fprintf(stderr, "[warn] multiple links=%d on %s\n", l, ctx.cur_iface); }
struct ETCP_CONN* conn = find_srv_conn();
if (conn) {
check_conn_health(conn);
if (has_stale_links(conn)) fail("stale links detected");
}
}
static void phase_check_del_last(void* arg) {
(void)arg;
ctx.step = 12;
int l = count_links_to_srv(rounds[N_ROUNDS-1].ifname);
if (l > 0) {
static int cnt = 0;
if (++cnt < 2) { uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_check_del_last, "chk_del_last"); return; }
diag_dump_state("chk_del_last fail");
fail("links survived last del"); return;
}
/* все dummy-сокеты должны исчезнуть */
int ntotal = count_all_client_udp() + count_all_client_tcp();
int ndummy = 0;
for (int i = 0; i < N_ROUNDS; i++) ndummy += count_sockets_on_iface(rounds[i].ifname);
if (ndummy > 0) fail("dummy sockets still exist after all deleted");
fprintf(stderr, " r=%d s=%d: no links after del_last total_socks=%d dummy_socks=%d (OK)\n",
ctx.round, ctx.step, ntotal, ndummy); fflush(stderr);
if (!ctx.all_deleted);
ctx.all_deleted = 1;
uasync_set_timeout(ctx.ua, STEP_TB * 3, NULL, phase_full_disconnect, "full_disconnect");
}
#define MIN_RECOVERY_PKTS 5
static void phase_check_ip(void* arg) {
(void)arg; if (ctx.result) return;
static uint64_t wait_start = 0;
static int diag_cnt = 0;
static uint32_t recv_ok;
ctx.step = 6;
if (count_links_to_srv(ctx.cur_iface) == 0 || ctx.total_recv <= ctx.recv_at_ip_change + MIN_RECOVERY_PKTS) {
if (++diag_cnt <= 3) diag_dump_state("wait chk_ip");
if (!wait_start) { wait_start = get_time_tb(); recv_ok = 0; }
else if (ctx.total_recv > ctx.recv_at_ip_change) recv_ok++;
if (get_time_tb() - wait_start < (uint64_t)STEP_TB * 10) {
uasync_set_timeout(ctx.ua, STEP_TB/3, NULL, phase_check_ip, "chk_ip"); return;
}
diag_dump_state("timeout chk_ip");
fail("traffic not recovered after IP change");
return;
}
diag_cnt = 0;
wait_start = 0;
do_check(); if (ctx.result) return;
if (ctx.round == N_ROUNDS - 1 && ctx.ip_changes_on_last < 2) {
ctx.ip_changes_on_last++;
fprintf(stderr, " r=%d s=%d: IP change #%d verified — another\n", ctx.round, ctx.step, ctx.ip_changes_on_last); fflush(stderr);
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_change_ip, "chg_ip2");
} else if (ctx.round == N_ROUNDS - 1 && ctx.ip_changes_on_last >= 2) {
fprintf(stderr, " r=%d s=%d: both IP changes on last round — deleting\n", ctx.round, ctx.step); fflush(stderr);
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_del_last, "del_last");
} else {
strncpy(ctx.prev_iface, ctx.cur_iface, IFNAMSIZ - 1);
ctx.round++;
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_add, "next_add");
}
}
static void phase_change_ip(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 5;
const char* old_ip = (ctx.ip_changes_on_last >= 2) ? rounds[ctx.round].ip2 : rounds[ctx.round].ip1;
const char* new_ip = (ctx.ip_changes_on_last >= 2) ? rounds[ctx.round].ip1 : rounds[ctx.round].ip2;
ip_addr_del(rounds[ctx.round].ifname, old_ip);
ip_addr_add(rounds[ctx.round].ifname, new_ip);
ctx.recv_at_ip_change = ctx.total_recv;
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_check_ip, "chk_ip");
}
static void phase_check_del(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 4;
do_check(); if (ctx.result) return;
/* сокеты удалённого интерфейса должны исчезнуть */
int stale = count_sockets_on_iface(ctx.prev_iface);
if (stale > 0) fail("sockets survived for deleted iface");
fprintf(stderr, " r=%d s=%d: del_prev OK links=%d stale_socks=%d\n", ctx.round, ctx.step, count_links_to_srv(ctx.cur_iface), stale);
fflush(stderr);
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_change_ip, "chg_ip");
}
static void phase_del_prev(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 3;
ip_link_del(ctx.prev_iface);
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_check_del, "chk_del");
}
static void phase_check_add(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 2;
if (ctx.round == 0 && !ctx.connected) {
uasync_set_timeout(ctx.ua, STEP_TB / 3, NULL, phase_check_add, "chk_add"); return;
}
do_check(); if (ctx.result) return;
/* на текущем интерфейсе должен быть хотя бы 1 UDP и 1 TCP сокет */
int socks = count_sockets_on_iface(ctx.cur_iface);
if (socks < 2) { fprintf(stderr, "[warn] only %d sockets on %s\n", socks, ctx.cur_iface); }
fprintf(stderr, " r=%d s=%d: add OK links=%d socks=%d udp=%d tcp=%d\n",
ctx.round, ctx.step, count_links_to_srv(ctx.cur_iface), socks, count_all_client_udp(), count_all_client_tcp());
fflush(stderr);
if (ctx.round == 0) start_traffic();
/* snapshot reinit на входе в цикл */
struct ETCP_CONN* conn = find_srv_conn();
if (conn) { ctx.conn_reinit_snapshot = conn->reinit_count; ctx.max_allowed_reinits = 2; }
if (ctx.prev_iface[0]) {
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_del_prev, "del_prev");
} else {
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_change_ip, "chg_ip");
}
}
static void phase_add(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 1;
strncpy(ctx.cur_iface, rounds[ctx.round].ifname, IFNAMSIZ - 1);
ip_link_add(ctx.cur_iface);
ip_addr_add(ctx.cur_iface, rounds[ctx.round].ip1);
ip_link_up(ctx.cur_iface);
if (ctx.round == 0) {
struct TOPO_GROUP_NODE* sn = mk_srv_node();
if (!sn) { fail("mk_srv_node"); return; }
queue_data_put_with_index(topo_groups_get_default(ctx.client->topo_groups)->nodes, &sn->ll);
etcp_connect(ctx.client, sn, connect_cb, NULL, ETCP_CONNECT_EARLY | ETCP_CONNECT_LATE);
}
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_check_add, "chk_add");
}
/* ── full disconnect + reconnect ── */
static void phase_full_disconnect(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 20;
for (int i = 0; i < N_ROUNDS; i++) ip_link_del(rounds[i].ifname);
ctx.pre_delete_total_recv = ctx.total_recv;
fprintf(stderr, " r=%d s=%d: all interfaces deleted — awaiting reconnect\n", ctx.round, ctx.step); fflush(stderr);
uasync_set_timeout(ctx.ua, STEP_TB * 5, NULL, phase_reconnect, "reconnect");
}
static void phase_reconnect(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 21;
/* убеждаемся что все линки упали */
struct ETCP_CONN* conn = find_srv_conn();
int live = conn ? count_live_links_on_conn(conn) : 0;
fprintf(stderr, " r=%d s=%d: live_links=%d\n", ctx.round, ctx.step, live); fflush(stderr);
/* создаём новый интерфейс */
char new_if[] = "dummy_reconn";
ip_link_add(new_if);
ip_addr_add(new_if, "10.90.1.100/16");
ip_link_up(new_if);
strncpy(ctx.cur_iface, new_if, IFNAMSIZ - 1);
ctx.recv_at_ip_change = ctx.total_recv;
conn = find_srv_conn();
ctx.conn_reinit_snapshot = conn ? conn->reinit_count : 0;
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_check_reconnect, "chk_reconn");
}
static void phase_check_reconnect(void* arg) {
(void)arg; if (ctx.result) return;
ctx.step = 22;
static uint64_t wait_start = 0;
if (count_links_to_srv(ctx.cur_iface) == 0 || ctx.total_recv <= ctx.pre_delete_total_recv + MIN_RECOVERY_PKTS) {
if (!wait_start) wait_start = get_time_tb();
if (get_time_tb() - wait_start < (uint64_t)STEP_TB * 20) {
uasync_set_timeout(ctx.ua, STEP_TB/3, NULL, phase_check_reconnect, "chk_reconn"); return;
}
diag_dump_state("reconnect timeout");
fail("connection not recovered after full disconnect");
return;
}
wait_start = 0;
do_check(); if (ctx.result) return;
struct ETCP_CONN* conn = find_srv_conn();
check_conn_health(conn);
ip_link_del("dummy_reconn");
fprintf(stderr, " r=%d s=%d: reconnect OK links=%d reinit=%u\n",
ctx.round, ctx.step, count_links_to_srv(ctx.cur_iface), conn ? conn->reinit_count : 0);
fflush(stderr);
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_done, "done");
}
/* ═══════════════════════════════════════════════════════════
* setup / cleanup / main
* ═══════════════════════════════════════════════════════════ */
static void cleanup_ifaces(void) {
(void)system("ip link del dummy_srv 2>/dev/null");
for (int i = 0; i < N_ROUNDS; i++) ip_link_del(rounds[i].ifname);
}
static void setup(void) {
if (geteuid() != 0) { fprintf(stderr, "SKIP: test requires root\n"); exit(77); }
cleanup_ifaces();
test_mkdtemp(tdir);
snprintf(scf, sizeof(scf), "%s/s.conf", tdir);
snprintf(ccf, sizeof(ccf), "%s/c.conf", tdir);
/* create db directories */
{ char dbd[256]; snprintf(dbd, sizeof(dbd), "%s/db_srv", tdir); utun_mkdir(dbd, 0755);
snprintf(dbd, sizeof(dbd), "%s/db_cli", tdir); utun_mkdir(dbd, 0755); }
/* server: fixed [server] on dummy_srv */
ip_link_add("dummy_srv");
ip_addr_add("dummy_srv", "10.90.0.1/16");
ip_link_up("dummy_srv");
/* client: first interface */
ip_link_add(rounds[0].ifname);
ip_addr_add(rounds[0].ifname, rounds[0].ip1);
ip_link_up(rounds[0].ifname);
/* Step 1: write minimal configs → generate keys + node_id via config_ensure */
wf(scf, "[global]\ntun_ip=10.99.0.1/24\ntun_ifname=tun_srv\ntun_test_mode=1\n"
"auto_sockets=no\ndb_path=%s/db_srv\n"
"[server: fixed]\naddr=10.90.0.1:9001\ntype=public\n[allowed_keys]\nallow_all=1\n", tdir);
wf(ccf, "[global]\ntun_ip=10.99.0.2/24\ntun_ifname=tun_cli\ntun_test_mode=1\n"
"auto_sockets=yes\ndb_path=%s/db_cli\n[allowed_keys]\nallow_all=1\n", tdir);
config_ensure_keys_and_node_id(scf);
config_ensure_keys_and_node_id(ccf);
/* Step 2: read generated keys and node_id */
char *spub = gv(scf, "pub"), *spriv = gv(scf, "priv");
char *cpub = gv(ccf, "pub"), *cpriv = gv(ccf, "priv");
{ struct utun_config* cs = parse_config(scf); ctx.srv_node_id = cs->global.my_node_id; free_config(cs); }
/* Step 3: write final configs with all values embedded */
wf(scf, "[global]\nmy_private_key=%s\nmy_public_key=%s\nmy_node_id=0x%016llx\n"
"tun_ip=10.99.0.1/24\ntun_ifname=tun_srv\ntun_test_mode=1\n"
"auto_sockets=no\ndb_path=%s/db_srv\n"
"[server: fixed]\naddr=10.90.0.1:9001\ntype=public\n[allowed_keys]\nallow_all=1\n",
spriv, spub, (unsigned long long)ctx.srv_node_id, tdir);
wf(ccf, "[global]\nmy_private_key=%s\nmy_public_key=%s\n"
"tun_ip=10.99.0.2/24\ntun_ifname=tun_cli\ntun_test_mode=1\n"
"auto_sockets=yes\ndb_path=%s/db_cli\n[allowed_keys]\nallow_all=1\n",
cpriv, cpub, tdir);
/* extract server pubkey binary */
struct utun_config* cs2 = parse_config(scf);
if (cs2 && cs2->global.my_public_key_hex)
sc_hex_to_binary(cs2->global.my_public_key_hex, ctx.srv_pubkey, SC_PUBKEY_SIZE);
free_config(cs2);
u_free(spub); u_free(spriv); u_free(cpub); u_free(cpriv);
}
static void cleanup(void) { test_unlink(scf); test_unlink(ccf); test_rmdir(tdir); }
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
utun_instance_set_tun_init_enabled(0);
setup();
ctx.ua = uasync_create();
ctx.server = utun_instance_create(ctx.ua, scf);
ctx.client = utun_instance_create(ctx.ua, ccf);
if (!ctx.server || !ctx.client) goto done;
utun_instance_init(ctx.server);
utun_instance_init(ctx.client);
etcp_bind(ctx.server, ETCP_RT_ID_TEST, srv_traffic_handler);
etcp_bind(ctx.client, ETCP_RT_ID_TEST, cli_traffic_handler);
/* store init state */
ctx.prev_iface[0] = '\0';
strncpy(ctx.prev_iface, rounds[0].ifname, IFNAMSIZ - 1);
ctx.prev_iface[0] = '\0'; /* round 0 has no prev */
uasync_set_timeout(ctx.ua, STEP_TB, NULL, phase_add, "init");
timeout_handle = uasync_set_timeout(ctx.ua, TIMEOUT_TB, NULL, to_cb, "to");
{ uint64_t start = get_time_tb();
while (!ctx.result && (int)(get_time_tb() - start) < TIMEOUT_TB + 50000)
uasync_poll(ctx.ua, POLL_MS); }
fprintf(stderr, "final result=%d\n", ctx.result); fflush(stderr);
done:
if (timeout_handle) uasync_cancel_timeout(ctx.ua, timeout_handle);
if (ctx.server) { ctx.server->running = 0; utun_instance_destroy(ctx.server); }
if (ctx.client) { ctx.client->running = 0; utun_instance_destroy(ctx.client); }
if (ctx.ua) { uasync_destroy(ctx.ua, 0); ctx.ua = NULL; }
cleanup();
return (ctx.result == 2) ? 0 : 1;
}