Browse Source

Add randomized ETCP padding and isolate STCP CTR streams per connection

master
evgeny 5 days ago
parent
commit
a49cf5434e
  1. 2
      build_direct.sh
  2. 27
      doc/etcp_protocol.txt
  3. 17
      doc/tasks.md
  4. 4
      src/Makefile.am
  5. 6
      src/transport_layer/etcp.c
  6. 3
      src/transport_layer/etcp.h
  7. 40
      src/transport_layer/etcp_connections.c
  8. 2
      src/transport_layer/etcp_connections.h
  9. 27
      src/transport_layer/etcp_keepalive.c
  10. 1
      src/transport_layer/etcp_keepalive.h
  11. 77
      src/transport_layer/etcp_padding.c
  12. 36
      src/transport_layer/etcp_padding.h
  13. 22
      src/transport_layer/secure_channel.c
  14. 4
      src/transport_layer/secure_channel.h
  15. 12
      src/transport_layer/secure_channel_doc.md
  16. 5
      src/transport_layer/stcp.h
  17. 39
      src/transport_layer/stcp_client.c
  18. 7
      src/transport_layer/stcp_client.h
  19. 11
      src/transport_layer/stcp_client_doc.md
  20. 2
      src/transport_layer/stcp_link.c
  21. 32
      src/transport_layer/stcp_server.c
  22. 12
      src/transport_layer/stcp_server_doc.md
  23. 4
      src/utun_instance.c
  24. 2
      src/utun_instance.h
  25. 6
      tests/Makefile.am
  26. 259
      tests/test_etcp_padding.c
  27. 9
      tests/test_standby_transport.c
  28. 215
      tests/test_stcp.c
  29. 4
      tests/test_stcp_link.c
  30. 26
      tests/test_stcp_ping.c
  31. 44
      tests/test_stream_cipher.c

2
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

27
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:

17
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 отображается одной горизонтальной прокручиваемой строкой с выделением своего смайлика.
Ядро допускает одну реакцию на сообщение от автора; другой смайлик атомарно заменяет прежний, повторный клик отменяет.

4
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 \

6
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);

3
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 <stdint.h>
@ -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;

40
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;
}

2
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 — встречной попытки на этой паре нет.

27
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 <string.h>
#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) {
/* Ответ на мой анонс сна — команда дошла, ретраи сняты, счётчик провалов обнулён. */

1
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)

77
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 <stdlib.h>
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;
}

36
src/transport_layer/etcp_padding.h

@ -0,0 +1,36 @@
/* Независимый диапазон padding владельца; таймер и выбор длины работают в его uasync-потоке. */
#ifndef ETCP_PADDING_H
#define ETCP_PADDING_H
#include <stddef.h>
#include <stdint.h>
#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

22
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;
}

4
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);

12
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`. |

5
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;

39
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;
}

7
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);

11
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).

2
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",

32
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);

12
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`).

4
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 */

2
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;

6
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)

259
tests/test_etcp_padding.c

@ -0,0 +1,259 @@
/* Padding: реальные таймеры/UDP, проверка расшифрованных пакетов и независимости владельцев. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

9
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;

215
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);

4
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;

26
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 ===");

44
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);

Loading…
Cancel
Save