From e801fce2c6b9d2531adc16512464de116cfa1804 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 23 Oct 2026 13:38:36 +0200 Subject: [PATCH] Add encrypted UDP INIT liveness and one-way recovery regression --- tests/Makefile.am | 5 + tests/test_etcp_init_liveness.c | 172 ++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 tests/test_etcp_init_liveness.c diff --git a/tests/Makefile.am b/tests/Makefile.am index 8fb2c2cd..7edaa50a 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -52,6 +52,7 @@ check_PROGRAMS = \ test_reality_bgp \ test_tcp_proxy_client \ test_etcp_session \ + test_etcp_init_liveness \ test_proxy_packets \ test_media_delivery_download \ test_transport \ @@ -1023,6 +1024,10 @@ test_etcp_session_SOURCES = test_etcp_session.c $(test_clock_sources) test_etcp_session_LDADD = $(LIBUTUN) $(COMMON_LIBS) test_etcp_session_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=ll_alloc_lldgram +test_etcp_init_liveness_SOURCES = test_etcp_init_liveness.c $(test_clock_sources) +test_etcp_init_liveness_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_etcp_init_liveness_LDFLAGS = $(test_clock_ldflags) + test_etcp_windows_SOURCES = test_etcp_windows.c $(test_clock_sources) test_dummynet_channels_SOURCES = test_dummynet_channels.c $(test_clock_sources) test_dummynet_channels_LDADD = $(LIBUTUN) $(COMMON_LIBS) diff --git a/tests/test_etcp_init_liveness.c b/tests/test_etcp_init_liveness.c new file mode 100644 index 00000000..39c4ca2e --- /dev/null +++ b/tests/test_etcp_init_liveness.c @@ -0,0 +1,172 @@ +/* Настоящие UDP/INIT/AES-CCM: односторонний разрыв, повторный handshake и события живости. */ +#include +#include +#include "etcp.h" +#include "etcp_api.h" +#include "etcp_connections.h" +#include "etcp_keepalive.h" +#include "utun_instance.h" +#include "test_clock.h" +#include "test_net.h" +#include "../lib/mem.h" + +#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "FAIL %s:%d: %s", __func__, __LINE__, #x); exit(1); } } while (0) +struct endpoint { + struct UTUN_INSTANCE* inst; + struct ETCP_SOCKET* socket; + struct ETCP_CONN* conn; + unsigned block_id, init_zero, response_one, up, down, reinit, received; +}; +static struct endpoint ep[2]; +static struct UASYNC* ua; +static unsigned close_action, close_fired; +static const char* pubkeys[] = { + "b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01", + "ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a" +}; +static const char* privkeys[] = { + "704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f", + "38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68" +}; + +/* Наблюдаем уже зашифрованный wire, отдельная копия crypto не меняет счётчики рабочего соединения. */ +static ssize_t send_observer(socket_t fd, const void* bytes, size_t len, const struct sockaddr* addr, + socklen_t addr_len, struct ETCP_LINK* link, void* arg) { + struct endpoint* e = arg; + uint8_t plain[PACKET_DATA_SIZE]; size_t size = 0; + sc_context_t copy = link->etcp->crypto_ctx; + int decoded = sc_decrypt(©, bytes, len, plain, &size) == SC_OK; + if (!decoded && len > SC_PUBKEY_ENC_SIZE) { + copy = link->etcp->crypto_ctx; + decoded = sc_decrypt(©, bytes, len - SC_PUBKEY_ENC_SIZE, plain, &size) == SC_OK; + } + CHECK(decoded && size >= 4); + if ((plain[3] == ETCP_INIT_REQUEST || plain[3] == ETCP_INIT_REQUEST_NOINIT) && !(plain[2] & 1)) e->init_zero++; + if ((plain[3] == ETCP_INIT_RESPONSE || plain[3] == ETCP_INIT_RESPONSE_NOINIT) && (plain[2] & 1)) e->response_one++; + if (e->block_id && (e->block_id == 256 || e->block_id == link->local_link_id)) return len; + return socket_sendto(fd, bytes, len, addr, addr_len); +} + +/* Подписка действует и на новые линки; close проверяется именно в цепочке уведомления DOWN. */ +static void link_event(struct ETCP_CONN* conn, struct ETCP_LINK* link, int old_state, int old_status, void* arg) { + (void)old_state; + struct endpoint* e = arg; + link->send_hook = send_observer; link->send_hook_ctx = e; + if (e == &ep[1] && old_status && !link->link_status && close_action == 1) { + CHECK(!close_fired++); etcp_connection_close(conn); + } +} + +static void conn_event(struct ETCP_CONN* conn, int event, void* arg) { + struct endpoint* e = arg; + if (event == ETCP_CBK_EVENT_UP) e->up++; + if (event == ETCP_CBK_EVENT_DOWN) e->down++; + if (event == ETCP_CBK_EVENT_REINIT) e->reinit++; + if (e == &ep[1] && event == ETCP_CBK_EVENT_DOWN && close_action == 2) { + CHECK(!close_fired++); etcp_connection_close(conn); + } +} + +static void receive(struct ETCP_CONN* conn, struct ll_entry* entry) { + unsigned side = conn->instance == ep[1].inst; + CHECK(entry->len == 2 && entry->dgram[0] == 253 && entry->dgram[1] == 17); + ep[side].received++; queue_dgram_free(entry); queue_entry_free(entry); +} + +static struct ETCP_LINK* first_link(unsigned side) { + return ep[side].conn->links; +} + +static void run_for(unsigned tb) { test_clock_run_for(ua, tb); } + +/* Только ядро и одна явно выбранная UDP-пара: BGP/NCD не создают обходных путей. */ +static void setup(unsigned swapped, unsigned count) { + memset(ep, 0, sizeof(ep)); close_action = close_fired = 0; + ua = uasync_create(); CHECK(ua); + for (unsigned side = 0; side < 2; side++) { + unsigned key = side ^ swapped; + char config[1536]; + snprintf(config, sizeof(config), "[global]\nmy_public_key=%s\nmy_private_key=%s\n" + "keepalive_interval=50\nkeepalive_timeout=500\nkeepalive_adaptive=0\nauto_sockets=no\n" + "[server:srv]\naddr=127.0.0.1:0\ntype=public\n[allowed_keys]\nallow_all=1\n", pubkeys[key], privkeys[key]); + ep[side].inst = utun_instance_create_from_str(ua, config); CHECK(ep[side].inst); + CHECK(utun_core_start(ep[side].inst) == 0); + ep[side].socket = test_etcp_bound_socket(ep[side].inst, "srv"); + etcp_add_link_status_cbk(ep[side].inst, link_event, &ep[side]); + CHECK(etcp_bind(ep[side].inst, 253, receive) == 0); + } + ep[0].conn = etcp_connection_create(ep[0].inst, "liveness"); CHECK(ep[0].conn); + ep[0].conn->peer_node_id = ep[1].inst->node_id; + CHECK(sc_set_peer_public_key(&ep[0].conn->crypto_ctx, ep[1].inst->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK); + etcp_conn_add_cbk(ep[0].conn, conn_event, &ep[0], ETCP_CBK_EVENT_UP | ETCP_CBK_EVENT_DOWN | ETCP_CBK_EVENT_REINIT); + for (unsigned i = 0; i < count; i++) { + struct ETCP_SOCKET* socket = ep[0].socket; + if (i) { socket = etcp_socket_add(ep[0].inst, "127.0.0.1", 0, "extra", 0); CHECK(socket); } + CHECK(etcp_link_new(ep[0].conn, socket, &ep[1].socket->interface_addr, 0)); + } + uint64_t end = get_time_tb() + 30000; + while (get_time_tb() < end) { + test_clock_poll(ua, 10); + ep[1].conn = instance_find_conn(ep[1].inst, ep[0].inst->node_id); + if (ep[1].conn && ep[1].conn->links_up && ep[0].conn->links_up && ep[1].response_one) break; + } + CHECK(ep[1].conn && ep[0].conn->links_up && ep[1].conn->links_up); + etcp_conn_add_cbk(ep[1].conn, conn_event, &ep[1], ETCP_CBK_EVENT_UP | ETCP_CBK_EVENT_DOWN | ETCP_CBK_EVENT_REINIT); + CHECK(etcp_conn_ref_take(ep[1].conn) == 0); + run_for(1000); + CHECK(ep[0].init_zero && ep[1].response_one && ep[0].conn->initialized && ep[1].conn->initialized); +} + +static void teardown(void) { + close_action = 0; + etcp_conn_ref_free(ep[1].conn); + for (unsigned side = 0; side < 2; side++) utun_instance_destroy(ep[side].inst); + uasync_drain_immediate(ua); + CHECK(ua->timer_alloc_count == ua->timer_free_count); + uasync_destroy(ua, 0); +} + +/* При работающем B->A повтор INIT(0) не сохраняет ложный UP на A; восстановление не меняет эпоху. */ +static void one_way(unsigned swapped, unsigned blocked_side) { + setup(swapped, 1); + uint64_t epoch[] = {ep[0].conn->reset_id, ep[1].conn->reset_id}; + unsigned before = ep[0].init_zero; + ep[blocked_side].block_id = 256; + run_for(25000); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "One-way swapped=%u blocked_side=%u INIT0=%u up=%u/%u remote=%u/%u DOWN=%u/%u", + swapped, blocked_side, ep[0].init_zero-before, ep[0].conn->links_up, ep[1].conn->links_up, + first_link(0)->remote_keepalive, first_link(1)->remote_keepalive, ep[0].down, ep[1].down); + CHECK(!ep[0].conn->links_up && !ep[1].conn->links_up && ep[0].down == 1 && ep[1].down == 1); + if (blocked_side == 1) CHECK(ep[0].init_zero >= before + 2 && !first_link(1)->remote_keepalive && first_link(1)->recv_keepalive); + ep[blocked_side].block_id = 0; run_for(10000); + CHECK(ep[0].conn->links_up && ep[1].conn->links_up && ep[1].up == 1); + for (unsigned side = 0; side < 2; side++) { + CHECK(ep[side].conn->reset_id == epoch[side] && !ep[side].reinit); + struct ll_entry* entry = ll_alloc_lldgram(2); CHECK(entry); + entry->len = 2; entry->dgram[0] = 253; entry->dgram[1] = 17; + CHECK(etcp_send(ep[side].conn, entry) == 0); + } + run_for(1000); CHECK(ep[0].received == 1 && ep[1].received == 1); + teardown(); +} + +static void close_in_callback(unsigned action) { + setup(0, 1); close_action = action; ep[1].block_id = 256; + run_for(15000); + CHECK(close_fired == 1 && ep[1].conn->state == 2 && !ep[1].conn->normalizer && !ep[1].conn->callbacks_running); + teardown(); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + debug_set_category_level(DEBUG_CATEGORY_CRYPTO, DEBUG_LEVEL_DISABLED); + if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); + test_clock_init(10); CHECK(socket_platform_init() == 0); + size_t baseline = u_get_allocated_count(); + for (unsigned swapped = 0; swapped < 2; swapped++) for (unsigned blocked = 0; blocked < 2; blocked++) { + one_way(swapped, blocked); CHECK(u_get_allocated_count() == baseline); + } + for (unsigned action = 1; action <= 2; action++) { close_in_callback(action); CHECK(u_get_allocated_count() == baseline); } + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS encrypted INIT liveness: cold start, one-way loss, repeated INIT, recovery and callback close"); + test_clock_finish(); socket_platform_cleanup(); return 0; +}