Add randomized ETCP padding and isolate STCP CTR streams per connection

This commit is contained in:
evgeny committed 2026-10-07 10:45:42 +02:00
1 parent 1f5b42845b
commit a49cf5434e
31 files changed
+854 -103

No files matched your search

+4
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+34 -6
View File
@@ -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;
}
+1 -1
View File
@@ -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 — встречной попытки на этой паре нет.
+19 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+13 -9
View File
@@ -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;
}
+3 -1
View File
@@ -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);
+8 -4
View File
@@ -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`. |
+3 -2
View File
@@ -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;
+29 -10
View File
@@ -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;
}
+5 -2
View File
@@ -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);
+6 -5
View File
@@ -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).
+1 -1
View File
@@ -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",
+20 -12
View File
@@ -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);
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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;