From a49cf5434ebcdb071af9bc21c0d3facda7eb2494 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 10:45:42 +0200 Subject: [PATCH] Add randomized ETCP padding and isolate STCP CTR streams per connection --- build_direct.sh | 2 +- doc/etcp_protocol.txt | 27 ++- doc/tasks.md | 17 ++ src/Makefile.am | 4 + src/transport_layer/etcp.c | 6 + src/transport_layer/etcp.h | 3 + src/transport_layer/etcp_connections.c | 40 +++- src/transport_layer/etcp_connections.h | 2 +- src/transport_layer/etcp_keepalive.c | 27 ++- src/transport_layer/etcp_keepalive.h | 1 + src/transport_layer/etcp_padding.c | 77 +++++++ src/transport_layer/etcp_padding.h | 36 +++ src/transport_layer/secure_channel.c | 22 +- src/transport_layer/secure_channel.h | 4 +- src/transport_layer/secure_channel_doc.md | 12 +- src/transport_layer/stcp.h | 5 +- src/transport_layer/stcp_client.c | 39 +++- src/transport_layer/stcp_client.h | 7 +- src/transport_layer/stcp_client_doc.md | 11 +- src/transport_layer/stcp_link.c | 2 +- src/transport_layer/stcp_server.c | 32 ++- src/transport_layer/stcp_server_doc.md | 12 +- src/utun_instance.c | 4 + src/utun_instance.h | 2 + tests/Makefile.am | 6 +- tests/test_etcp_padding.c | 259 ++++++++++++++++++++++ tests/test_standby_transport.c | 9 +- tests/test_stcp.c | 215 +++++++++++++++++- tests/test_stcp_link.c | 4 +- tests/test_stcp_ping.c | 26 ++- tests/test_stream_cipher.c | 44 ++-- 31 files changed, 854 insertions(+), 103 deletions(-) create mode 100644 src/transport_layer/etcp_padding.c create mode 100644 src/transport_layer/etcp_padding.h create mode 100644 tests/test_etcp_padding.c diff --git a/build_direct.sh b/build_direct.sh index 71c098b8..37b7237c 100644 --- a/build_direct.sh +++ b/build_direct.sh @@ -70,7 +70,7 @@ echo "Building utun.exe..." cd src EXE_NEEDS_REBUILD=0 -CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c" +CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c" for src in $CORE_SOURCES; do if [ -f "$src" ]; then diff --git a/doc/etcp_protocol.txt b/doc/etcp_protocol.txt index e17a458b..cf3e8f8b 100644 --- a/doc/etcp_protocol.txt +++ b/doc/etcp_protocol.txt @@ -167,13 +167,13 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected. - random_padding: случайные байты 3) PING (0x06): - [0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] + [0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией). Используется для проверки доступности узла и NAT-тестирования. 4) PONG (0x07): - [0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] + [0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] Ответ на PING. nonce копируется из запроса для сопоставления. @@ -183,11 +183,32 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected. приём кодограмм из UDP выглядит так: uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue -Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0). +Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...]. +При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с. +Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding. keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика. Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента). flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты). +Padding keepalive, PING/PONG и STCP-handshake: +- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков. +- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN. +- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные. +- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется. +- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования. +- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы. + user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса. +- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт + нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding; + padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance. +- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client. + CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта. + Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела. + После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр. +- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU; + если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных. +- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug. + Клиент: если все линки =down то начинается процедура восстановления связи: - клиент посылает init без сброса (0x04), link_state=2 - сервер как получает init проверяет session_id: diff --git a/doc/tasks.md b/doc/tasks.md index 4471739c..5feafec3 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -3,6 +3,23 @@ Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ. ## Текущая задача +[+] **Случайный зашифрованный padding ETCP** — 07.10.2026: + keepalive UDP/TCP, UDP PING/PONG и STCP-handshake используют нулевой padding переменной длины. + Каждый ETCP_CONN хранит свой диапазон и таймер; отдельный диапазон instance обслуживает пинги без соединения. + min=rand(0,30), max=min+rand(32,128), обновление каждые rand(50,300) секунд. + Добавлены проверки границ 0/158, эхо-данных, sleep/ACK, независимости и отмены таймеров, ошибок MTU/формата. + TCP-пинг не удерживает instance. Полный check.sh: 134 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS. + +[+] **STCP: исключить повтор CTR-потока между подключениями** — 07.10.2026: + CTR IV теперь зависит от session_key, направления и соли отправителя из существующего блока handshake. + Потоки инициализируются по мере получения солей; шифротекст Ed25519 и нулевого padding меняется. + Проверяются повторные и параллельные подключения одной пары, короткие send/recv и подмена соли при том же ECDH. + STCP — 14/14; полный check.sh — 134 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS. + +[ ] **REALITY: исправить формат ClientHello и продолжение TLS-потока** — 07.10.2026: + supported_groups и signature_algorithms не содержат внутренние двухбайтовые длины списков. + После ClientHello/ServerHello идёт STCP без TLS record framing; это распознаётся пассивным наблюдателем. + [+] **Одна реакция автора и горизонтальный выбор смайликов** — 07.10.2026: выбор в Android и desktop отображается одной горизонтальной прокручиваемой строкой с выделением своего смайлика. Ядро допускает одну реакцию на сообщение от автора; другой смайлик атомарно заменяет прежний, повторный клик отменяет. diff --git a/src/Makefile.am b/src/Makefile.am index 8d1c19ff..71eec505 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -32,6 +32,8 @@ utun_CORE_SOURCES = \ tun_freebsd.c \ tun_windows.c \ transport_layer/etcp.c \ + transport_layer/etcp_padding.c \ + transport_layer/etcp_padding.h \ transport_layer/etcp_session.c \ transport_layer/etcp_session.h \ transport_layer/etcp_connections.c \ @@ -170,6 +172,8 @@ libutun_a_SOURCES = \ tun_freebsd.c \ tun_windows.c \ transport_layer/etcp.c \ + transport_layer/etcp_padding.c \ + transport_layer/etcp_padding.h \ transport_layer/etcp_session.c \ transport_layer/etcp_session.h \ transport_layer/etcp_connections.c \ diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index b564c822..b0e40ad2 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -276,6 +276,11 @@ struct ETCP_CONN* etcp_connection_create(struct UTUN_INSTANCE* instance, char* n queue_data_put_with_index(instance->connections, qe); } + if (etcp_padding_start(&etcp->padding, instance->ua, etcp->log_name) < 0) { + etcp_connection_close(etcp); + return NULL; + } + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] connection initialized. ETCP=%p mtu=%d, next_tx_id=%u state=pending", etcp->log_name, etcp, etcp->mtu, etcp->next_tx_id); @@ -423,6 +428,7 @@ void etcp_connection_close(struct ETCP_CONN* etcp) { callring_put(CR_CCLOSE_IN, (uintptr_t)etcp, 0); etcp->close_requested = 1; + etcp_padding_stop(&etcp->padding); if (etcp->callbacks_running) { if (etcp->close_token) return; DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] close requested inside callback chain — deferring", etcp->log_name); diff --git a/src/transport_layer/etcp.h b/src/transport_layer/etcp.h index 2b1dbc32..8338d482 100644 --- a/src/transport_layer/etcp.h +++ b/src/transport_layer/etcp.h @@ -8,6 +8,7 @@ #include "etcp_connections.h" #include "secure_channel.h" +#include "etcp_padding.h" #include "../lib/ll_queue.h" #include @@ -121,6 +122,8 @@ struct ETCP_CONN { uint8_t tcp_link_count; // количество UP TCP линков uint8_t last_assigned_link_id; // последний выданный link_id (для циклического перебора) + struct ETCP_PADDING padding; // диапазон и независимый таймер всех линков этого соединения + // Crypto and state struct secure_channel crypto_ctx; struct PKTNORM* normalizer; diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index e5bd6ca3..ab43e7c2 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -363,16 +363,16 @@ void etcp_link_enter_reinit(struct ETCP_LINK* link) { // Собирает 19-байтовый ключ из sockaddr (port+addr+family) для hash-индекса links_queue. -static void sockaddr_to_key(struct sockaddr_storage* addr, uint8_t key[LINK_ADDR_KEY_SIZE], int is_tcp) { +static void sockaddr_to_key(const struct sockaddr_storage* addr, uint8_t key[LINK_ADDR_KEY_SIZE], int is_tcp) { memset(key, 0, LINK_ADDR_KEY_SIZE); if (!addr) return; if (addr->ss_family == AF_INET) { - struct sockaddr_in* sa = (struct sockaddr_in*)addr; + const struct sockaddr_in* sa = (const struct sockaddr_in*)addr; memcpy(key, &sa->sin_port, 2); memcpy(key + 2, &sa->sin_addr.s_addr, 4); key[18] = 4 | (is_tcp ? 0x80 : 0); } else { - struct sockaddr_in6* sa = (struct sockaddr_in6*)addr; + const struct sockaddr_in6* sa = (const struct sockaddr_in6*)addr; memcpy(key, &sa->sin6_port, 2); memcpy(key + 2, &sa->sin6_addr, 16); key[18] = 6 | (is_tcp ? 0x80 : 0); @@ -480,7 +480,7 @@ static int sockaddr_equal(const struct sockaddr_storage* a, const struct sockadd } // Поиск линка в сокете по remote-адресу (через hash-индекс links_queue). -struct ETCP_LINK* etcp_link_find_by_addr(struct ETCP_SOCKET* e_sock, struct sockaddr_storage* addr, int is_tcp) { +struct ETCP_LINK* etcp_link_find_by_addr(struct ETCP_SOCKET* e_sock, const struct sockaddr_storage* addr, int is_tcp) { DEBUG_TRACE(DEBUG_CATEGORY_ETCP, ""); if (!e_sock || !addr || !e_sock->links_queue) return NULL; uint8_t key[LINK_ADDR_KEY_SIZE]; @@ -1451,6 +1451,25 @@ static int etcp_send_ping_raw(struct ETCP_DGRAM* dgram, struct ETCP_SOCKET* e_so } int len = dgram->data_len - dgram->noencrypt_len; uint8_t enc_buf[1600]; + struct ETCP_LINK* link = etcp_link_find_by_addr(e_sock, addr, 0); + int mtu = link ? link->mtu : PACKET_DATA_MAX_MTU; + int udp_overhead = addr->ss_family == AF_INET6 ? IP_UDP_OVERHEAD_V6 : IP_UDP_OVERHEAD_V4; + int available = mtu - udp_overhead - len - 3 - SC_NONCE_SIZE - SC_CRC32_SIZE - SC_TAG_SIZE - dgram->noencrypt_len; + if (len < 0 || available < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "ping exceeds MTU mtu=%d body=%d tail=%u addr=%s", + mtu, len, dgram->noencrypt_len, sockaddr_storage_to_str(addr).str); + return -1; + } + const struct ETCP_PADDING* profile = etcp_padding_for_peer(e_sock->instance, sc->peer_public_key); + int padding = etcp_padding_size(profile, available); + if (padding < 0) return -1; + memmove(dgram->data + len + padding, dgram->data + len, dgram->noencrypt_len); + memset(dgram->data + len, 0, padding); + dgram->data_len += padding; + len += padding; + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "ping TX code=%u owner=%s range=%u..%u padding=%d body=%d mtu=%d", + dgram->data[0], profile == &e_sock->instance->padding ? "instance" : "connection", + profile->min, profile->max, padding, len, mtu); if (len < 0 || len + (int)dgram->noencrypt_len + SC_ENCRYPT_OVERHEAD > (int)sizeof(enc_buf)) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "ping packet too large len=%d ne=%d", len, dgram->noencrypt_len); return -1; @@ -1699,7 +1718,7 @@ int etcp_send_tcp_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubke struct stcp_client* cli = stcp_ping_send(instance->ua, addr_str, port, &instance->my_keys, peer_pubkey_bin, instance->my_ed25519_pubkey, instance->client_type, - instance->keepalive_interval, timeout_ms, tcp_ping_cb_adapter, a, socks); + instance->keepalive_interval, timeout_ms, tcp_ping_cb_adapter, a, socks, instance); if (!cli) { u_free(a); return -4; } if (out_cli) *out_cli = cli; DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "tcp ping to %s:%u timeout=%d", addr_str, (unsigned)port, timeout_ms); @@ -1718,7 +1737,11 @@ static int handle_ping(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const uint64_t peer_id = wire_read64(pkt->data + 2); uint64_t nonce = wire_read64(pkt->data + 10); uint16_t ulen = wire_read16(pkt->data + 18); - if (ulen > pkt->data_len - 20) ulen = (uint16_t)(pkt->data_len - 20); + if (ulen > pkt->data_len - 20 || pkt->data_len - 20 - ulen > ETCP_PADDING_MAX) { + DEBUG_WARN(DEBUG_CATEGORY_ETCP, "invalid PING user_len=%u body=%u from=%s", + ulen, pkt->data_len, sockaddr_storage_to_str(addr).str); + return 7; + } const uint8_t* udata = (ulen > 0) ? (pkt->data + 20) : NULL; if ((flags & ETCP_PING_FLAG_SEND_RTT) && ulen >= 2) { @@ -1784,6 +1807,11 @@ static int handle_pong(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const const uint8_t* udata = NULL; if (pkt->data_len >= 20) { ulen = wire_read16(pkt->data + 18); + if (ulen > pkt->data_len - 20 || pkt->data_len - 20 - ulen > ETCP_PADDING_MAX) { + DEBUG_WARN(DEBUG_CATEGORY_ETCP, "invalid PONG user_len=%u body=%u from=%s", + ulen, pkt->data_len, sockaddr_storage_to_str(addr).str); + return 7; + } if (ulen > 0 && pkt->data_len >= 20 + ulen) { udata = pkt->data + 20; } diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 822487d0..f5472d47 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -383,7 +383,7 @@ void etcp_link_close(struct ETCP_LINK* link); //int etcp_input_cbk(struct packet_buffer* pkt, struct ETCP_SOCKET* conn);// получает расшифрованный пакет int etcp_encrypt_send(struct ETCP_DGRAM* dgram);// зашифровывает и отправляет пакет // find link by address -struct ETCP_LINK* etcp_link_find_by_addr(struct ETCP_SOCKET* e_sock, struct sockaddr_storage* addr, int is_tcp); +struct ETCP_LINK* etcp_link_find_by_addr(struct ETCP_SOCKET* e_sock, const struct sockaddr_storage* addr, int is_tcp); // Выбор исходящего линка для collision INIT по принимающему сокету и адресу отправителя. // Линк через другой локальный сокет не подходит. NULL — встречной попытки на этой паре нет. diff --git a/src/transport_layer/etcp_keepalive.c b/src/transport_layer/etcp_keepalive.c index dcd11c8b..f2a401f6 100644 --- a/src/transport_layer/etcp_keepalive.c +++ b/src/transport_layer/etcp_keepalive.c @@ -7,6 +7,7 @@ #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/memory_pool.h" +#include #ifdef UTUN_HAVE_STANDBY #include "standby.h" #endif @@ -43,7 +44,9 @@ static void etcp_link_send_keepalive_ex(struct ETCP_LINK* link, uint8_t flags) { DEBUG_TRACE(DEBUG_CATEGORY_KEEPALIVE, ""); if (!link || !link->etcp || !link->etcp->instance) return; - struct ETCP_DGRAM* dgram = u_malloc(sizeof(struct ETCP_DGRAM) + 8); + int padding = etcp_padding_size(&link->etcp->padding, ETCP_PADDING_MAX); + if (padding < 0) return; + struct ETCP_DGRAM* dgram = u_malloc(sizeof(struct ETCP_DGRAM) + 8 + padding); if (!dgram) { DEBUG_ERROR(DEBUG_CATEGORY_KEEPALIVE, "malloc failed"); return; @@ -63,11 +66,15 @@ static void etcp_link_send_keepalive_ex(struct ETCP_LINK* link, uint8_t flags) { } else { dgram->data_len = 4; } + memset(dgram->data + dgram->data_len, 0, padding); + dgram->data_len += padding; dgram->noencrypt_len = 0; dgram->timestamp = get_current_timestamp(); dgram->flag_up = link->is_tcp ? 1 : link->recv_keepalive; link->keepalive_sent_count++; + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive TX link=%u flags=%02x padding=%d body=%u transport=%s", + link->etcp->log_name, link->local_link_id, flags, padding, dgram->data_len, link->is_tcp ? "tcp" : "udp"); etcp_encrypt_send(dgram); u_free(dgram); @@ -485,14 +492,18 @@ int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, uint8_t code = pkt->data[0]; if (code == ETCP_KEEPALIVE) { - uint16_t peer_period = 0; - if (pkt->data_len >= 4) - peer_period = pkt->data[2] | ((uint16_t)pkt->data[3] << 8); - else if (pkt->data_len >= 3) /* легаси-формат (без флагов) */ - peer_period = pkt->data[1] | ((uint16_t)pkt->data[2] << 8); - if (peer_period) link->keepalive_timeout = (uint32_t)peer_period * KA_TIMEOUT_MULT; - uint8_t flags = (pkt->data_len >= 2) ? pkt->data[1] : 0; + size_t body_len = (flags & KA_FLAG_SLEEP) ? 8 : 4; + if (pkt->data_len < body_len || pkt->data_len - body_len > ETCP_PADDING_MAX) { + DEBUG_WARN(DEBUG_CATEGORY_KEEPALIVE, "[%s] invalid keepalive size=%u flags=%02x body=%zu", + link->etcp->log_name, pkt->data_len, flags, body_len); + memory_pool_free(e_sock->instance->pkt_pool, pkt); + return 1; + } + uint16_t peer_period = pkt->data[2] | ((uint16_t)pkt->data[3] << 8); + if (peer_period) link->keepalive_timeout = (uint32_t)peer_period * KA_TIMEOUT_MULT; + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive RX link=%u flags=%02x padding=%zu period=%ums", + link->etcp->log_name, link->local_link_id, flags, pkt->data_len - body_len, peer_period); if (flags & KA_FLAG_SLEEP_ACK) { /* Ответ на мой анонс сна — команда дошла, ретраи сняты, счётчик провалов обнулён. */ diff --git a/src/transport_layer/etcp_keepalive.h b/src/transport_layer/etcp_keepalive.h index f6741848..0494b8bc 100644 --- a/src/transport_layer/etcp_keepalive.h +++ b/src/transport_layer/etcp_keepalive.h @@ -31,6 +31,7 @@ extern "C" { * data[2..3] = ka_period_ms (LE 16-bit) — база dead-link таймаута, мс * data[4..5] = active_remaining (LE 16-bit, x0.1с) — до SLEEP-фазы (0 = уже спим) * data[6..7] = sleep_duration (LE 16-bit, x0.1с) — длительность SLEEP-фазы + * далее = зашифрованный нулевой padding; тело без SLEEP занимает 4 байта, с SLEEP — 8 * * Протокол полностью симметричный (без привязки к STCP-роли is_server): * спящий шлёт анонс KA_FLAG_SLEEP, пир «затыкается» (гасит свой normal-keepalive) diff --git a/src/transport_layer/etcp_padding.c b/src/transport_layer/etcp_padding.c new file mode 100644 index 00000000..c5daf1fa --- /dev/null +++ b/src/transport_layer/etcp_padding.c @@ -0,0 +1,77 @@ +#include "etcp_padding.h" +#include "etcp.h" +#include "utun_instance.h" +#include "../lib/debug_config.h" +#include "../lib/u_async.h" +#include + +static unsigned padding_random(unsigned min, unsigned max) { + return min + (unsigned)rand() % (max - min + 1); +} + +static void padding_generate(struct ETCP_PADDING* padding) { + padding->min = padding_random(0, 30); + padding->max = padding->min + padding_random(32, 128); +} + +static void padding_timer_cb(void* arg); + +static int padding_refresh(struct ETCP_PADDING* padding) { + padding_generate(padding); + unsigned seconds = padding_random(50, 300); + padding->next_refresh_tb = get_time_tb() + (uint64_t)seconds * 10000; + padding->timer = uasync_set_timeout(padding->ua, seconds * 10000, padding, padding_timer_cb, "etcp_padding"); + if (!padding->timer) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] padding=%p: refresh timer allocation failed", padding->name, (void*)padding); + return -1; + } + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] padding=%p: range=%u..%u refresh_in=%us", + padding->name, (void*)padding, padding->min, padding->max, seconds); + return 0; +} + +static void padding_timer_cb(void* arg) { + struct ETCP_PADDING* padding = arg; + padding->timer = NULL; + padding_refresh(padding); +} + +int etcp_padding_start(struct ETCP_PADDING* padding, struct UASYNC* ua, const char* name) { + if (!padding || !ua || padding->timer) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "padding: invalid start padding=%p ua=%p", (void*)padding, (void*)ua); + return -1; + } + padding->ua = ua; + padding->name = name && name[0] ? name : "instance"; + return padding_refresh(padding); +} + +void etcp_padding_stop(struct ETCP_PADDING* padding) { + if (!padding) return; + if (padding->timer) { + uasync_cancel_timeout(padding->ua, padding->timer); + padding->timer = NULL; + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] padding=%p: refresh timer cancelled", padding->name, (void*)padding); + } + padding->ua = NULL; + padding->next_refresh_tb = 0; +} + +int etcp_padding_size(const struct ETCP_PADDING* padding, size_t available) { + struct ETCP_PADDING temporary = {0}; + if (!padding) { padding_generate(&temporary); padding = &temporary; } + if (padding->min > padding->max || padding->max > ETCP_PADDING_MAX || available < padding->min) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "padding: invalid range/capacity min=%u max=%u available=%zu", + padding->min, padding->max, available); + return -1; + } + unsigned max = available < padding->max ? (unsigned)available : padding->max; + return padding_random(padding->min, max); +} + +const struct ETCP_PADDING* etcp_padding_for_peer(struct UTUN_INSTANCE* inst, const uint8_t* public_key) { + if (!inst || !public_key) return NULL; + struct ETCP_CONN* conn = instance_find_conn(inst, sc_derive_node_id_from_pubkey(public_key)); + if (conn && !conn->close_requested) return &conn->padding; + return &inst->padding; +} diff --git a/src/transport_layer/etcp_padding.h b/src/transport_layer/etcp_padding.h new file mode 100644 index 00000000..ddc8a991 --- /dev/null +++ b/src/transport_layer/etcp_padding.h @@ -0,0 +1,36 @@ +/* Независимый диапазон padding владельца; таймер и выбор длины работают в его uasync-потоке. */ +#ifndef ETCP_PADDING_H +#define ETCP_PADDING_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct UASYNC; +struct UTUN_INSTANCE; + +#define ETCP_PADDING_MAX 158 + +struct ETCP_PADDING { + uint16_t min, max; + void* timer; + struct UASYNC* ua; + const char* name; // заимствованное имя владельца для диагностики + uint64_t next_refresh_tb; +}; + +int etcp_padding_start(struct ETCP_PADDING* padding, struct UASYNC* ua, const char* name); +void etcp_padding_stop(struct ETCP_PADDING* padding); +/* NULL допустим только для автономного STCP без instance: диапазон генерируется на операцию. */ +int etcp_padding_size(const struct ETCP_PADDING* padding, size_t available); +/* Существующий незакрываемый ETCP_CONN владеет своим диапазоном; иначе используется instance. */ +const struct ETCP_PADDING* etcp_padding_for_peer(struct UTUN_INSTANCE* inst, const uint8_t* public_key); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/transport_layer/secure_channel.c b/src/transport_layer/secure_channel.c index 6b016cbd..5c4333ce 100644 --- a/src/transport_layer/secure_channel.c +++ b/src/transport_layer/secure_channel.c @@ -107,8 +107,8 @@ static void sc_derive_session_key(const uint8_t *shared_secret, uint8_t *session memcpy(session_key, hash, SC_SESSION_KEY_SIZE); } -// Derive 12-byte nonce for stream cipher from session key + stream_id -static void sc_stream_derive_nonce(const uint8_t *session_key, uint32_t stream_id, uint8_t *nonce_out) { +// Соль отправителя разделяет CTR-потоки повторных и параллельных подключений одной пары узлов. +static void sc_stream_derive_nonce(const uint8_t *session_key, uint32_t stream_id, const uint8_t *salt, uint8_t *nonce_out) { SHA256_CTX sha_ctx; uint8_t hash[SC_HASH_SIZE]; uint8_t stream_id_le[4]; @@ -119,6 +119,7 @@ static void sc_stream_derive_nonce(const uint8_t *session_key, uint32_t stream_i SHA256_Init(&sha_ctx); SHA256_Update(&sha_ctx, session_key, SC_SESSION_KEY_SIZE); SHA256_Update(&sha_ctx, stream_id_le, sizeof(stream_id_le)); + SHA256_Update(&sha_ctx, salt, SC_PUBKEY_ENC_SALT_SIZE); SHA256_Update(&sha_ctx, (const uint8_t *)"uTun3-stream", 12); SHA256_Final(hash, &sha_ctx); memcpy(nonce_out, hash, SC_STREAM_NONCE_SIZE); @@ -354,24 +355,27 @@ sc_status_t sc_compute_public_key_from_private(const uint8_t *private_key, uint8 // --- Streaming cipher (AES-128-CTR) --- -sc_status_t sc_stream_init(sc_context_t *ctx, struct sc_stream_state *state, uint32_t stream_id) { - if (!ctx || !state) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "invalid args"); return SC_ERR_INVALID_ARG; } - if (!ctx->session_ready) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "session not ready"); return SC_ERR_NOT_INITIALIZED; } +sc_status_t sc_stream_init(const uint8_t session_key[SC_SESSION_KEY_SIZE], struct sc_stream_state *state, + uint32_t stream_id, const uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]) { + if (!session_key || !state || !salt || state->initialized) { + DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "stream init: invalid arguments or already initialized, direction=%u", stream_id); + return SC_ERR_INVALID_ARG; + } uint8_t nonce[SC_STREAM_NONCE_SIZE]; - sc_stream_derive_nonce(ctx->session_key, stream_id, nonce); + sc_stream_derive_nonce(session_key, stream_id, salt, nonce); uint8_t iv[16]; memcpy(iv, nonce, SC_STREAM_NONCE_SIZE); memset(iv + SC_STREAM_NONCE_SIZE, 0, 4); EVP_CIPHER_CTX *ectx = EVP_CIPHER_CTX_new(); if (!ectx) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "EVP_CIPHER_CTX_new failed"); return SC_ERR_CRYPTO; } - if (EVP_EncryptInit_ex(ectx, EVP_aes_128_ctr(), NULL, ctx->session_key, iv) != 1) { + if (EVP_EncryptInit_ex(ectx, EVP_aes_128_ctr(), NULL, session_key, iv) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "EVP_EncryptInit_ex failed"); EVP_CIPHER_CTX_free(ectx); return SC_ERR_CRYPTO; } state->ectx = ectx; state->initialized = 1; - DEBUG_INFO(DEBUG_CATEGORY_CRYPTO, "stream_id=%u nonce=%02x%02x%02x%02x...", - stream_id, nonce[0], nonce[1], nonce[2], nonce[3]); + DEBUG_DEBUG(DEBUG_CATEGORY_CRYPTO, "stream init: direction=%u salt=%02x%02x%02x%02x... nonce=%02x%02x%02x%02x...", + stream_id, salt[0], salt[1], salt[2], salt[3], nonce[0], nonce[1], nonce[2], nonce[3]); return SC_OK; } diff --git a/src/transport_layer/secure_channel.h b/src/transport_layer/secure_channel.h index b359dc69..fce55600 100644 --- a/src/transport_layer/secure_channel.h +++ b/src/transport_layer/secure_channel.h @@ -92,7 +92,9 @@ struct sc_stream_state { uint8_t initialized; }; -sc_status_t sc_stream_init(sc_context_t *ctx, struct sc_stream_state *state, uint32_t stream_id); +/* Производный ключ, соль отправителя и направление задают CTR IV. state — нулевой или после cleanup. */ +sc_status_t sc_stream_init(const uint8_t session_key[SC_SESSION_KEY_SIZE], struct sc_stream_state *state, + uint32_t stream_id, const uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]); sc_status_t sc_stream_xor(struct sc_stream_state *state, uint8_t *data, size_t data_len); void sc_stream_cleanup(struct sc_stream_state *state); diff --git a/src/transport_layer/secure_channel_doc.md b/src/transport_layer/secure_channel_doc.md index 9e7b719a..d91259f6 100644 --- a/src/transport_layer/secure_channel_doc.md +++ b/src/transport_layer/secure_channel_doc.md @@ -87,8 +87,10 @@ sc_obfuscate_pubkey(salt, my_pubkey, received_obfuscated, decrypted_pubkey); Для больших объёмов данных, где не нужна посылочная аутентификация: ```c -struct sc_stream_state stream; -sc_stream_init(&ctx, &stream, stream_id); // stream_id — произвольный uint32_t +struct sc_stream_state stream = {0}; +uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]; +if (random_bytes(salt, sizeof(salt)) != 0) return -1; +if (sc_stream_init(ctx.session_key, &stream, stream_id, salt) != SC_OK) return -1; sc_stream_xor(&stream, data_chunk1, len1); // XOR in-place (шифрует/дешифрует) sc_stream_xor(&stream, data_chunk2, len2); @@ -96,7 +98,9 @@ sc_stream_xor(&stream, data_chunk2, len2); sc_stream_cleanup(&stream); ``` -Nonce (12 байт) для CTR выводится из `SHA256(session_key || stream_id_LE || "uTun3-stream")`. +Nonce — первые 12 байт `SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream")`; последние 4 байта IV нулевые. +Получатель использует соль отправителя. Ключ должен быть заранее выведен через ECDH; state нулевой или после cleanup. +Повторная инициализация активного state отклоняется. STCP передаёт соль в существующем открытом блоке handshake. ### 2.4. Ed25519 подписи @@ -233,7 +237,7 @@ sc_sha_transcode(key, key_len, data, data_len); | Функция | Описание | |---|---| -| `sc_stream_init(ctx, state, stream_id)` | Инициализирует CTR-контекст. Nonce из `SHA256(session_key \|\| stream_id_LE \|\| "uTun3-stream")`. | +| `sc_stream_init(session_key, state, stream_id, salt)` | Инициализирует CTR-контекст с выведенным ключом и солью отправителя. Nonce из `SHA256(session_key \|\| stream_id_LE \|\| salt \|\| "uTun3-stream")`. | | `sc_stream_xor(state, data, len)` | XOR данных с keystream in-place. Нулевая длина — no-op. | | `sc_stream_cleanup(state)` | Освобождает `EVP_CIPHER_CTX`. | diff --git a/src/transport_layer/stcp.h b/src/transport_layer/stcp.h index f9307b96..410cd00b 100644 --- a/src/transport_layer/stcp.h +++ b/src/transport_layer/stcp.h @@ -12,6 +12,7 @@ extern "C" { #include "secure_channel.h" +#include "etcp_padding.h" #include "reality.h" #include "crc32.h" #include "../lib/u_async.h" @@ -46,7 +47,7 @@ typedef void (*stcp_ping_cb)(int success, uint16_t rtt, void *arg); // [44..45] keepalive_interval // [46] flags #define STCP_HS_PLAIN_SIZE 47 // без CRC32 -#define STCP_HS_PADDING_MAX 512 // верхняя граница padding_size в рукопожатии (легитимно 8) +#define STCP_HS_PADDING_MAX ETCP_PADDING_MAX #define STCP_HANDSHAKE_FLAG_PING 0x01 @@ -76,7 +77,7 @@ struct stcp_conn { struct UASYNC *ua; void *socket_id; - struct UTUN_INSTANCE *inst; // для lookup ETCP_CONN при хендшейке (сервер) + struct UTUN_INSTANCE *inst; // сервер: диапазон padding и поиск ETCP_CONN по ключу пира struct ETCP_CONN *etcp_conn; // клиент: родительский conn для live-чтения gop/reset_id при handshake enum stcp_state state; diff --git a/src/transport_layer/stcp_client.c b/src/transport_layer/stcp_client.c index 16e02f15..797e9929 100644 --- a/src/transport_layer/stcp_client.c +++ b/src/transport_layer/stcp_client.c @@ -32,6 +32,7 @@ struct stcp_client { uint8_t ping_done; uint8_t peer_pubkey[SC_PUBKEY_SIZE]; uint8_t my_ed25519_pubkey[SC_PUBKEY_SIZE]; + uint16_t handshake_padding; // снимок выбранной длины для одноразового TCP-пинга без владения instance // reality-камуфляж (клиент): перед STCP-хендшейком шлём ClientHello и читаем ServerHello uint8_t reality_enabled; struct reality_client_config reality_cfg; @@ -54,16 +55,20 @@ static int client_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey if (sc_set_peer_public_key(&sc, peer_pubkey, SC_PEER_PUBKEY_BIN) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client ECDH failed"); return -1; } memcpy(c->session_key, sc.session_key, SC_SESSION_KEY_SIZE); memcpy(c->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE); c->peer_pubkey_set = 1; - log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO, "stcp_client session_key", c->session_key, 16); - if (sc_stream_init(&sc, &c->stream_send, STCP_STREAM_CLIENT_SEND) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client stream_send init failed"); return -1; } - if (sc_stream_init(&sc, &c->stream_recv, STCP_STREAM_SERVER_SEND) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client stream_recv init failed"); return -1; } + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp_client: ECDH ready, streams await handshake salts"); return 0; } static void client_send_handshake(struct stcp_conn *c, const uint8_t *server_pubkey, const uint8_t *my_ed25519) { uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]; if (random_bytes(salt, SC_PUBKEY_ENC_SALT_SIZE) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client random_bytes failed"); stcp_conn_do_close(c, 1); return; } - uint16_t padding = 8; + if (sc_stream_init(c->session_key, &c->stream_send, STCP_STREAM_CLIENT_SEND, salt) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client: send stream initialization failed"); + stcp_conn_do_close(c, 2); return; + } + struct stcp_client* cli = (struct stcp_client*)c; + int padding = c->etcp_conn ? etcp_padding_size(&c->etcp_conn->padding, STCP_HS_PADDING_MAX) : cli->handshake_padding; + if (padding < 0) { stcp_conn_do_close(c, 1); return; } size_t total = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + padding; uint8_t *hs = u_malloc(total); if (!hs) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client hs malloc failed"); stcp_conn_do_close(c, 1); return; } @@ -84,7 +89,9 @@ static void client_send_handshake(struct stcp_conn *c, const uint8_t *server_pub memcpy(enc_dst, plain, STCP_HS_PLAIN_SIZE); enc_dst[47] = (uint8_t)(crc >> 0); enc_dst[48] = (uint8_t)(crc >> 8); enc_dst[49] = (uint8_t)(crc >> 16); enc_dst[50] = (uint8_t)(crc >> 24); if (sc_stream_xor(&c->stream_send, enc_dst, STCP_HS_ENC_CLIENT) != SC_OK) { u_free(hs); stcp_conn_do_close(c, 2); return; } - for (int i = 0; i < padding; i++) hs[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + i] = (uint8_t)(salt[0] ^ i); + uint8_t* pad = hs + SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT; + memset(pad, 0, padding); + if (sc_stream_xor(&c->stream_send, pad, padding) != SC_OK) { u_free(hs); stcp_conn_do_close(c, 2); return; } c->hs_send_time = get_time_tb(); c->state = STCP_STATE_HS_CLIENT_SENT; @@ -105,6 +112,10 @@ static void client_hs_cb(struct stcp_conn *c, uint8_t *data, size_t len) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client: server pubkey mismatch — possible MITM"); stcp_conn_do_close(c, 4); return; } + if (sc_stream_init(c->session_key, &c->stream_recv, STCP_STREAM_SERVER_SEND, salt) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client: receive stream initialization failed"); + stcp_conn_do_close(c, 3); return; + } uint8_t enc_hs[STCP_HS_ENC_SERVER]; memcpy(enc_hs, data + SC_PUBKEY_ENC_SIZE, STCP_HS_ENC_SERVER); @@ -130,11 +141,12 @@ static void client_hs_cb(struct stcp_conn *c, uint8_t *data, size_t len) { c->peer_flags = enc_hs[46]; DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: server response OK gop=%d rid=%016llx padding=%u dev=%d ka=%u flags=%02x", c->peer_got_initial_pkt, (unsigned long long)c->peer_reset_id, padding_size, c->peer_device_type, c->peer_keepalive_interval, c->peer_flags); - stcp_recv_set(c, padding_size, 0, client_hs_padding_cb); + if (padding_size) stcp_recv_set(c, padding_size, 0, client_hs_padding_cb); + else client_hs_padding_cb(c, NULL, 0); } static void client_hs_padding_cb(struct stcp_conn *c, uint8_t *data, size_t len) { - (void)data; (void)len; + if (sc_stream_xor(&c->stream_recv, data, len) != SC_OK) { stcp_conn_do_close(c, 3); return; } if (c->hs_timer) { uasync_cancel_timeout(c->ua, c->hs_timer); c->hs_timer = NULL; } if (c->hs_flags & STCP_HANDSHAKE_FLAG_PING) { struct stcp_client *cli = (struct stcp_client *)c; @@ -261,7 +273,8 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin const struct sockaddr_storage *local_addr, int timeout_ms, const struct reality_client_config *reality, - const struct socks_cfg *socks) { + const struct socks_cfg *socks, + struct UTUN_INSTANCE *inst) { if (!ua || !addr || !keys || !peer_pubkey || (!ready_cb && !ping_cb)) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "invalid args"); return NULL; } struct stcp_client *cli = u_calloc(1, sizeof(struct stcp_client)); if (!cli) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "calloc failed"); return NULL; } @@ -270,6 +283,11 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin memcpy(cli->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE); if (my_ed25519_pubkey) memcpy(cli->my_ed25519_pubkey, my_ed25519_pubkey, SC_PUBKEY_SIZE); if (reality) { cli->reality_enabled = 1; cli->reality_cfg = *reality; } + if (!etcp_conn) { + int padding = etcp_padding_size(etcp_padding_for_peer(inst, peer_pubkey), STCP_HS_PADDING_MAX); + if (padding < 0) { u_free(cli); return NULL; } + cli->handshake_padding = padding; + } struct stcp_conn *c = &cli->conn; c->ua = ua; c->state = STCP_STATE_INIT; c->is_server = 0; c->my_keys = *keys; c->on_ready = ready_cb; c->ready_arg = arg; @@ -359,11 +377,12 @@ struct stcp_client *stcp_ping_send(struct UASYNC *ua, const char *addr, uint16_t const uint8_t *my_ed25519_pubkey, uint8_t device_type, uint16_t keepalive_interval, int timeout_ms, stcp_ping_cb cb, void *arg, - const struct socks_cfg *socks) { + const struct socks_cfg *socks, + struct UTUN_INSTANCE *inst) { struct stcp_client *cli = stcp_client_connect(ua, addr, port, keys, peer_pubkey, my_ed25519_pubkey, 0, NULL, device_type, keepalive_interval, STCP_HANDSHAKE_FLAG_PING, - NULL, NULL, cb, arg, ping_close_cb, NULL, NULL, timeout_ms, NULL, socks); + NULL, NULL, cb, arg, ping_close_cb, NULL, NULL, timeout_ms, NULL, socks, inst); if (cli) cli->conn.free_on_close = cli; return cli; } diff --git a/src/transport_layer/stcp_client.h b/src/transport_layer/stcp_client.h index 25078307..4557a7ea 100644 --- a/src/transport_layer/stcp_client.h +++ b/src/transport_layer/stcp_client.h @@ -16,6 +16,7 @@ extern "C" { typedef void (*stcp_ready_cb)(struct stcp_conn *conn, void *arg); typedef void (*stcp_close_cb)(struct stcp_conn *conn, int err, void *arg); +/* inst используется только при запуске для выбора padding без etcp_conn; клиент не хранит ссылку на inst. */ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uint16_t port, struct SC_MYKEYS *keys, const uint8_t *peer_pubkey, const uint8_t *my_ed25519_pubkey, @@ -29,7 +30,8 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin const struct sockaddr_storage *local_addr, int timeout_ms, const struct reality_client_config *reality, - const struct socks_cfg *socks); + const struct socks_cfg *socks, + struct UTUN_INSTANCE *inst); /* TCP-ping: подключается и шлёт хендшейк с флагом STCP_HANDSHAKE_FLAG_PING. * Ответчик отвечает обычным хендшейк-ответом и мягко закрывает соединение. * По завершении хендшейка вызывается cb(success=1, rtt, arg); при ошибке/таймауте — cb(0, 0, arg). */ @@ -38,7 +40,8 @@ struct stcp_client *stcp_ping_send(struct UASYNC *ua, const char *addr, uint16_t const uint8_t *my_ed25519_pubkey, uint8_t device_type, uint16_t keepalive_interval, int timeout_ms, stcp_ping_cb cb, void *arg, - const struct socks_cfg *socks); + const struct socks_cfg *socks, + struct UTUN_INSTANCE *inst); void stcp_client_destroy(struct stcp_client *cli); struct stcp_conn *stcp_client_get_conn(struct stcp_client *cli); diff --git a/src/transport_layer/stcp_client_doc.md b/src/transport_layer/stcp_client_doc.md index 1a91fdb9..454b093a 100644 --- a/src/transport_layer/stcp_client_doc.md +++ b/src/transport_layer/stcp_client_doc.md @@ -12,7 +12,8 @@ stcp_close_cb on_close = (stcp_close_cb)my_close_handler; struct stcp_client *cli = stcp_client_connect( ua, addr, port, &my_keys, peer_pubkey_bin, - on_ready, ready_ctx, on_close, close_ctx + my_ed25519_pubkey, 0, NULL, instance->client_type, instance->keepalive_interval, 0, + on_ready, ready_ctx, NULL, NULL, on_close, close_ctx, NULL, 0, NULL, NULL, instance ); // После handshake — on_ready. // Соединение доступно через stcp_client_get_conn(cli). @@ -22,9 +23,9 @@ struct stcp_client *cli = stcp_client_connect( ``` ### Ключевые нюансы -- **Handshake**: клиент посылает salt(8) + обфусцированный pubkey(XOR с SHA256(salt||server_pubkey), 32) + padding_size(2) + CRC32(4), зашифрованные stream_xor, плюс padding (8+ байт). Сервер присылает аналогичный ответ со статусом. После проверки CRC — переход в `STCP_STATE_DATA`. -- **Стрим-шифр**: используется `sc_stream_xor` с начальным nonce = `SHA256(session_key)`. Два стрима: `STCP_STREAM_CLIENT_SEND` (клиент→сервер) и `STCP_STREAM_SERVER_SEND` (сервер→клиент) — инициализируются в `client_derive_session`. -- **Фрейминг в DATA**: каждое сообщение — `len(2 LE)` + данные + `CRC32(4)`. CRC считается от данных, затем данные+CRC шифруются stream_xor. +- **Handshake**: salt(8) + обфусцированный pubkey(32) открыты; следующие 51 байт содержат Ed25519 pubkey, состояние/эпоху ETCP, padding_size, тип устройства, keepalive, flags и CRC32. Они и следующий нулевой padding шифруются одним CTR-потоком. Длина padding выбирается независимо из диапазона ETCP_CONN, иначе instance; диапазон обновляется своим таймером каждые 50–300 секунд. Приём продвигает CTR через padding, при padding_size=0 сразу завершает handshake. +- **Стрим-шифр**: nonce — первые 12 байт `SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream")`, последние 4 байта CTR IV нулевые. `STCP_STREAM_CLIENT_SEND` использует соль клиента, `STCP_STREAM_SERVER_SEND` — соль сервера. Отправка запускается с собственной солью, приём — после чтения ответа; CTR продолжается через padding и DATA. +- **Фрейминг в DATA**: `len(2 LE)` + данные + `CRC32(4)`. CRC считается от данных, весь кадр вместе с длиной шифруется stream_xor. - **Очереди**: `tx_queue` через callback `client_tx_queue_cb` шифрует каждое сообщение и отправляет. `rx_queue` получает расшифрованные сообщения для вышележащего слоя. - **Асинхронность**: все операции через `UASYNC` — сокет в неблокирующем режиме. Поддержка частичной отправки (send_offset) и EAGAIN/WOULDBLOCK. - **Обработка ошибок**: CRC mismatch, ошибки шифрования, сетевые ошибки — все ведут к `client_do_close` с кодом ошибки. @@ -48,7 +49,7 @@ struct stcp_client *cli = stcp_client_connect( - **`stcp_close_cb`**: вызывается при закрытии или ошибке соединения, получает код ошибки. ### Внутренние функции -- **`client_derive_session()`** — вычисляет session_key через ECDH, инициализирует stream_send/stream_recv. +- **`client_derive_session()`** — вычисляет session_key через ECDH. Потоки инициализируются после получения соответствующих солей. - **`client_send_handshake()`** — формирует и отправляет handshake-пакет (salt + обфусцированный pubkey + зашифрованный padding+CRC + padding). - **`process_server_response()`** — деобфусцирует pubkey сервера, расшифровывает и проверяет CRC ответа сервера. - **`stcp_conn_process_recv()`** — конечный автомат разбора входящих данных: в HS-состоянии ждёт полный ответ, в DATA — разбирает фреймы (len + data + CRC). diff --git a/src/transport_layer/stcp_link.c b/src/transport_layer/stcp_link.c index 6d1387b5..29da9d8b 100644 --- a/src/transport_layer/stcp_link.c +++ b/src/transport_layer/stcp_link.c @@ -254,7 +254,7 @@ struct stcp_link *stcp_link_connect(struct ETCP_LINK *etcp_link, etcp->got_initial_pkt, etcp, inst->client_type, inst->keepalive_interval, 0, - client_ready_cb, link, NULL, NULL, NULL, NULL, local_addr, 0, reality, socks); + client_ready_cb, link, NULL, NULL, NULL, NULL, local_addr, 0, reality, socks, inst); if (!link->cli) { u_free(link); return NULL; } DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_link: connecting to %s:%u pubkey=%016llx bind=%s", diff --git a/src/transport_layer/stcp_server.c b/src/transport_layer/stcp_server.c index 6285a3cf..9e8dc9af 100644 --- a/src/transport_layer/stcp_server.c +++ b/src/transport_layer/stcp_server.c @@ -60,7 +60,7 @@ static void server_data_cb(struct stcp_conn *c, uint8_t *plain_data, size_t data static void reality_ch_hdr_cb(struct stcp_conn *c, uint8_t *data, size_t len); static void reality_ch_body_cb(struct stcp_conn *c, uint8_t *data, size_t len); -static int derive_session_and_streams(struct stcp_conn *c, const uint8_t *peer_pubkey) { +static int server_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey, const uint8_t *salt) { struct secure_channel sc; sc_init_ctx(&sc, &c->my_keys); if (sc_set_peer_public_key(&sc, peer_pubkey, SC_PEER_PUBKEY_BIN) != SC_OK) { @@ -68,17 +68,13 @@ static int derive_session_and_streams(struct stcp_conn *c, const uint8_t *peer_p return -1; } memcpy(c->session_key, sc.session_key, SC_SESSION_KEY_SIZE); - log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO, "stcp_server session_key", c->session_key, 16); - if (sc_stream_init(&sc, &c->stream_send, STCP_STREAM_SERVER_SEND) != SC_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_send init failed"); - return -1; - } - if (sc_stream_init(&sc, &c->stream_recv, STCP_STREAM_CLIENT_SEND) != SC_OK) { + if (sc_stream_init(c->session_key, &c->stream_recv, STCP_STREAM_CLIENT_SEND, salt) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_recv init failed"); return -1; } memcpy(c->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE); c->peer_pubkey_set = 1; + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp_server: ECDH and receive stream ready, send stream awaits response salt"); return 0; } @@ -88,7 +84,7 @@ static void server_hs_phase1_cb(struct stcp_conn *c, uint8_t *data, size_t len) const uint8_t *enc_pubkey = salt + SC_PUBKEY_ENC_SALT_SIZE; uint8_t client_pubkey[SC_PUBKEY_SIZE]; sc_obfuscate_pubkey(salt, c->my_keys.public_key, enc_pubkey, client_pubkey); - if (derive_session_and_streams(c, client_pubkey)) { stcp_conn_do_close(c, 1); return; } + if (server_derive_session(c, client_pubkey, salt)) { stcp_conn_do_close(c, 1); return; } uint8_t enc_hs[STCP_HS_ENC_CLIENT]; memcpy(enc_hs, data + SC_PUBKEY_ENC_SIZE, STCP_HS_ENC_CLIENT); size_t hs_data_len; @@ -112,11 +108,12 @@ static void server_hs_phase1_cb(struct stcp_conn *c, uint8_t *data, size_t len) c->peer_flags = enc_hs[46]; DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: client handshake OK gop=%d rid=%016llx padding=%u dev=%d ka=%u flags=%02x", c->peer_got_initial_pkt, (unsigned long long)c->peer_reset_id, padding_size, c->peer_device_type, c->peer_keepalive_interval, c->peer_flags); - stcp_recv_set(c, padding_size, 0, server_hs_phase2_cb); + if (padding_size) stcp_recv_set(c, padding_size, 0, server_hs_phase2_cb); + else server_hs_phase2_cb(c, NULL, 0); } static void server_hs_phase2_cb(struct stcp_conn *c, uint8_t *data, size_t len) { - (void)data; (void)len; + if (sc_stream_xor(&c->stream_recv, data, len) != SC_OK) { stcp_conn_do_close(c, 2); return; } if (c->hs_timer) { uasync_cancel_timeout(c->ua, c->hs_timer); c->hs_timer = NULL; } @@ -137,7 +134,13 @@ static void server_hs_phase2_cb(struct stcp_conn *c, uint8_t *data, size_t len) DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "random_bytes failed"); stcp_conn_do_close(c, 3); return; } - uint16_t padding = 8; + if (sc_stream_init(c->session_key, &c->stream_send, STCP_STREAM_SERVER_SEND, salt2) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_server: send stream initialization failed"); + stcp_conn_do_close(c, 3); return; + } + const struct ETCP_PADDING* profile = etcp_padding_for_peer(c->inst, c->peer_pubkey); + int padding = etcp_padding_size(profile, STCP_HS_PADDING_MAX); + if (padding < 0) { stcp_conn_do_close(c, 3); return; } size_t total_resp = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + padding; uint8_t *resp = u_malloc(total_resp); if (!resp) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "malloc failed"); stcp_conn_do_close(c, 3); return; } @@ -161,7 +164,12 @@ static void server_hs_phase2_cb(struct stcp_conn *c, uint8_t *data, size_t len) u_free(resp); stcp_conn_do_close(c, 3); return; } log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO, "stcp_server hs_resp AFTER xor", enc_dst, STCP_HS_ENC_SERVER); - for (int i = 0; i < padding; i++) resp[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + i] = (uint8_t)(salt2[0] ^ i); + uint8_t* pad = resp + SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER; + memset(pad, 0, padding); + if (sc_stream_xor(&c->stream_send, pad, padding) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_server: padding encryption failed size=%d", padding); + u_free(resp); stcp_conn_do_close(c, 3); return; + } log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_ETCP, "stcp_server FULL RESP", resp, total_resp); int r = stcp_try_send(c, resp, total_resp); diff --git a/src/transport_layer/stcp_server_doc.md b/src/transport_layer/stcp_server_doc.md index 1af4706a..34967593 100644 --- a/src/transport_layer/stcp_server_doc.md +++ b/src/transport_layer/stcp_server_doc.md @@ -22,7 +22,7 @@ stcp_server_destroy(srv); Ключевые нюансы: - Сервер принимает клиентский pubkey+salt (обфусцированный), извлекает pubkey через `sc_obfuscate_pubkey`, выполняет ECDH. -- Отправляет ответ: статус OK + padding + CRC32, всё зашифровано AES-CTR. +- Отправляет ответ: открытый блок salt/pubkey, зашифрованное тело handshake с CRC32 и зашифрованный padding. - После рукопожатая трафик шифруется потоковым AES-CTR (XOR), для каждого направления — свой stream. - Рукопожатие обрабатывается в два этапа: `process_client_handshake` (извлечение ключа, дешифровка) → `finish_client_handshake` (отправка ответа, переход в DATA). @@ -36,9 +36,9 @@ stcp_server_destroy(srv); - `stcp_server_destroy(srv)` — закрыть слушающий сокет, освободить память. ### Протокол рукопожатия (серверная сторона) -1. Сервер читает `SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT` байт (pubkey+salt + зашифрованные padding_size + CRC32). -2. Деобфусцирует клиентский pubkey (`sc_obfuscate_pubkey`, salt XOR с SHA256(salt||server_pubkey)). -3. Выполняет ECDH (`sc_init_ctx` + `sc_set_peer_public_key`) → получает `session_key`, инициализирует `stream_send`/`stream_recv`. -4. Расшифровывает padding_size, проверяет CRC32, ожидает padding-байты. -5. Формирует ответ: salt2(8) + обфусцированный серверный pubkey(32) + status=OK(1) + padding_size(2) + CRC32(4) + padding(8). Всё кроме salt2 зашифровано `sc_stream_xor`. +1. Сервер читает `SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT` байт (открытый salt/pubkey и 51 байт зашифрованного тела). +2. Деобфусцирует клиентский pubkey через XOR с `SHA256(salt || server_pubkey)`. +3. Выполняет ECDH (`sc_init_ctx` + `sc_set_peer_public_key`) → получает `session_key`, запускает `stream_recv` с клиентской солью. Для ответа генерирует собственную соль и запускает `stream_send` с ней; соль входит в вывод CTR IV вместе с ключом и направлением. +4. Расшифровывает тело, проверяет CRC32 и границы padding_size, затем принимает и расшифровывает padding. +5. Формирует ответ: открытые salt2(8) + обфусцированный pubkey(32), затем зашифрованные 51 байт handshake и нулевой padding случайной длины. Диапазон берётся у существующего ETCP_CONN, иначе у instance. Приём padding продвигает CTR; при длине 0 handshake завершается сразу. 6. Переходит в `STCP_STATE_DATA`, вызывает `on_ready` (== `connect_cb`). diff --git a/src/utun_instance.c b/src/utun_instance.c index da6ce98b..8d813301 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -152,6 +152,7 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u memcpy(instance->my_ed25519_privkey, ed_priv_hash, SC_PRIVKEY_SIZE); } DEBUG_INFO(DEBUG_CATEGORY_CRYPTO, "Ed25519 pubkey derived successfully"); + if (etcp_padding_start(&instance->padding, ua, instance->name) < 0) return -1; // Initialize networks queue (indexed by 56-bit network id) instance->networks = queue_new(ua, 16, offsetof(struct NETWORK_ENTRY, id), 7, "networks"); @@ -328,6 +329,7 @@ struct UTUN_INSTANCE* utun_instance_create(struct UASYNC* ua, const char *config // Initialize using common function if (instance_init_common(instance, ua, config) != 0) { // Cleanup on error + etcp_padding_stop(&instance->padding); if (instance->config) free_config(instance->config); u_free(instance); return NULL; @@ -368,6 +370,7 @@ struct UTUN_INSTANCE* utun_instance_create_from_config(struct UASYNC* ua, struct // Initialize using common function (config ownership transferred to instance) if (instance_init_common(instance, ua, config) != 0) { // Cleanup on error - caller still owns config since we failed + etcp_padding_stop(&instance->padding); u_free(instance); return NULL; } @@ -401,6 +404,7 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { if (instance->ua) uasync_print_resources(instance->ua, "INSTANCE_DESTROY_BEFORE"); instance->running = 0; + etcp_padding_stop(&instance->padding); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] A done — diagnose complete"); /* Phase B: NTP */ diff --git a/src/utun_instance.h b/src/utun_instance.h index 743209c6..ec868226 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -17,6 +17,7 @@ extern "C" { #include "../lib/memory_pool.h" #include "../lib/sqlite3.h" #include "secure_channel.h" +#include "etcp_padding.h" #include "etcp_api.h" #include "config_parser.h" #include "stcp_link.h" @@ -179,6 +180,7 @@ struct UTUN_INSTANCE { // Pending one-shot pings (for callback on PONG or timeout) struct PING_CONTEXT* pending_pings; + struct ETCP_PADDING padding; // диапазон и таймер PING/PONG без ETCP_CONN // Routing statistics uint64_t routed_packets; diff --git a/tests/Makefile.am b/tests/Makefile.am index f5ecea7c..8dd6a8a4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -130,7 +130,7 @@ check_PROGRAMS = \ # Linux-only: test_auto_socket_dynamic требует root + dummy-интерфейсы + iproute2/netlink. if OS_LINUX check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport test_group_connect_standby test_android_udp_log test_pool_corruption_log -check_PROGRAMS += test_uasync_regressions test_ll_queue_regressions +check_PROGRAMS += test_uasync_regressions test_ll_queue_regressions test_etcp_padding endif # Silero VAD: только при включённом ONNX Runtime (--with-silero-vad) @@ -164,6 +164,7 @@ COMMON_LIBS = $(top_builddir)/lib/libuasync.a $(CRYPTO_LIBS) -lpthread $(WIN_LIB # Вариант общего ядра с Android duty-cycle, без JNI и устройства. if OS_LINUX +test_stcp_LDFLAGS = -Wl,--wrap=send -Wl,--wrap=random_bytes standby_test_sources = ../tools/chatgui-android/libutun_lite/standby.c ../src/utun_instance.c ../src/transport_layer/etcp_keepalive.c standby_test_cppflags = $(AM_CPPFLAGS) -I$(top_srcdir)/src/chat -I$(top_srcdir)/tools/chatgui-android/libutun_lite -DUTUN_HAVE_STANDBY test_standby_SOURCES = ../tools/chatgui-android/libutun_lite/tests/test_standby.c $(standby_test_sources) @@ -186,6 +187,9 @@ test_pool_corruption_log_LDADD = $(COMMON_LIBS) async_regression_sources = async_regression.c async_regression.h async_regression_ldflags = -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl \ -Wl,--wrap=gettimeofday -Wl,--wrap=epoll_wait -Wl,--wrap=epoll_ctl -Wl,--wrap=eventfd -Wl,--wrap=epoll_create1 +test_etcp_padding_SOURCES = test_etcp_padding.c +test_etcp_padding_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_etcp_padding_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=socket_sendto test_uasync_regressions_SOURCES = test_uasync_regressions.c $(async_regression_sources) test_uasync_regressions_LDADD = $(COMMON_LIBS) test_uasync_regressions_LDFLAGS = $(async_regression_ldflags) diff --git a/tests/test_etcp_padding.c b/tests/test_etcp_padding.c new file mode 100644 index 00000000..916f8310 --- /dev/null +++ b/tests/test_etcp_padding.c @@ -0,0 +1,259 @@ +/* Padding: реальные таймеры/UDP, проверка расшифрованных пакетов и независимости владельцев. */ +#include +#include +#include +#include "etcp.h" +#include "etcp_keepalive.h" +#include "utun_instance.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL %s:%d: %s\n", __func__, __LINE__, #x); exit(1); } } while (0) + +static struct { struct ETCP_PADDING* padding; timeout_callback_t callback; void* timer; int delay; } timers[128]; +static size_t timer_count; +static int fail_timer; +static struct UTUN_INSTANCE *sender, *receiver; +static sc_context_t wire_crypto; +static int capture_ping, ping_requests, ping_responses, ping_done, expected_req_padding, expected_resp_padding, wire_calls; +static int keepalive_packets, expected_keepalive_padding; +static const uint8_t payload[] = {0, 0xff, 0x12, 0, 0x34, 0x56}; + +void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*); +void* __wrap_uasync_set_timeout(struct UASYNC* ua, int delay, void* arg, timeout_callback_t cb, const char* name) { + int padding = name && strcmp(name, "etcp_padding") == 0; + if (padding && fail_timer) return NULL; + void* timer = __real_uasync_set_timeout(ua, delay, arg, cb, name); + if (padding && timer) { + CHECK(timer_count < sizeof(timers) / sizeof(timers[0])); + CHECK(delay >= 500000 && delay <= 3000000); + timers[timer_count].padding = arg; timers[timer_count].callback = cb; + timers[timer_count].timer = timer; timers[timer_count++].delay = delay; + } + return timer; +} + +static void check_zeroes(const uint8_t* data, size_t size) { + for (size_t i = 0; i < size; i++) CHECK(data[i] == 0); +} + +ssize_t __real_socket_sendto(socket_t, const void*, size_t, const struct sockaddr*, socklen_t); +ssize_t __wrap_socket_sendto(socket_t sock, const void* buf, size_t len, const struct sockaddr* dest, socklen_t dest_len) { + wire_calls++; + if (capture_ping) { + CHECK(len <= PACKET_DATA_MAX_MTU - IP_UDP_OVERHEAD_V4); + CHECK(len >= 96); + const uint8_t* bytes = buf; + uint8_t plain[PACKET_DATA_SIZE]; size_t plain_len = 0; + CHECK(sc_decrypt(&wire_crypto, bytes, len - SC_PUBKEY_ENC_SIZE, plain, &plain_len) == SC_OK); + int code = plain[3]; CHECK(code == ETCP_PING || code == ETCP_PONG); + size_t ulen = ((size_t)plain[21] << 8) | plain[22]; + CHECK(ulen == sizeof(payload) && memcmp(plain + 3 + 20, payload, ulen) == 0); + size_t padding = plain_len - 3 - 20 - ulen; + CHECK(padding == (size_t)(code == ETCP_PING ? expected_req_padding : expected_resp_padding)); + check_zeroes(plain + 3 + 20 + ulen, padding); + if (code == ETCP_PING) ping_requests++; else ping_responses++; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "wire PING/PONG code=%u payload=%zu padding=%zu bytes=%zu", code, ulen, padding, len); + } + return __real_socket_sendto(sock, buf, len, dest, dest_len); +} + +static ssize_t keepalive_hook(socket_t fd, const void* buf, size_t len, const struct sockaddr* addr, socklen_t alen, + struct ETCP_LINK* link, void* arg) { + (void)fd; (void)addr; (void)alen; (void)arg; + uint8_t plain[PACKET_DATA_SIZE]; size_t plain_len = 0; + CHECK(sc_decrypt(&wire_crypto, buf, len, plain, &plain_len) == SC_OK); + CHECK(plain[3] == ETCP_KEEPALIVE && plain[4] == 0); + CHECK(plain_len == 3 + 4 + (size_t)expected_keepalive_padding); + check_zeroes(plain + 3 + 4, expected_keepalive_padding); + struct ETCP_DGRAM* pkt = memory_pool_alloc(sender->pkt_pool); CHECK(pkt); + memcpy(pkt->data, plain + 3, plain_len - 3); pkt->data_len = plain_len - 3; + uint32_t before = link->keepalive_recv_count; + CHECK(etcp_keepalive_on_recv(sender->etcp_sockets, pkt, link, plain_len) == 1); + CHECK(link->keepalive_recv_count == before + 1); + keepalive_packets++; + return len; +} + +static void force_refresh(struct ETCP_PADDING* padding) { + size_t i = timer_count; + while (i && timers[i - 1].timer != padding->timer) i--; + CHECK(i > 0 && timers[i - 1].padding == padding); + CHECK(uasync_cancel_timeout(padding->ua, padding->timer) == 0); + timers[i - 1].callback(padding); + CHECK(padding->timer && padding->min <= 30); + CHECK(padding->max >= padding->min + 32 && padding->max <= padding->min + 128); + uint64_t now = get_time_tb(); + CHECK(padding->next_refresh_tb > now && padding->next_refresh_tb <= now + 3000000); +} + +static struct UTUN_INSTANCE* make_instance(struct UASYNC* ua, const char* private_key, const char* public_key) { + char config[1024]; + snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\nauto_sockets=no\n" + "[server: padding]\naddr=127.0.0.1:0\ntype=public\n", private_key, public_key); + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); CHECK(inst); + CHECK(inst->padding.timer && inst->padding.min <= 30 && inst->padding.max <= ETCP_PADDING_MAX); + return inst; +} + +static struct ETCP_CONN* make_connection(struct UTUN_INSTANCE* inst, const struct SC_MYKEYS* peer) { + struct ETCP_CONN* conn = etcp_connection_create(inst, "padding-test"); CHECK(conn); + conn->peer_node_id = sc_derive_node_id_from_pubkey(peer->public_key); + etcp_conn_queue_set_ready(conn); + CHECK(sc_init_ctx(&conn->crypto_ctx, &inst->my_keys) == SC_OK); + CHECK(sc_set_peer_public_key(&conn->crypto_ctx, peer->public_key, SC_PEER_PUBKEY_BIN) == SC_OK); + CHECK(conn->padding.timer && conn->padding.timer != inst->padding.timer); + return conn; +} + +static void ping_cb(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* data, size_t size) { + (void)rtt; (void)arg; (void)nonce; + CHECK(success && size == sizeof(payload) && memcmp(data, payload, size) == 0); + ping_done = 1; +} + +static void ping_case(int req_padding, int resp_padding) { + expected_req_padding = req_padding; expected_resp_padding = resp_padding; + ping_requests = ping_responses = ping_done = 0; + struct sockaddr_storage addr; socklen_t size = sizeof(addr); + CHECK(getsockname(receiver->etcp_sockets->fd, (struct sockaddr*)&addr, &size) == 0); + capture_ping = 1; + CHECK(etcp_send_ping(sender, receiver->my_keys.public_key, &addr, 500, ping_cb, NULL, payload, sizeof(payload)) == 0); + for (int i = 0; !ping_done && i < 1000; i++) uasync_poll(sender->ua, 10); + capture_ping = 0; + CHECK(ping_done && ping_requests == 1 && ping_responses == 1 && !sender->pending_pings); +} + +/* Слишком длинный user_data и лишний padding отбрасываются до эхо/доставки callback. */ +static void malformed_ping_cases(void) { + for (int code = ETCP_PING; code <= ETCP_PONG; code++) { + for (int extra = 0; extra <= 1; extra++) { + struct sockaddr_storage addr; socklen_t alen = sizeof(addr); + CHECK(getsockname(receiver->etcp_sockets->fd, (struct sockaddr*)&addr, &alen) == 0); + uint8_t plain[3 + 20 + ETCP_PADDING_MAX + 1] = {0}, packet[512]; + plain[3] = code; + plain[22] = extra ? 0 : 1; + struct UTUN_INSTANCE *source = sender, *target = receiver; + if (code == ETCP_PONG) { + /* Битый ответ имеет nonce настоящего ожидающего пинга и приходит раньше корректного ответа. */ + ping_done = 0; + CHECK(etcp_send_ping(sender, receiver->my_keys.public_key, &addr, 500, + ping_cb, NULL, payload, sizeof(payload)) == 0); + CHECK(sender->pending_pings); + uint64_t nonce = sender->pending_pings->nonce; + for (int i = 0; i < 8; i++) plain[3 + 10 + i] = nonce >> (56 - i * 8); + source = receiver; target = sender; + alen = sizeof(addr); + CHECK(getsockname(target->etcp_sockets->fd, (struct sockaddr*)&addr, &alen) == 0); + } + size_t enc_len = 0; + size_t plain_len = extra ? sizeof(plain) : 23; + CHECK(sc_encrypt(&wire_crypto, plain, plain_len, packet, &enc_len) == SC_OK); + memset(packet + enc_len, 0, SC_PUBKEY_ENC_SALT_SIZE); + CHECK(sc_obfuscate_pubkey(packet + enc_len, target->my_keys.public_key, source->my_keys.public_key, + packet + enc_len + SC_PUBKEY_ENC_SALT_SIZE) == SC_OK); + int before = wire_calls; + size_t len = enc_len + SC_PUBKEY_ENC_SIZE; + CHECK(__real_socket_sendto(source->etcp_sockets->fd, packet, len, (const struct sockaddr*)&addr, alen) == (ssize_t)len); + if (code == ETCP_PING) { + for (int i = 0; i < 10; i++) uasync_poll(sender->ua, 10); + CHECK(wire_calls == before); + } else { + for (int i = 0; !ping_done && i < 1000; i++) uasync_poll(sender->ua, 10); + CHECK(ping_done && !sender->pending_pings && wire_calls == before + 1); + } + } + } + puts("PASS malformed PING/PONG: invalid user length and oversized padding rejected"); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO); + debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_KEEPALIVE, DEBUG_LEVEL_DEBUG); + utun_instance_set_tun_init_enabled(0); utun_instance_set_topo_group_enabled(0); + struct UASYNC* ua = uasync_create(); CHECK(ua); + sender = make_instance(ua, "38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68", + "ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a"); + receiver = make_instance(ua, "704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f", + "b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01"); + sc_init_ctx(&wire_crypto, &sender->my_keys); + CHECK(sc_set_peer_public_key(&wire_crypto, receiver->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK); + sender->padding.min = sender->padding.max = 0; + receiver->padding.min = receiver->padding.max = ETCP_PADDING_MAX; + ping_case(0, ETCP_PADDING_MAX); + sender->padding.min = sender->padding.max = ETCP_PADDING_MAX; + receiver->padding.min = receiver->padding.max = 0; + ping_case(ETCP_PADDING_MAX, 0); + puts("PASS stateless PING/PONG: encrypted padding boundaries and byte-exact echo"); + + struct ETCP_CONN* conn = make_connection(sender, &receiver->my_keys); + struct ETCP_CONN* peer_conn = make_connection(receiver, &sender->my_keys); + conn->padding.min = conn->padding.max = 7; + peer_conn->padding.min = peer_conn->padding.max = 13; + CHECK(etcp_padding_for_peer(sender, receiver->my_keys.public_key) == &conn->padding); + ping_case(7, 13); + puts("PASS PING/PONG: connection ranges override independent instance ranges"); + malformed_ping_cases(); + uint8_t large[1370] = {0}; struct sockaddr_storage addr; socklen_t alen = sizeof(addr); + CHECK(getsockname(receiver->etcp_sockets->fd, (struct sockaddr*)&addr, &alen) == 0); + int before = wire_calls; + CHECK(etcp_send_ping(sender, receiver->my_keys.public_key, &addr, 500, ping_cb, NULL, large, sizeof(large)) < 0); + CHECK(wire_calls == before && !sender->pending_pings); + puts("PASS oversized PING: minimum padding must fit MTU, user_data is not truncated"); + + struct ETCP_LINK link = {0}; link.etcp = conn; link.conn = sender->etcp_sockets; + link.mtu = 1280; link.remote_addr.ss_family = AF_INET; link.ka_period_ms = 200; + link.send_hook = keepalive_hook; + for (int i = 0; i < 2; i++) { + expected_keepalive_padding = i ? ETCP_PADDING_MAX : 0; + conn->padding.min = conn->padding.max = expected_keepalive_padding; + etcp_link_send_keepalive(&link); + } + CHECK(keepalive_packets == 2); + struct ETCP_DGRAM* malformed = memory_pool_alloc(sender->pkt_pool); CHECK(malformed); + memset(malformed->data, 0, 7); malformed->data[0] = ETCP_KEEPALIVE; + malformed->data[1] = KA_FLAG_SLEEP; malformed->data_len = 7; + uint32_t received = link.keepalive_recv_count; + CHECK(etcp_keepalive_on_recv(sender->etcp_sockets, malformed, &link, 10) == 1); + CHECK(link.keepalive_recv_count == received && !link.ka_peer_sleeping); + puts("PASS keepalive: encrypted padding boundaries and receive parsing"); + + struct ETCP_CONN* sibling = etcp_connection_create(sender, "padding-sibling"); CHECK(sibling); + struct ETCP_PADDING saved = sender->padding, saved_peer = peer_conn->padding, saved_sibling = sibling->padding; + force_refresh(&conn->padding); + CHECK(memcmp(&saved, &sender->padding, sizeof(saved)) == 0); + CHECK(memcmp(&saved_peer, &peer_conn->padding, sizeof(saved_peer)) == 0); + CHECK(memcmp(&saved_sibling, &sibling->padding, sizeof(saved_sibling)) == 0); + saved = conn->padding; + force_refresh(&sender->padding); + CHECK(memcmp(&saved, &conn->padding, sizeof(saved)) == 0); + int seen[ETCP_PADDING_MAX + 1] = {0}, different = 0; + for (int i = 0; i < 512; i++) { + int size = etcp_padding_size(&conn->padding, ETCP_PADDING_MAX); + CHECK(size >= conn->padding.min && size <= conn->padding.max); + if (!seen[size]++) different++; + } + CHECK(different > 1); + struct ETCP_PADDING boundary = {.min = 10, .max = 20}; + CHECK(etcp_padding_size(&boundary, 9) == -1 && etcp_padding_size(&boundary, 10) == 10); + puts("PASS independent refresh timers, range invariants and MTU capacity"); + + CHECK(etcp_conn_ref_take(conn) == 0); + etcp_connection_close(conn); CHECK(!conn->padding.timer); + CHECK(etcp_padding_for_peer(sender, receiver->my_keys.public_key) == &sender->padding); + etcp_conn_ref_free(conn); etcp_connection_close(peer_conn); etcp_connection_close(sibling); uasync_poll(ua, 0); + fail_timer = 1; + CHECK(etcp_connection_create(sender, "padding-allocation-failure") == NULL); + size_t allocations = u_get_allocated_count(); + CHECK(utun_instance_create_from_config(ua, sender->config) == NULL); + CHECK(u_get_allocated_count() == allocations); + fail_timer = 0; uasync_poll(ua, 0); + CHECK(!sender->connections->count); + utun_instance_destroy(sender); utun_instance_destroy(receiver); uasync_poll(ua, 0); + CHECK(ua->timer_alloc_count == ua->timer_free_count); + uasync_destroy(ua, 0); + puts("PASS timer cancellation and connection creation failure cleanup"); + return 0; +} diff --git a/tests/test_standby_transport.c b/tests/test_standby_transport.c index 728c2904..8e403a84 100644 --- a/tests/test_standby_transport.c +++ b/tests/test_standby_transport.c @@ -24,10 +24,13 @@ int __wrap_etcp_encrypt_send(struct ETCP_DGRAM* pkt) { int i; for (i = 0; i < 3 && pkt->link != &links[i]; i++); assert(i < 3); + size_t body = (pkt->data[1] & KA_FLAG_SLEEP) ? 8 : 4; + assert(pkt->data_len >= body + pkt->link->etcp->padding.min && pkt->data_len <= body + pkt->link->etcp->padding.max); + for (size_t j = body; j < pkt->data_len; j++) assert(pkt->data[j] == 0); sent[i].count++; sent[i].flags = pkt->data[1]; - sent[i].active = pkt->data_len >= 6 ? pkt->data[4] | ((uint16_t)pkt->data[5] << 8) : 0; - sent[i].sleep = pkt->data_len >= 8 ? pkt->data[6] | ((uint16_t)pkt->data[7] << 8) : 0; + sent[i].active = (pkt->data[1] & KA_FLAG_SLEEP) ? pkt->data[4] | ((uint16_t)pkt->data[5] << 8) : 0; + sent[i].sleep = (pkt->data[1] & KA_FLAG_SLEEP) ? pkt->data[6] | ((uint16_t)pkt->data[7] << 8) : 0; return pkt->data_len; } @@ -71,6 +74,8 @@ static void setup(void) { activity_events = wait_calls = fail_phase_timer = 0; inst.ua = ua; inst.client_type = CLIENT_TYPE_MOBILE; inst.client_activity = CLIENT_ACTIVITY_STANDBY; udp.instance = tcp.instance = &inst; + udp.padding.min = 7; udp.padding.max = 13; + tcp.padding.min = 19; tcp.padding.max = 24; strcpy(udp.log_name, "standby-test-udp"); strcpy(tcp.log_name, "standby-test-tcp"); for (int i = 0; i < 3; i++) { links[i].etcp = i == 2 ? &tcp : &udp; diff --git a/tests/test_stcp.c b/tests/test_stcp.c index 4e88e371..963795ca 100644 --- a/tests/test_stcp.c +++ b/tests/test_stcp.c @@ -3,6 +3,7 @@ #include "stcp_server.h" #include "stcp_client.h" #include "secure_channel.h" +#include "utun_instance.h" #include "../lib/u_async.h" #include "../lib/ll_queue.h" #include "../lib/debug_config.h" @@ -17,6 +18,60 @@ static struct SC_MYKEYS s_keys, c_keys; #define BASE_PORT 23456 +#ifdef __linux__ +#define WIRE_PADDING 32 +#define WIRE_HANDSHAKE_SIZE (STCP_HS_CLIENT_MIN + WIRE_PADDING) +static int capture_wire, salt_number; +static uint16_t capture_port; +static size_t wire_count; +static struct { + uint16_t client_port; + size_t size[2]; + uint8_t bytes[2][WIRE_HANDSHAKE_SIZE]; +} wire[2]; + +int __real_random_bytes(uint8_t*, size_t); +int __wrap_random_bytes(uint8_t* out, size_t len) { + if (capture_wire && len == SC_PUBKEY_ENC_SALT_SIZE) { + memset(out, ++salt_number, len); + return 0; + } + return __real_random_bytes(out, len); +} + +ssize_t __real_send(socket_t, const void*, size_t, int); +ssize_t __wrap_send(socket_t sock, const void* buf, size_t len, int flags) { + if (!capture_wire) return __real_send(sock, buf, len, flags); + struct sockaddr_in local, peer; socklen_t alen = sizeof(local); + if (getsockname(sock, (struct sockaddr*)&local, &alen) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "wire capture: getsockname failed fd=%d", (int)sock); + return -1; + } + alen = sizeof(peer); + if (getpeername(sock, (struct sockaddr*)&peer, &alen) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "wire capture: getpeername failed fd=%d", (int)sock); + return -1; + } + int direction = ntohs(local.sin_port) == capture_port ? STCP_STREAM_SERVER_SEND : STCP_STREAM_CLIENT_SEND; + uint16_t port = direction ? ntohs(peer.sin_port) : ntohs(local.sin_port); + size_t i = 0; + while (i < wire_count && wire[i].client_port != port) i++; + if (i == wire_count) { + if (wire_count >= 2) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "wire capture: unexpected connection port=%u", port); return -1; } + wire[wire_count++].client_port = port; + } + /* Реальные send/recv получают короткие части: соль, тело и padding пересекают границы чтений. */ + ssize_t sent = __real_send(sock, buf, len > 7 ? 7 : len, flags); + if (sent > 0) { + size_t copied = WIRE_HANDSHAKE_SIZE - wire[i].size[direction]; + if (copied > (size_t)sent) copied = sent; + memcpy(wire[i].bytes[direction] + wire[i].size[direction], buf, copied); + wire[i].size[direction] += copied; + } + return sent; +} +#endif + #define TASSERT(cond) do { \ if (!(cond)) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, " FAIL: %s", #cond); return 1; } \ } while(0) @@ -104,7 +159,7 @@ static int test1_sizes(void) { uint16_t port = BASE_PORT + 1; struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); size_t sizes[] = {0, 1, 16, 17, 255, 256, 1000, 65535}; int n_sizes = 8; @@ -144,7 +199,7 @@ static int test2_many(void) { uint16_t port = BASE_PORT + 2; struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int sent = 0, ticks = 0; while (srv.msg_count < 200 && ticks < 200) { @@ -185,7 +240,7 @@ static int test3_wrong_key(void) { struct SC_MYKEYS rogue; TASSERT(sc_generate_keypair(&rogue) == SC_OK); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, rogue.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, rogue.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int ticks = 0; while (ticks < 200) { @@ -210,7 +265,7 @@ static int test4_close(void) { uint16_t port = BASE_PORT + 4; struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int closed = 0, ticks = 0; while (!srv.closed && ticks < 200) { @@ -260,7 +315,7 @@ static int test5_multi(void) { struct stcp_client *clients[NCLI] = {0}; for (int i = 0; i < NCLI; i++) { - clients[i] = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &clip[i], NULL, NULL, peer_close_cb, &clip[i], NULL, 0, NULL, NULL); + clients[i] = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &clip[i], NULL, NULL, peer_close_cb, &clip[i], NULL, 0, NULL, NULL, NULL); TASSERT(clients[i]); } @@ -309,7 +364,7 @@ static int test6_interleaved(void) { uint16_t port = BASE_PORT + 6; struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int round = 0, ticks = 0; while (srv.msg_count < 50 || cli.msg_count < 50) { @@ -343,7 +398,7 @@ static int test7_bulk_4mb(void) { uint16_t port = BASE_PORT + 7; struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); #define N_BULK 64 #define SZ_BULK 65535 @@ -382,7 +437,7 @@ static int test8_srv_recv_close(void) { uint16_t port = BASE_PORT + 8; struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); - struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL); TASSERT(sc); + struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int ticks = 0; while ((!srv.ready || !cli.ready) && ticks < 200) { uasync_poll(ua, 10); ticks++; } @@ -450,9 +505,14 @@ static int build_malicious_handshake(uint16_t padding_size, uint8_t *out) { struct secure_channel sc; sc_init_ctx(&sc, &c_keys); if (sc_set_peer_public_key(&sc, s_keys.public_key, SC_PEER_PUBKEY_BIN) != SC_OK) return -1; - struct sc_stream_state ss; - if (sc_stream_init(&sc, &ss, STCP_STREAM_CLIENT_SEND) != SC_OK) return -1; + struct sc_stream_state ss = {0}; + if (sc_stream_init(sc.session_key, &ss, STCP_STREAM_CLIENT_SEND, salt) != SC_OK) return -1; int rc = sc_stream_xor(&ss, enc, STCP_HS_ENC_CLIENT); + if (rc == SC_OK && padding_size <= STCP_HS_PADDING_MAX) { + uint8_t* pad = out + SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT; + memset(pad, 0, padding_size); + rc = sc_stream_xor(&ss, pad, padding_size); + } sc_stream_cleanup(&ss); if (rc != SC_OK) return -1; @@ -471,7 +531,6 @@ static int test9_raw_handshake_ok(void) { uint8_t buf[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + 8]; TASSERT(build_malicious_handshake(8, buf) == 0); - memset(buf + SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT, 0, 8); // 8 байт паддинга (как легитимный клиент) socket_t s = raw_connect_send(port, buf, sizeof(buf)); TASSERT(s != SOCKET_INVALID); @@ -542,6 +601,135 @@ static int test11_garbage_handshake(void) { return 0; } +/* Границы padding в обе стороны; проверяем продолжение CTR после handshake реальными данными. */ +static int test12_padding_boundaries(void) { + for (int i = 0; i < 2; i++) { + struct UASYNC* ua = uasync_create(); TASSERT(ua); + struct UTUN_INSTANCE si = {0}, ci = {0}; + si.ua = ci.ua = ua; si.my_keys = s_keys; ci.my_keys = c_keys; + si.keepalive_interval = ci.keepalive_interval = 200; + si.padding.min = si.padding.max = i ? ETCP_PADDING_MAX : 0; + ci.padding.min = ci.padding.max = i ? 0 : ETCP_PADDING_MAX; + struct test_peer srv = {0}, cli = {0}; + uint16_t port = BASE_PORT + 12 + i; + struct stcp_server* ss = stcp_server_create(ua, port, &s_keys, NULL, &si, + server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); + struct stcp_client* sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, + NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli, + NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL, NULL, &ci); TASSERT(sc); + for (int j = 0; (!srv.ready || !cli.ready) && j < 1000; j++) uasync_poll(ua, 10); + TASSERT(srv.ready && cli.ready); + const uint8_t data[] = {0, 0xff, 3, 4, 0}; + TASSERT(peer_send(&cli, data, sizeof(data)) == 0 && peer_send(&srv, data, sizeof(data)) == 0); + for (int j = 0; (!srv.msg_count || !cli.msg_count) && j < 1000; j++) uasync_poll(ua, 10); + TASSERT(srv.accum_len == sizeof(data) && cli.accum_len == sizeof(data)); + TASSERT(memcmp(srv.accum, data, sizeof(data)) == 0 && memcmp(cli.accum, data, sizeof(data)) == 0); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "padding boundary: client=%u server=%u byte-exact DATA OK", ci.padding.max, si.padding.max); + peer_cleanup(&srv); peer_cleanup(&cli); + if (srv.conn) stcp_conn_free(srv.conn); + stcp_client_destroy(sc); stcp_server_destroy(ss); uasync_destroy(ua, 1); + } + return 0; +} + +#ifdef __linux__ +/* Два одновременных линка и повторное подключение: поля одинаковы, меняются только соли и шифротекст. */ +static int test13_salted_handshakes(void) { + uint8_t ed_client[32], ed_server[32], history[4][2][WIRE_HANDSHAKE_SIZE]; + TASSERT(sc_derive_ed25519_pubkey(c_keys.private_key, ed_client) == SC_OK); + TASSERT(sc_derive_ed25519_pubkey(s_keys.private_key, ed_server) == SC_OK); + sc_context_t crypto; + sc_init_ctx(&crypto, &c_keys); + TASSERT(sc_set_peer_public_key(&crypto, s_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK); + salt_number = 0; + for (int round = 0; round < 2; round++) { + struct UASYNC* ua = uasync_create(); TASSERT(ua); + struct UTUN_INSTANCE si = {0}, ci = {0}; + si.ua = ci.ua = ua; si.my_keys = s_keys; ci.my_keys = c_keys; + si.keepalive_interval = ci.keepalive_interval = 200; + si.padding.min = si.padding.max = ci.padding.min = ci.padding.max = WIRE_PADDING; + struct test_peer srv[2] = {{0}}, cli[2] = {{0}}; + struct stcp_client* clients[2] = {0}; + g_multi_peers = srv; g_multi_idx = 0; g_multi_max = 2; + capture_port = BASE_PORT + 14 + round; + struct stcp_server* ss = stcp_server_create(ua, capture_port, &s_keys, ed_server, &si, + multi_connect_cb, NULL, NULL, NULL, AF_INET); TASSERT(ss); + memset(wire, 0, sizeof(wire)); wire_count = 0; capture_wire = 1; + for (int i = 0; i < 2; i++) { + clients[i] = stcp_client_connect(ua, "127.0.0.1", capture_port, &c_keys, s_keys.public_key, + ed_client, 0, NULL, 0, 200, 0, client_ready_cb, &cli[i], + NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, &ci); + TASSERT(clients[i]); + } + for (int i = 0; (!srv[0].ready || !srv[1].ready || !cli[0].ready || !cli[1].ready) && i < 1000; i++) { + uasync_poll(ua, 10); + } + TASSERT(srv[0].ready && srv[1].ready && cli[0].ready && cli[1].ready); + TASSERT(wire_count == 2 && g_multi_idx == 2); + for (int i = 0; i < 2; i++) { + TASSERT(memcmp(srv[i].conn->peer_ed25519_pubkey, ed_client, 32) == 0); + TASSERT(memcmp(cli[i].conn->peer_ed25519_pubkey, ed_server, 32) == 0); + for (int direction = 0; direction < 2; direction++) { + TASSERT(wire[i].size[direction] == WIRE_HANDSHAKE_SIZE); + struct sc_stream_state stream = {0}; + TASSERT(sc_stream_init(crypto.session_key, &stream, direction, wire[i].bytes[direction]) == SC_OK); + uint8_t plain[STCP_HS_ENC_CLIENT + WIRE_PADDING]; size_t size = 0; + memcpy(plain, wire[i].bytes[direction] + SC_PUBKEY_ENC_SIZE, sizeof(plain)); + TASSERT(stcp_frame_decrypt(plain, STCP_HS_ENC_CLIENT, &stream, &size) == 0); + TASSERT(size == STCP_HS_PLAIN_SIZE); + TASSERT(memcmp(plain, direction ? ed_server : ed_client, 32) == 0); + TASSERT(sc_stream_xor(&stream, plain + STCP_HS_ENC_CLIENT, WIRE_PADDING) == SC_OK); + sc_stream_cleanup(&stream); + for (size_t p = STCP_HS_ENC_CLIENT; p < sizeof(plain); p++) TASSERT(plain[p] == 0); + size_t current = round * 2 + i; + for (size_t old = 0; old < current; old++) { + TASSERT(memcmp(history[old][direction], wire[i].bytes[direction], SC_PUBKEY_ENC_SALT_SIZE) != 0); + TASSERT(memcmp(history[old][direction] + 40, wire[i].bytes[direction] + 40, 32) != 0); + TASSERT(memcmp(history[old][direction] + 91, wire[i].bytes[direction] + 91, WIRE_PADDING) != 0); + } + memcpy(history[current][direction], wire[i].bytes[direction], WIRE_HANDSHAKE_SIZE); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "wire handshake=%zu direction=%d: fresh identity/padding ciphertext", current, direction); + } + } + const uint8_t data[] = {1, 0, 0xff, 8, 13}; + for (int i = 0; i < 2; i++) { + TASSERT(peer_send(&cli[i], data, sizeof(data)) == 0); + TASSERT(peer_send(&srv[i], data, sizeof(data)) == 0); + } + for (int i = 0; (!srv[0].msg_count || !srv[1].msg_count || !cli[0].msg_count || !cli[1].msg_count) && i < 1000; i++) { + uasync_poll(ua, 10); + } + capture_wire = 0; + for (int i = 0; i < 2; i++) { + TASSERT(srv[i].accum_len == sizeof(data) && cli[i].accum_len == sizeof(data)); + TASSERT(memcmp(srv[i].accum, data, sizeof(data)) == 0 && memcmp(cli[i].accum, data, sizeof(data)) == 0); + peer_cleanup(&srv[i]); peer_cleanup(&cli[i]); + stcp_conn_free(srv[i].conn); stcp_client_destroy(clients[i]); + } + stcp_server_destroy(ss); uasync_destroy(ua, 1); + } + return 0; +} +#endif + +/* Меняем соль и заново маскируем прежний X25519 ключ: ECDH тот же, но CTR обязан отвергнуть тело. */ +static int test14_modified_salt(void) { + struct UASYNC* ua = uasync_create(); TASSERT(ua); + struct test_peer srv = {0}; + uint16_t port = BASE_PORT + 16; + struct stcp_server* ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, + server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); + uint8_t packet[STCP_HS_CLIENT_MIN + 8]; + TASSERT(build_malicious_handshake(8, packet) == 0); + packet[0] ^= 1; + TASSERT(sc_obfuscate_pubkey(packet, s_keys.public_key, c_keys.public_key, packet + SC_PUBKEY_ENC_SALT_SIZE) == SC_OK); + socket_t sock = raw_connect_send(port, packet, sizeof(packet)); TASSERT(sock != SOCKET_INVALID); + for (int i = 0; !srv.closed && i < 1000; i++) uasync_poll(ua, 10); + TASSERT(srv.closed && !srv.ready && srv.close_err == 2); + socket_close_wrapper(sock); stcp_server_destroy(ss); uasync_destroy(ua, 1); + return 0; +} + // ======================= main ======================= int main(void) { @@ -566,6 +754,11 @@ int main(void) { TRUN(test9_raw_handshake_ok); TRUN(test10_oversized_padding); TRUN(test11_garbage_handshake); + TRUN(test12_padding_boundaries); +#ifdef __linux__ + TRUN(test13_salted_handshakes); +#endif + TRUN(test14_modified_salt); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "============================================"); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Results: %d/%d passed", tests_passed, tests_total); diff --git a/tests/test_stcp_link.c b/tests/test_stcp_link.c index 794048b0..8f48789d 100644 --- a/tests/test_stcp_link.c +++ b/tests/test_stcp_link.c @@ -52,7 +52,7 @@ static int test1_basic(void) { struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", BASE_PORT + 1, &c_keys, s_keys.public_key, NULL, 0, NULL, - 0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL); + 0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int ticks = 0; @@ -81,7 +81,7 @@ static int test2_wrong_key(void) { struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", BASE_PORT + 2, &c_keys, rogue.public_key, NULL, 0, NULL, - 0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL); + 0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL); TASSERT(sc); int ticks = 0; diff --git a/tests/test_stcp_ping.c b/tests/test_stcp_ping.c index 51015a37..184caf48 100644 --- a/tests/test_stcp_ping.c +++ b/tests/test_stcp_ping.c @@ -63,6 +63,8 @@ int main(void) { uint8_t srv_pub[SC_PUBKEY_SIZE]; memcpy(srv_pub, srv->my_keys.public_key, SC_PUBKEY_SIZE); + cli->padding.min = cli->padding.max = 0; + srv->padding.min = srv->padding.max = ETCP_PADDING_MAX; /* success path: ping listening STCP server */ struct sockaddr_storage addr; memset(&addr, 0, sizeof(addr)); @@ -77,6 +79,17 @@ int main(void) { if (ping_ok != 1) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "=== FAILED (success path): success=%d rtt=%u ===", ping_ok, ping_rtt); return 1; } DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== success path PASSED: rtt=%u ===", ping_rtt); + /* Обратная граница: нулевой ответ сервера должен завершиться без ожидания следующего пакета. */ + ping_ok = -1; + cli->padding.min = cli->padding.max = ETCP_PADDING_MAX; + srv->padding.min = srv->padding.max = 0; + rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL); + if (rc != 0) return 1; + ticks = 0; + while (ping_ok < 0 && ticks < 5000) { uasync_poll(ua, 10); ticks++; } + if (ping_ok != 1) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "TCP ping zero server padding failed"); return 1; } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== reversed padding boundary PASSED ==="); + /* failure path: port with no listener */ ping_ok = -1; ping_rtt = 0; struct sockaddr_storage bad_addr; memset(&bad_addr, 0, sizeof(bad_addr)); @@ -88,8 +101,17 @@ int main(void) { if (ping_ok != 0) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "=== FAILED (failure path): success=%d rtt=%u rc=%d ===", ping_ok, ping_rtt, rc); return 1; } DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== failure path PASSED: success=%d ===", ping_ok); - srv->running = 0; cli->running = 0; - utun_instance_destroy(srv); utun_instance_destroy(cli); + /* Одноразовый TCP-пинг не удерживает instance: длина выбирается до асинхронного connect. */ + ping_ok = -1; + rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL); + if (rc != 0) return 1; + cli->running = 0; utun_instance_destroy(cli); cli = NULL; + ticks = 0; + while (ping_ok < 0 && ticks < 5000) { uasync_poll(ua, 10); ticks++; } + if (ping_ok != 1) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "TCP ping outliving instance failed"); return 1; } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== TCP ping without retained instance PASSED ==="); + srv->running = 0; + utun_instance_destroy(srv); uasync_destroy(ua, 0); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== PASSED ==="); diff --git a/tests/test_stream_cipher.c b/tests/test_stream_cipher.c index ba25bae4..b0b4fc2d 100644 --- a/tests/test_stream_cipher.c +++ b/tests/test_stream_cipher.c @@ -34,11 +34,13 @@ int main(void) { CHECK(sc_set_peer_public_key(&ctx_a, keys_b.public_key, SC_PEER_PUBKEY_BIN) == SC_OK, "set peer key A<-B"); CHECK(sc_set_peer_public_key(&ctx_b, keys_a.public_key, SC_PEER_PUBKEY_BIN) == SC_OK, "set peer key B<-A"); - struct sc_stream_state send_a, recv_a, send_b, recv_b; - CHECK(sc_stream_init(&ctx_a, &send_a, 0) == SC_OK, "init send_A (id=0)"); - CHECK(sc_stream_init(&ctx_a, &recv_a, 1) == SC_OK, "init recv_A (id=1)"); - CHECK(sc_stream_init(&ctx_b, &recv_b, 0) == SC_OK, "init recv_B (id=0)"); - CHECK(sc_stream_init(&ctx_b, &send_b, 1) == SC_OK, "init send_B (id=1)"); + const uint8_t salt_a[SC_PUBKEY_ENC_SALT_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8}; + const uint8_t salt_b[SC_PUBKEY_ENC_SALT_SIZE] = {8, 7, 6, 5, 4, 3, 2, 1}; + struct sc_stream_state send_a = {0}, recv_a = {0}, send_b = {0}, recv_b = {0}; + CHECK(sc_stream_init(ctx_a.session_key, &send_a, 0, salt_a) == SC_OK, "init send_A (id=0, salt_A)"); + CHECK(sc_stream_init(ctx_a.session_key, &recv_a, 1, salt_b) == SC_OK, "init recv_A (id=1, salt_B)"); + CHECK(sc_stream_init(ctx_b.session_key, &recv_b, 0, salt_a) == SC_OK, "init recv_B (id=0, salt_A)"); + CHECK(sc_stream_init(ctx_b.session_key, &send_b, 1, salt_b) == SC_OK, "init send_B (id=1, salt_B)"); uint8_t original[2048], data[2048]; for (int i = 0; i < (int)sizeof(original); i++) original[i] = (uint8_t)(i * 3 + 17); @@ -109,20 +111,34 @@ int main(void) { // Test 8: error handling CHECK(sc_stream_xor(&send_a, NULL, 0) == SC_OK, "xor len=0 OK"); CHECK(sc_stream_xor(&send_a, data, 0) == SC_OK, "xor data_len=0 OK"); - CHECK(sc_stream_init(&ctx_a, NULL, 0) == SC_ERR_INVALID_ARG, "init NULL state"); - CHECK(sc_stream_init(NULL, &send_a, 0) == SC_ERR_INVALID_ARG, "init NULL ctx"); + CHECK(sc_stream_init(ctx_a.session_key, NULL, 0, salt_a) == SC_ERR_INVALID_ARG, "init NULL state"); + CHECK(sc_stream_init(NULL, &send_a, 0, salt_a) == SC_ERR_INVALID_ARG, "init NULL key"); + CHECK(sc_stream_init(ctx_a.session_key, &send_a, 0, NULL) == SC_ERR_INVALID_ARG, "init NULL salt"); + CHECK(sc_stream_init(ctx_a.session_key, &send_a, 0, salt_a) == SC_ERR_INVALID_ARG, "reject initialized stream reuse"); struct sc_stream_state uninit; memset(&uninit, 0, sizeof(uninit)); CHECK(sc_stream_xor(&uninit, data, 1) == SC_ERR_NOT_INITIALIZED, "xor uninitialized"); - // У ctx без session_ready - sc_context_t no_session_ctx; - struct SC_MYKEYS dummy_keys; - memset(&dummy_keys, 0, sizeof(dummy_keys)); - sc_init_ctx(&no_session_ctx, &dummy_keys); - struct sc_stream_state bad_stream; - CHECK(sc_stream_init(&no_session_ctx, &bad_stream, 0) == SC_ERR_NOT_INITIALIZED, "init without session"); + /* Одинаковые ключ, позиция и текст: отдельно проверяем влияние соли и направления на поток. */ + for (uint32_t direction = 0; direction < 2; direction++) { + struct sc_stream_state first = {0}, repeat = {0}, fresh = {0}, opposite = {0}; + uint8_t one[256] = {0}, same[256] = {0}, other[256] = {0}, reverse[256] = {0}; + CHECK(sc_stream_init(ctx_a.session_key, &first, direction, salt_a) == SC_OK, "init reference stream"); + CHECK(sc_stream_init(ctx_b.session_key, &repeat, direction, salt_a) == SC_OK, "init matching peer stream"); + CHECK(sc_stream_init(ctx_a.session_key, &fresh, direction, salt_b) == SC_OK, "init fresh salt stream"); + CHECK(sc_stream_init(ctx_a.session_key, &opposite, direction ^ 1, salt_a) == SC_OK, "init opposite direction"); + CHECK(sc_stream_xor(&first, one, sizeof(one)) == SC_OK, "reference zero plaintext"); + CHECK(sc_stream_xor(&repeat, same, sizeof(same)) == SC_OK, "matching zero plaintext"); + CHECK(sc_stream_xor(&fresh, other, sizeof(other)) == SC_OK, "fresh salt zero plaintext"); + CHECK(sc_stream_xor(&opposite, reverse, sizeof(reverse)) == SC_OK, "opposite direction zero plaintext"); + CHECK(memcmp(one, same, sizeof(one)) == 0, "peer uses identical key/direction/salt"); + CHECK(memcmp(one, other, 32) != 0, "fresh salt changes first 32 encrypted bytes"); + CHECK(memcmp(one + 51, other + 51, 158) != 0, "fresh salt changes zero padding keystream"); + CHECK(memcmp(one, reverse, sizeof(one)) != 0, "direction separates streams at equal position"); + sc_stream_cleanup(&first); sc_stream_cleanup(&repeat); + sc_stream_cleanup(&fresh); sc_stream_cleanup(&opposite); + } // Cleanup sc_stream_cleanup(&send_a);