Browse Source

Protect REALITY STCP traffic with authenticated TLS-like records

master
evgeny 4 days ago
parent
commit
a069d11ef9
  1. 2
      build_direct.sh
  2. 8
      src/Makefile.am
  3. 8
      src/config_parser.c
  4. 2
      src/routing_layer/conn_mgr_monitor.c
  5. 20
      src/routing_layer/route_connectivity.c
  6. 14
      src/routing_layer/topo_node.c
  7. 1
      src/routing_layer/topo_node.h
  8. 4
      src/transport_layer/etcp_connections.c
  9. 2
      src/transport_layer/etcp_connections.h
  10. 442
      src/transport_layer/reality.c
  11. 37
      src/transport_layer/reality.h
  12. 51
      src/transport_layer/reality_fingerprint.c
  13. 394
      src/transport_layer/reality_io.c
  14. 39
      src/transport_layer/reality_io.h
  15. 128
      src/transport_layer/reality_record.c
  16. 41
      src/transport_layer/reality_record.h
  17. 32
      src/transport_layer/reality_relay.c
  18. 5
      src/transport_layer/reality_relay.h
  19. 128
      src/transport_layer/stcp.c
  20. 14
      src/transport_layer/stcp.h
  21. 80
      src/transport_layer/stcp_client.c
  22. 2
      src/transport_layer/stcp_client.h
  23. 81
      src/transport_layer/stcp_server.c
  24. 2
      src/utun_instance.c
  25. 2
      src/utun_instance.h
  26. 28
      tests/Makefile.am
  27. 3
      tests/fixtures/reality_openssl_hello.hex
  28. 112
      tests/test_reality_fallback.c
  29. 24
      tests/test_reality_hello.c
  30. 118
      tests/test_reality_record.c
  31. 147
      tests/test_reality_tls.c
  32. 209
      tests/test_stcp.c
  33. 27
      tests/test_stcp_ping.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_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"
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/reality_record.c transport_layer/reality_io.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

8
src/Makefile.am

@ -44,6 +44,10 @@ utun_CORE_SOURCES = \
transport_layer/etcp_dump.c \
transport_layer/secure_channel.c \
transport_layer/reality.c \
transport_layer/reality_record.c \
transport_layer/reality_record.h \
transport_layer/reality_io.c \
transport_layer/reality_io.h \
transport_layer/reality_fingerprint.c \
transport_layer/reality_relay.c \
transport_layer/crc32.c \
@ -184,6 +188,10 @@ libutun_a_SOURCES = \
transport_layer/etcp_dump.c \
transport_layer/secure_channel.c \
transport_layer/reality.c \
transport_layer/reality_record.c \
transport_layer/reality_record.h \
transport_layer/reality_io.c \
transport_layer/reality_io.h \
transport_layer/reality_fingerprint.c \
transport_layer/reality_relay.c \
transport_layer/crc32.c \

8
src/config_parser.c

@ -778,8 +778,8 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
return 0;
}
if (strcmp(key, "fingerprint") == 0) {
if (strcmp(value, "chrome") == 0) { cli->reality.fingerprint = REALITY_FP_CHROME; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown client reality fingerprint '%s' (valid: chrome)", filename, line_num, value);
if (strcmp(value, "openssl") == 0) { cli->reality.fingerprint = REALITY_FP_OPENSSL; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown client reality fingerprint '%s' (valid: openssl)", filename, line_num, value);
return -1;
}
@ -920,8 +920,8 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
if (strcmp(key, "time_window") == 0) { rc->time_window_sec = atoi(value); return 0; }
if (strcmp(key, "relay_idle_timeout") == 0) { rc->relay_idle_timeout_sec = atoi(value); return 0; }
if (strcmp(key, "fingerprint") == 0) {
if (strcmp(value, "chrome") == 0) { rc->fingerprint = REALITY_FP_CHROME; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown reality fingerprint '%s' (valid: chrome)", filename, line_num, value);
if (strcmp(value, "openssl") == 0) { rc->fingerprint = REALITY_FP_OPENSSL; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown reality fingerprint '%s' (valid: openssl)", filename, line_num, value);
return -1;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown reality option '%s'. Valid: enabled, server_name, dest, short_id, short_ids, public_key, private_key, version, time_window, relay_idle_timeout, fingerprint", filename, line_num, key);

2
src/routing_layer/conn_mgr_monitor.c

@ -197,7 +197,7 @@ void cm_candidate_ping_timer_cb(void* arg) {
struct stcp_client** slot = NULL;
for (uint8_t i = 0; i < CONN_MGR_MAX_CANDIDATES; i++)
if (!mgr->cand_tcp_pings[i]) { slot = &mgr->cand_tcp_pings[i]; break; }
if (slot) etcp_send_tcp_ping(mgr->instance, ni->public_key, &link->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_cand_tcp_ping_cb, slot, slot);
if (slot) etcp_send_tcp_ping(mgr->instance, ni->public_key, &link->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_cand_tcp_ping_cb, slot, slot, link->reality_set ? &link->reality : NULL);
} else
etcp_send_ping(mgr->instance, ni->public_key, &link->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_bg_ping_noop_cb, NULL, NULL, 0);
}

20
src/routing_layer/route_connectivity.c

@ -52,6 +52,8 @@ struct conn_probe_ctx {
uint16_t min_rtt; // min RTT в текущей серии
uint16_t timeout_ms;
void* ping_timer;
uint8_t reality_set;
struct reality_client_config reality;
struct stcp_client* tcp_cli; // хендл влётного TCP-пинга (для отмены), NULL для UDP
};
@ -147,7 +149,7 @@ static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance,
static int conn_probe_send(struct conn_probe_ctx* ctx) {
if (ctx->is_tcp) {
return etcp_send_tcp_ping(ctx->instance, ctx->peer_pubkey, &ctx->target_addr,
ctx->timeout_ms, conn_probe_single_cb, ctx, &ctx->tcp_cli);
ctx->timeout_ms, conn_probe_single_cb, ctx, &ctx->tcp_cli, ctx->reality_set ? &ctx->reality : NULL);
}
struct ETCP_SOCKET* sock = ctx->candidate_sockets[ctx->candidate_index];
return etcp_send_ping_to_socket(ctx->instance, sock, ctx->peer_pubkey,
@ -333,6 +335,14 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G
ctx->timeout_ms = g_probe_timeout_ms; ctx->best_across_sockets = 65535;
if (a->protocol & TOPO_PROTO_TCP) {
const struct TOPO_REALITY_SOCK *rs = topo_node_find_reality_sock(ni, a->socket_id);
if (a->protocol & TOPO_PROTO_REALITY) {
if (!rs) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "probe rejected: REALITY parameters missing for socket=%u", a->socket_id);
u_free(ctx); continue;
}
}
if (rs) { topo_node_reality_config(&ctx->reality, rs); ctx->reality_set = 1; }
ctx->is_tcp = 1;
ctx->candidate_count = 1; ctx->candidate_index = 0; ctx->count_total = 1;
} else {
@ -371,6 +381,14 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G
ctx->timeout_ms = g_probe_timeout_ms; ctx->best_across_sockets = 65535;
if (a6->protocol & TOPO_PROTO_TCP) {
const struct TOPO_REALITY_SOCK *rs = topo_node_find_reality_sock(ni, a6->socket_id);
if (a6->protocol & TOPO_PROTO_REALITY) {
if (!rs) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "probe rejected: REALITY parameters missing for socket=%u", a6->socket_id);
u_free(ctx); continue;
}
}
if (rs) { topo_node_reality_config(&ctx->reality, rs); ctx->reality_set = 1; }
ctx->is_tcp = 1;
ctx->candidate_count = 1; ctx->candidate_index = 0; ctx->count_total = 1;
} else {

14
src/routing_layer/topo_node.c

@ -73,13 +73,17 @@ const struct TOPO_REALITY_SOCK* topo_node_find_reality_sock(const struct TOPO_NO
}
/* Копирует reality-параметры узла в link->reality и поднимает флаг reality_set. */
void topo_node_reality_config(struct reality_client_config *cfg, const struct TOPO_REALITY_SOCK *rs) {
memcpy(cfg->server_static_pubkey, rs->server_pubkey, REALITY_AUTH_KEY_SIZE);
memcpy(cfg->short_id, rs->short_id, REALITY_SHORT_ID_SIZE);
memcpy(cfg->version, rs->version, REALITY_VERSION_SIZE);
snprintf(cfg->server_name, sizeof(cfg->server_name), "%s", rs->server_name);
cfg->fingerprint = REALITY_FP_OPENSSL;
}
void topo_node_apply_reality(struct ETCP_LINK* link, const struct TOPO_REALITY_SOCK* rs) {
if (!link || !rs) return;
memcpy(link->reality.server_static_pubkey, rs->server_pubkey, REALITY_AUTH_KEY_SIZE);
memcpy(link->reality.short_id, rs->short_id, REALITY_SHORT_ID_SIZE);
memcpy(link->reality.version, rs->version, REALITY_VERSION_SIZE);
snprintf(link->reality.server_name, sizeof(link->reality.server_name), "%s", rs->server_name);
link->reality.fingerprint = REALITY_FP_CHROME;
topo_node_reality_config(&link->reality, rs);
link->reality_set = 1;
}

1
src/routing_layer/topo_node.h

@ -289,6 +289,7 @@ static inline const struct TOPO_SUBNET6* topo_v6_subnets(const struct TOPO_NOD
const struct TOPO_REALITY_SOCK* topo_node_find_reality_sock(const struct TOPO_NODE* ni, uint8_t socket_id);
/* Заполнить link->reality из reality-инфо пира (rs из BGP). link->reality_set = 1. */
void topo_node_reality_config(struct reality_client_config *cfg, const struct TOPO_REALITY_SOCK *rs);
void topo_node_apply_reality(struct ETCP_LINK* link, const struct TOPO_REALITY_SOCK* rs);
#ifdef __cplusplus

4
src/transport_layer/etcp_connections.c

@ -1679,7 +1679,7 @@ static void tcp_ping_cb_adapter(int success, uint16_t rtt, void* arg) {
int etcp_send_tcp_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubkey_bin,
const struct sockaddr_storage* addr, int timeout_ms,
etcp_ping_callback_t cb, void* user_arg,
struct stcp_client** out_cli) {
struct stcp_client** out_cli, const struct reality_client_config *reality) {
if (!instance || !peer_pubkey_bin || !addr || timeout_ms <= 0 || !cb) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "bad args: inst=%p pubkey=%p addr=%p timeout=%d cb=%p",
(void*)instance, (void*)peer_pubkey_bin, (void*)addr, timeout_ms, (void*)(uintptr_t)cb);
@ -1718,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);
instance->keepalive_interval, timeout_ms, tcp_ping_cb_adapter, a, socks, instance, reality);
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);

2
src/transport_layer/etcp_connections.h

@ -428,7 +428,7 @@ int etcp_send_ping_to_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET*
int etcp_send_tcp_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubkey_bin,
const struct sockaddr_storage* addr, int timeout_ms,
etcp_ping_callback_t cb, void* user_arg,
struct stcp_client** out_cli);
struct stcp_client** out_cli, const struct reality_client_config *reality);
void etcp_connections_read_callback_socket(socket_t sock, void* arg);

442
src/transport_layer/reality.c

@ -25,6 +25,8 @@
#include <time.h>
#include <openssl/evp.h>
#include <openssl/kdf.h>
#include <openssl/hmac.h>
#include <openssl/crypto.h>
// TLS-константы
#define TLS_RECORD_HANDSHAKE 0x16
@ -43,6 +45,8 @@
#define REALITY_HKDF_INFO "REALITY"
static int session_derive(struct reality_session *s, const uint8_t peer[32]);
// ─── Буфер записи с контролем границ ───
struct rbuf {
uint8_t *p;
@ -89,6 +93,7 @@ out:
}
int reality_generate_keypair(uint8_t priv[32], uint8_t pub[32]) {
if (!priv || !pub) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid keypair output"); return REALITY_ERR_ARG; }
return reality_gen_keypair(priv, pub) == 0 ? REALITY_OK : REALITY_ERR_CRYPTO;
}
@ -114,19 +119,19 @@ void reality_config_set_defaults(struct reality_config *cfg) {
memset(cfg->private_key, 0, sizeof(cfg->private_key));
cfg->has_public_key = 0;
cfg->has_private_key = 0;
cfg->version[0] = 1; cfg->version[1] = 0; cfg->version[2] = 0;
cfg->version[0] = 2; cfg->version[1] = 0; cfg->version[2] = 0;
cfg->time_window_sec = 30;
cfg->relay_idle_timeout_sec = 5;
cfg->fingerprint = REALITY_FP_CHROME;
cfg->fingerprint = REALITY_FP_OPENSSL;
}
void reality_client_config_set_defaults(struct reality_client_config *cfg) {
if (!cfg) return;
memset(cfg->server_static_pubkey, 0, sizeof(cfg->server_static_pubkey));
memset(cfg->short_id, 0, sizeof(cfg->short_id));
cfg->version[0] = 1; cfg->version[1] = 0; cfg->version[2] = 0;
cfg->version[0] = 2; cfg->version[1] = 0; cfg->version[2] = 0;
cfg->server_name[0] = '\0';
cfg->fingerprint = REALITY_FP_CHROME;
cfg->fingerprint = REALITY_FP_OPENSSL;
}
static int reality_x25519(const uint8_t priv[32], const uint8_t pub[32], uint8_t shared[32]) {
@ -149,26 +154,6 @@ out:
return rc;
}
static int reality_hkdf_sha256(const uint8_t *ikm, size_t ikm_len,
const uint8_t *salt, size_t salt_len,
const uint8_t *info, size_t info_len,
uint8_t *out, size_t out_len) {
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
if (!ctx) return -1;
int rc = -1;
if (EVP_PKEY_derive_init(ctx) <= 0) goto out;
if (EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) <= 0) goto out;
if (EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt, (int)salt_len) <= 0) goto out;
if (EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm, (int)ikm_len) <= 0) goto out;
if (EVP_PKEY_CTX_add1_hkdf_info(ctx, info, (int)info_len) <= 0) goto out;
size_t len = out_len;
if (EVP_PKEY_derive(ctx, out, &len) <= 0 || len != out_len) goto out;
rc = 0;
out:
EVP_PKEY_CTX_free(ctx);
return rc;
}
static int reality_aes_gcm_seal(const uint8_t key[32], const uint8_t nonce[12],
const uint8_t *aad, size_t aad_len,
const uint8_t *plain, size_t plain_len,
@ -250,21 +235,45 @@ static int reality_build_client_hello_body(const struct reality_client_config *c
if (rbuf_put(&b, cfg->server_name, name_len) < 0) return REALITY_ERR_ARG;
}
const uint8_t point_formats[] = {0, 11, 0, 4, 3, 0, 1, 2};
if (rbuf_put(&b, point_formats, sizeof(point_formats))) return REALITY_ERR_ARG;
// supported_groups
{
int count = 0;
for (const uint16_t *g = fp->supported_groups; *g != 0; g++) count++;
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_GROUPS) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(2 + count * 2)) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(count * 2)) < 0) return REALITY_ERR_ARG;
for (const uint16_t *g = fp->supported_groups; *g != 0; g++)
if (rbuf_put_u16(&b, *g) < 0) return REALITY_ERR_ARG;
}
const uint8_t ticket[] = {0, 35, 0, 0};
if (rbuf_put(&b, ticket, sizeof(ticket))) return REALITY_ERR_ARG;
// ALPN
{
size_t alpn_list_len = 0;
for (const char *const *a = fp->alpn; *a; a++) alpn_list_len += 1 + strlen(*a);
if (rbuf_put_u16(&b, TLS_EXT_ALPN) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(alpn_list_len + 2)) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)alpn_list_len) < 0) return REALITY_ERR_ARG;
for (const char *const *a = fp->alpn; *a; a++) {
size_t l = strlen(*a);
if (rbuf_put_u8(&b, (uint8_t)l) < 0) return REALITY_ERR_ARG;
if (rbuf_put(&b, *a, l) < 0) return REALITY_ERR_ARG;
}
}
const uint8_t legacy_extensions[] = {0, 22, 0, 0, 0, 23, 0, 0};
if (rbuf_put(&b, legacy_extensions, sizeof(legacy_extensions))) return REALITY_ERR_ARG;
// signature_algorithms
{
int count = 0;
for (const uint16_t *s = fp->sig_algs; *s != 0; s++) count++;
if (rbuf_put_u16(&b, TLS_EXT_SIG_ALGS) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(2 + count * 2)) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(count * 2)) < 0) return REALITY_ERR_ARG;
for (const uint16_t *s = fp->sig_algs; *s != 0; s++)
if (rbuf_put_u16(&b, *s) < 0) return REALITY_ERR_ARG;
@ -290,20 +299,8 @@ static int reality_build_client_hello_body(const struct reality_client_config *c
if (rbuf_put_u16(&b, 32) < 0) return REALITY_ERR_ARG;
if (rbuf_put(&b, eph_pub, 32) < 0) return REALITY_ERR_ARG;
// ALPN
{
size_t alpn_list_len = 0;
for (const char *const *a = fp->alpn; *a; a++) alpn_list_len += 1 + strlen(*a);
if (rbuf_put_u16(&b, TLS_EXT_ALPN) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(alpn_list_len + 2)) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)alpn_list_len) < 0) return REALITY_ERR_ARG;
for (const char *const *a = fp->alpn; *a; a++) {
size_t l = strlen(*a);
if (rbuf_put_u8(&b, (uint8_t)l) < 0) return REALITY_ERR_ARG;
if (rbuf_put(&b, *a, l) < 0) return REALITY_ERR_ARG;
}
}
const uint8_t certificate_compression[] = {0, 27, 0, 5, 4, 0, 1, 0, 3};
if (rbuf_put(&b, certificate_compression, sizeof(certificate_compression))) return REALITY_ERR_ARG;
rbuf_patch_u16(&b, ext_len_pos, (uint16_t)(b.len - ext_len_pos - 2));
*body_len = b.len;
return REALITY_OK;
@ -314,7 +311,7 @@ int reality_client_hello_build(const struct reality_client_config *cfg,
return reality_client_hello_build_at(cfg, (uint32_t)time(NULL), out, out_cap, out_len);
}
int reality_client_hello_build_at(const struct reality_client_config *cfg, uint32_t now,
static int client_hello_build(struct reality_session *session, const struct reality_client_config *cfg, uint32_t now,
uint8_t *out, size_t out_cap, size_t *out_len) {
if (!cfg || !out || !out_len || out_cap < REALITY_MAX_CH_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: invalid args");
@ -325,37 +322,34 @@ int reality_client_hello_build_at(const struct reality_client_config *cfg, uint3
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: unknown fingerprint %d", cfg->fingerprint);
return REALITY_ERR_ARG;
}
if (cfg->server_name[0] == '\0') {
if (cfg->server_name[0] == '\0' || !memchr(cfg->server_name, 0, sizeof(cfg->server_name))) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: empty server_name");
return REALITY_ERR_ARG;
}
// 1. эфемерный ключ + random
uint8_t eph_priv[32], eph_pub[32], random32[32];
uint8_t eph_priv[32] = {0}, eph_pub[32], random32[32], shared[32] = {0}, auth_key[32] = {0}, plaintext[16] = {0};
int rc = REALITY_ERR_CRYPTO;
if (reality_gen_keypair(eph_priv, eph_pub) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: X25519 keygen failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
if (random_bytes(random32, sizeof(random32)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: random_bytes failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
// 2. shared + AuthKey
uint8_t shared[32], auth_key[32];
if (reality_x25519(eph_priv, cfg->server_static_pubkey, shared) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: X25519 derive failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
if (reality_hkdf_sha256(shared, sizeof(shared), random32, 20,
(const uint8_t *)REALITY_HKDF_INFO, sizeof(REALITY_HKDF_INFO) - 1,
auth_key, sizeof(auth_key)) != 0) {
if (reality_kdf(shared, sizeof(shared), random32, 20, REALITY_HKDF_INFO, auth_key, sizeof(auth_key)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: HKDF failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
// 3. plaintext[0:16] = version + reserved + timestamp + short_id
uint8_t plaintext[16];
memcpy(plaintext, cfg->version, 3);
plaintext[3] = 0;
plaintext[4] = (uint8_t)(now >> 24);
@ -365,16 +359,16 @@ int reality_client_hello_build_at(const struct reality_client_config *cfg, uint3
memcpy(plaintext + 8, cfg->short_id, REALITY_SHORT_ID_SIZE);
// 4. собрать body (SessionId = нули)
uint8_t body[REALITY_MAX_CH_SIZE];
uint8_t body[2048];
size_t body_len = 0;
int rc = reality_build_client_hello_body(cfg, fp, random32, eph_pub, body, sizeof(body), &body_len);
rc = reality_build_client_hello_body(cfg, fp, random32, eph_pub, body, sizeof(body), &body_len);
if (rc != REALITY_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: body build failed rc=%d", rc);
return rc;
goto out;
}
// 5. handshake-сообщение = 0x01 + length + body (SessionId на позиции 39..71)
uint8_t hs[REALITY_MAX_CH_SIZE];
uint8_t hs[2048];
size_t hs_len = 4 + body_len;
hs[0] = TLS_HANDSHAKE_CLIENT_HELLO;
hs[1] = (uint8_t)(body_len >> 16);
@ -387,7 +381,7 @@ int reality_client_hello_build_at(const struct reality_client_config *cfg, uint3
if (reality_aes_gcm_seal(auth_key, random32 + 20, hs, hs_len,
plaintext, 16, seal_out, seal_out + 16) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: AES-GCM seal failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
memcpy(hs + 39, seal_out, 32); // SessionId = ciphertext || tag
@ -401,15 +395,21 @@ int reality_client_hello_build_at(const struct reality_client_config *cfg, uint3
memcpy(out + 5, hs, hs_len);
*out_len = total;
DEBUG_INFO(DEBUG_CATEGORY_REALITY,
"client hello built: total=%zu sn=%s ver=%d.%d.%d short_id=%02x%02x%02x%02x%02x%02x%02x%02x",
total, cfg->server_name, cfg->version[0], cfg->version[1], cfg->version[2],
plaintext[8], plaintext[9], plaintext[10], plaintext[11],
plaintext[12], plaintext[13], plaintext[14], plaintext[15]);
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "client auth_key=%02x%02x%02x%02x... shared=%02x%02x%02x%02x...",
auth_key[0], auth_key[1], auth_key[2], auth_key[3],
shared[0], shared[1], shared[2], shared[3]);
return REALITY_OK;
if (session) {
memcpy(session->ephemeral_private, eph_priv, 32);
memcpy(session->auth_key, auth_key, 32);
memcpy(session->session_id, seal_out, 32);
if (reality_transcript_add(session, hs, hs_len)) { rc = REALITY_ERR_CRYPTO; goto out; }
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "client hello built: total=%zu sni=%s version=%u.%u.%u", total,
cfg->server_name, cfg->version[0], cfg->version[1], cfg->version[2]);
rc = REALITY_OK;
out:
OPENSSL_cleanse(eph_priv, sizeof(eph_priv));
OPENSSL_cleanse(shared, sizeof(shared));
OPENSSL_cleanse(auth_key, sizeof(auth_key));
OPENSSL_cleanse(plaintext, sizeof(plaintext));
return rc;
}
// ─── Сервер: разбор ClientHello ───
@ -421,79 +421,84 @@ struct reality_ch_parsed {
};
// Возвращает REALITY_OK / REALITY_ERR_FORMAT.
static uint16_t read_u16(const uint8_t *p) { return ((uint16_t)p[0] << 8) | p[1]; }
static int reality_parse_client_hello(const uint8_t *ch, size_t ch_len,
const uint8_t **hs_out, size_t *hs_len_out,
struct reality_ch_parsed *p) {
if (!ch || ch_len < 5 || ch[0] != TLS_RECORD_HANDSHAKE) return REALITY_ERR_FORMAT;
size_t rec_len = ((size_t)ch[3] << 8) | ch[4];
if (ch_len < 5 + rec_len) return REALITY_ERR_FORMAT;
if (!ch || ch_len < 9 || ch[0] != 22 || ch[1] != 3 || (ch[2] != 1 && ch[2] != 3) ||
read_u16(ch + 3) != ch_len - 5 || ch[5] != 1) return REALITY_ERR_FORMAT;
const uint8_t *hs = ch + 5;
size_t hs_len = rec_len;
if (hs_len < 4 || hs[0] != TLS_HANDSHAKE_CLIENT_HELLO) return REALITY_ERR_FORMAT;
size_t body_len = ((size_t)hs[1] << 16) | ((size_t)hs[2] << 8) | hs[3];
if (hs_len < 4 + body_len) return REALITY_ERR_FORMAT;
const uint8_t *b = hs + 4;
size_t bl = body_len;
if (bl < 2 + 32 + 1) return REALITY_ERR_FORMAT;
b += 2; // legacy_version
p->random = b; b += 32;
uint8_t sid_len = *b; b += 1;
if (sid_len != 32 || bl < 2 + 32 + 1 + 32) return REALITY_ERR_FORMAT;
p->session_id = b; b += 32;
// cipher suites
if (bl < (size_t)(b - (hs + 4)) + 2) return REALITY_ERR_FORMAT;
uint16_t cs_len = (uint16_t)((b[0] << 8) | b[1]); b += 2;
if (bl < (size_t)(b - (hs + 4)) + cs_len + 1) return REALITY_ERR_FORMAT;
b += cs_len; // cipher suites
uint8_t comp_len = *b; b += 1;
if (bl < (size_t)(b - (hs + 4)) + comp_len + 2) return REALITY_ERR_FORMAT;
b += comp_len; // compression
// extensions
uint16_t ext_len = (uint16_t)((b[0] << 8) | b[1]); b += 2;
const uint8_t *ext = b;
size_t remaining = ext_len;
const uint8_t *end = ext + remaining;
if (end > hs + 4 + bl) return REALITY_ERR_FORMAT;
p->key_share = NULL;
while (remaining >= 4) {
uint16_t type = (uint16_t)((ext[0] << 8) | ext[1]);
uint16_t len = (uint16_t)((ext[2] << 8) | ext[3]);
if (remaining < 4 + len) return REALITY_ERR_FORMAT;
if (type == TLS_EXT_KEY_SHARE && len >= 4) {
uint16_t shares_len = (uint16_t)((ext[4] << 8) | ext[5]);
if ((size_t)shares_len > (size_t)(len - 2)) return REALITY_ERR_FORMAT;
const uint8_t *s = ext + 6;
size_t sl = shares_len;
while (sl >= 4) {
uint16_t group = (uint16_t)((s[0] << 8) | s[1]);
uint16_t klen = (uint16_t)((s[2] << 8) | s[3]);
if (sl < 4 + klen) break;
if (group == TLS_GROUP_X25519 && klen == 32) {
p->key_share = s + 4;
break;
size_t n = ch_len - 5;
if (4 + ((size_t)hs[1] << 16) + ((size_t)hs[2] << 8) + hs[3] != n || n < 73 ||
hs[4] != 3 || hs[5] != 3 || hs[38] != 32) return REALITY_ERR_FORMAT;
p->random = hs + 6; p->session_id = hs + 39; p->key_share = NULL;
size_t pos = 71, suites = read_u16(hs + pos);
pos += 2;
if (!suites || suites % 2 || suites > n - pos) return REALITY_ERR_FORMAT;
int cipher_ok = 0, version_ok = 0;
for (size_t i = 0; i < suites; i += 2) if (read_u16(hs + pos + i) == 0x1301) cipher_ok = 1;
pos += suites;
if (n - pos < 4 || hs[pos] != 1 || hs[pos + 1] != 0) return REALITY_ERR_FORMAT;
pos += 2;
size_t extensions = read_u16(hs + pos);
pos += 2;
if (extensions != n - pos) return REALITY_ERR_FORMAT;
uint8_t seen[8192] = {0};
while (pos < n) {
if (n - pos < 4) return REALITY_ERR_FORMAT;
unsigned type = read_u16(hs + pos);
size_t len = read_u16(hs + pos + 2);
pos += 4;
if (len > n - pos || (seen[type / 8] & (1 << (type % 8)))) return REALITY_ERR_FORMAT;
seen[type / 8] |= (1 << (type % 8));
const uint8_t *e = hs + pos;
if (type == TLS_EXT_SUPPORTED_GROUPS || type == TLS_EXT_SIG_ALGS) {
if (len < 4 || read_u16(e) != len - 2 || len % 2) return REALITY_ERR_FORMAT;
} else if (type == TLS_EXT_SUPPORTED_VERSIONS) {
if (len < 3 || e[0] != len - 1 || !(len % 2)) return REALITY_ERR_FORMAT;
for (size_t i = 1; i < len; i += 2) if (read_u16(e + i) == 0x0304) version_ok = 1;
} else if (type == TLS_EXT_KEY_SHARE) {
if (len < 2 || read_u16(e) != len - 2) return REALITY_ERR_FORMAT;
size_t i = 2;
while (i < len) {
if (len - i < 4) return REALITY_ERR_FORMAT;
unsigned group = read_u16(e + i);
size_t size = read_u16(e + i + 2);
i += 4;
if (!size || size > len - i) return REALITY_ERR_FORMAT;
if (group == TLS_GROUP_X25519) {
if (size != 32 || p->key_share) return REALITY_ERR_FORMAT;
p->key_share = e + i;
}
s += 4 + klen;
sl -= 4 + klen;
i += size;
}
} else if (type == TLS_EXT_ALPN) {
if (len < 4 || read_u16(e) != len - 2) return REALITY_ERR_FORMAT;
size_t i = 2;
while (i < len) { size_t size = e[i++]; if (!size || size > len - i) return REALITY_ERR_FORMAT; i += size; }
} else if (type == TLS_EXT_SERVER_NAME) {
if (len < 6 || read_u16(e) != len - 2) return REALITY_ERR_FORMAT;
size_t i = 2;
while (i < len) {
if (len - i < 3) return REALITY_ERR_FORMAT;
size_t size = read_u16(e + i + 1); i += 3;
if (!size || size > len - i) return REALITY_ERR_FORMAT;
i += size;
}
}
ext += 4 + len;
remaining -= 4 + len;
pos += len;
}
*hs_out = hs;
*hs_len_out = hs_len;
if (!cipher_ok || !version_ok) return REALITY_ERR_AUTH;
*hs_out = hs; *hs_len_out = n;
return REALITY_OK;
}
int reality_server_hello_build_at(const struct reality_server_config *cfg, uint32_t now_sec,
static int server_hello_build(struct reality_session *session, const struct reality_server_config *cfg, uint32_t now_sec,
const uint8_t *ch, size_t ch_len,
uint8_t *out, size_t out_cap, size_t *out_len) {
if (!cfg || !ch || !out || !out_len || out_cap < REALITY_MAX_SH_SIZE) {
if (!cfg || !ch || !out || !out_len || out_cap < REALITY_MAX_SH_SIZE || cfg->short_id_count < 1 ||
cfg->short_id_count > REALITY_MAX_SHORT_IDS || cfg->time_window_sec < 0 || cfg->time_window_sec > 3600) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: invalid args");
return REALITY_ERR_ARG;
}
@ -512,30 +517,27 @@ int reality_server_hello_build_at(const struct reality_server_config *cfg, uint3
}
// shared + AuthKey
uint8_t shared[32], auth_key[32];
uint8_t shared[32] = {0}, auth_key[32] = {0}, plain[16] = {0}, eph_priv[32] = {0}, eph_pub[32], srv_random[32];
if (reality_x25519(cfg->static_privkey, p.key_share, shared) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: X25519 derive failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
if (reality_hkdf_sha256(shared, sizeof(shared), p.random, 20,
(const uint8_t *)REALITY_HKDF_INFO, sizeof(REALITY_HKDF_INFO) - 1,
auth_key, sizeof(auth_key)) != 0) {
if (reality_kdf(shared, sizeof(shared), p.random, 20, REALITY_HKDF_INFO, auth_key, sizeof(auth_key)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: HKDF failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
// AAD = hs с обнулённым SessionId [39:71]
uint8_t aad[REALITY_MAX_CH_SIZE];
if (hs_len > sizeof(aad)) return REALITY_ERR_FORMAT;
if (hs_len > sizeof(aad)) { rc = REALITY_ERR_FORMAT; goto out; }
memcpy(aad, hs, hs_len);
memset(aad + 39, 0, 32);
// расшифровать SessionId[0:16]=ciphertext, [16:32]=tag
uint8_t plain[16];
if (reality_aes_gcm_open(auth_key, p.random + 20, aad, hs_len,
p.session_id, 16, p.session_id + 16, plain) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: AES-GCM auth failed");
return REALITY_ERR_AUTH;
rc = REALITY_ERR_AUTH; goto out;
}
// сверка версии
@ -543,7 +545,7 @@ int reality_server_hello_build_at(const struct reality_server_config *cfg, uint3
DEBUG_WARN(DEBUG_CATEGORY_REALITY,
"reality_server_hello_build: version mismatch got=%d.%d.%d res=%d want=%d.%d.%d",
plain[0], plain[1], plain[2], plain[3], cfg->version[0], cfg->version[1], cfg->version[2]);
return REALITY_ERR_AUTH;
rc = REALITY_ERR_AUTH; goto out;
}
// сверка timestamp
uint32_t ts = ((uint32_t)plain[4] << 24) | ((uint32_t)plain[5] << 16) |
@ -553,58 +555,61 @@ int reality_server_hello_build_at(const struct reality_server_config *cfg, uint3
if (diff < -(int64_t)cfg->time_window_sec || diff > (int64_t)cfg->time_window_sec) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: timestamp out of window ts=%u now=%lld diff=%lld",
ts, (long long)now, (long long)diff);
return REALITY_ERR_AUTH;
rc = REALITY_ERR_AUTH; goto out;
}
// сверка short_id
int sid_ok = 0;
for (int i = 0; i < cfg->short_id_count; i++) {
if (memcmp(plain + 8, cfg->short_ids[i], REALITY_SHORT_ID_SIZE) == 0) { sid_ok = 1; break; }
if (CRYPTO_memcmp(plain + 8, cfg->short_ids[i], REALITY_SHORT_ID_SIZE) == 0) { sid_ok = 1; break; }
}
if (!sid_ok) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: short_id not in list");
return REALITY_ERR_AUTH;
rc = REALITY_ERR_AUTH; goto out;
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: auth OK short_id=%02x%02x%02x%02x%02x%02x%02x%02x ts=%u",
plain[8], plain[9], plain[10], plain[11], plain[12], plain[13], plain[14], plain[15], ts);
if (session) {
session->auth_timestamp = ts;
memcpy(session->auth_key, auth_key, 32);
if (reality_transcript_add(session, hs, hs_len)) { rc = REALITY_ERR_CRYPTO; goto out; }
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "client hello authenticated: timestamp=%u", ts);
// ─── собрать ServerHello ───
const struct reality_fingerprint *fp = reality_fingerprint_get(cfg->fingerprint);
if (!fp) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: unknown fingerprint"); return REALITY_ERR_ARG; }
if (!fp) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "unknown server fingerprint"); rc = REALITY_ERR_ARG; goto out; }
uint8_t eph_priv[32], eph_pub[32], srv_random[32];
if (reality_gen_keypair(eph_priv, eph_pub) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: keygen failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
(void)eph_priv;
if (random_bytes(srv_random, sizeof(srv_random)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: random_bytes failed");
return REALITY_ERR_CRYPTO;
rc = REALITY_ERR_CRYPTO; goto out;
}
// ServerHello body
uint8_t sh_body[256];
struct rbuf b = { sh_body, sizeof(sh_body), 0 };
if (rbuf_put_u16(&b, 0x0303) < 0) return REALITY_ERR_ARG; // legacy_version
if (rbuf_put(&b, srv_random, 32) < 0) return REALITY_ERR_ARG; // random
if (rbuf_put_u8(&b, 32) < 0) return REALITY_ERR_ARG; // session_id_echo len
if (rbuf_put(&b, p.session_id, 32) < 0) return REALITY_ERR_ARG; // echo SessionId клиента
if (rbuf_put_u16(&b, fp->server_cipher) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u8(&b, 0) < 0) return REALITY_ERR_ARG; // compression = null
if (rbuf_put_u16(&b, 0x0303) < 0) { rc = REALITY_ERR_ARG; goto out; } // legacy_version
if (rbuf_put(&b, srv_random, 32) < 0) { rc = REALITY_ERR_ARG; goto out; } // random
if (rbuf_put_u8(&b, 32) < 0) { rc = REALITY_ERR_ARG; goto out; } // session_id_echo len
if (rbuf_put(&b, p.session_id, 32) < 0) { rc = REALITY_ERR_ARG; goto out; } // echo SessionId клиента
if (rbuf_put_u16(&b, fp->server_cipher) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put_u8(&b, 0) < 0) { rc = REALITY_ERR_ARG; goto out; } // compression = null
size_t ext_len_pos = b.len;
if (rbuf_put_u16(&b, 0) < 0) return REALITY_ERR_ARG; // placeholder
if (rbuf_put_u16(&b, 0) < 0) { rc = REALITY_ERR_ARG; goto out; } // placeholder
// supported_versions
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_VERSIONS) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 2) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, TLS_VERSION_1_3) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_VERSIONS) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put_u16(&b, 2) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put_u16(&b, TLS_VERSION_1_3) < 0) { rc = REALITY_ERR_ARG; goto out; }
// key_share (server)
if (rbuf_put_u16(&b, TLS_EXT_KEY_SHARE) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 36) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, TLS_GROUP_X25519) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 32) < 0) return REALITY_ERR_ARG;
if (rbuf_put(&b, eph_pub, 32) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, TLS_EXT_KEY_SHARE) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put_u16(&b, 36) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put_u16(&b, TLS_GROUP_X25519) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put_u16(&b, 32) < 0) { rc = REALITY_ERR_ARG; goto out; }
if (rbuf_put(&b, eph_pub, 32) < 0) { rc = REALITY_ERR_ARG; goto out; }
rbuf_patch_u16(&b, ext_len_pos, (uint16_t)(b.len - ext_len_pos - 2));
size_t sh_body_len = b.len;
@ -622,8 +627,18 @@ int reality_server_hello_build_at(const struct reality_server_config *cfg, uint3
memcpy(out + 9, sh_body, sh_body_len);
*out_len = 5 + sh_hs_len;
if (session) {
memcpy(session->ephemeral_private, eph_priv, 32);
if (reality_transcript_add(session, out + 5, sh_hs_len) || session_derive(session, p.key_share)) { rc = REALITY_ERR_CRYPTO; goto out; }
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "server hello built: total=%zu", *out_len);
return REALITY_OK;
rc = REALITY_OK;
out:
OPENSSL_cleanse(shared, sizeof(shared));
OPENSSL_cleanse(auth_key, sizeof(auth_key));
OPENSSL_cleanse(plain, sizeof(plain));
OPENSSL_cleanse(eph_priv, sizeof(eph_priv));
return rc;
}
int reality_server_hello_build(const struct reality_server_config *cfg,
@ -631,3 +646,110 @@ int reality_server_hello_build(const struct reality_server_config *cfg,
uint8_t *out, size_t out_cap, size_t *out_len) {
return reality_server_hello_build_at(cfg, (uint32_t)time(NULL), ch, ch_len, out, out_cap, out_len);
}
int reality_client_hello_build_at(const struct reality_client_config *cfg, uint32_t now,
uint8_t *out, size_t cap, size_t *written) {
return client_hello_build(NULL, cfg, now, out, cap, written);
}
int reality_server_hello_build_at(const struct reality_server_config *cfg, uint32_t now,
const uint8_t *ch, size_t len, uint8_t *out, size_t cap, size_t *written) {
return server_hello_build(NULL, cfg, now, ch, len, out, cap, written);
}
void reality_session_handshake_done(struct reality_session *s) {
EVP_MD_CTX_free(s->transcript);
s->transcript = NULL;
OPENSSL_cleanse(s->finished_send, 32);
OPENSSL_cleanse(s->finished_recv, 32);
}
void reality_session_cleanup(struct reality_session *s) {
if (!s) return;
EVP_MD_CTX_free(s->transcript);
OPENSSL_cleanse(s, sizeof(*s));
}
int reality_transcript_add(struct reality_session *s, const uint8_t *data, size_t len) {
if (!s->transcript) {
s->transcript = EVP_MD_CTX_new();
if (!s->transcript || EVP_DigestInit_ex(s->transcript, EVP_sha256(), NULL) != 1) goto fail;
}
if (EVP_DigestUpdate(s->transcript, data, len) == 1) return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "handshake transcript update failed: size=%zu", len);
return -1;
}
static int transcript_digest(struct reality_session *s, uint8_t out[32]) {
EVP_MD_CTX *copy = EVP_MD_CTX_new();
unsigned n = 0;
int ok = copy && s->transcript && EVP_MD_CTX_copy_ex(copy, s->transcript) == 1 &&
EVP_DigestFinal_ex(copy, out, &n) == 1 && n == 32;
EVP_MD_CTX_free(copy);
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "handshake transcript digest failed");
return ok ? 0 : -1;
}
static int session_derive(struct reality_session *s, const uint8_t peer[32]) {
uint8_t ikm[64], hash[32], master[32], c2s[32], s2c[32];
memcpy(ikm, s->auth_key, 32);
int rc = -1;
if (reality_x25519(s->ephemeral_private, peer, ikm + 32) || transcript_digest(s, hash) ||
reality_kdf(ikm, 64, hash, 32, "uTun-reality-record-v2", master, 32) ||
reality_kdf(master, 32, NULL, 0, "client-traffic", c2s, 32) ||
reality_kdf(master, 32, NULL, 0, "server-traffic", s2c, 32) ||
reality_traffic_init(&s->send, s->is_server ? s2c : c2s) ||
reality_traffic_init(&s->recv, s->is_server ? c2s : s2c) ||
reality_kdf(s->send.secret, 32, NULL, 0, "finished", s->finished_send, 32) ||
reality_kdf(s->recv.secret, 32, NULL, 0, "finished", s->finished_recv, 32)) goto out;
s->keys_ready = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "record keys ready: role=%s", s->is_server ? "server" : "client");
rc = 0;
out:
OPENSSL_cleanse(ikm, sizeof(ikm));
OPENSSL_cleanse(master, sizeof(master));
OPENSSL_cleanse(c2s, sizeof(c2s));
OPENSSL_cleanse(s2c, sizeof(s2c));
OPENSSL_cleanse(s->ephemeral_private, 32);
OPENSSL_cleanse(s->auth_key, 32);
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record key derivation failed");
return rc;
}
int reality_finished(struct reality_session *s, int sending, uint8_t out[32]) {
uint8_t hash[32];
unsigned n = 0;
if (!s->keys_ready || transcript_digest(s, hash) ||
!HMAC(EVP_sha256(), sending ? s->finished_send : s->finished_recv, 32, hash, 32, out, &n) || n != 32) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "Finished calculation failed: sending=%d", sending);
return -1;
}
return 0;
}
int reality_session_client_start(struct reality_session *s, const struct reality_client_config *cfg, uint32_t now,
uint8_t *out, size_t cap, size_t *written) {
if (!s || s->transcript || s->keys_ready) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "client Hello context already used or missing"); return REALITY_ERR_ARG; }
return client_hello_build(s, cfg, now, out, cap, written);
}
int reality_session_server_start(struct reality_session *s, const struct reality_server_config *cfg, uint32_t now,
const uint8_t *ch, size_t len, uint8_t *out, size_t cap, size_t *written) {
if (!s || s->transcript || s->keys_ready) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "server Hello context already used or missing"); return REALITY_ERR_ARG; }
s->is_server = 1;
return server_hello_build(s, cfg, now, ch, len, out, cap, written);
}
int reality_session_client_accept(struct reality_session *s, const uint8_t *sh, size_t len) {
/* Единственный допустимый ServerHello: предложенные AES-128-GCM, X25519 и TLS 1.3. */
if (!s || !sh || !s->transcript || s->keys_ready || len != 127 || sh[0] != 22 || sh[1] != 3 || sh[2] != 3 || sh[3] != 0 || sh[4] != 122 ||
sh[5] != 2 || sh[6] || sh[7] || sh[8] != 118 || sh[9] != 3 || sh[10] != 3 || sh[43] != 32 ||
CRYPTO_memcmp(sh + 44, s->session_id, 32) || sh[76] != 0x13 || sh[77] != 1 || sh[78] ||
sh[79] || sh[80] != 46 || memcmp(sh + 81, "\x00\x2b\x00\x02\x03\x04\x00\x33\x00\x24\x00\x1d\x00\x20", 14)) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "ServerHello validation failed: size=%zu", len);
return REALITY_ERR_FORMAT;
}
if (reality_transcript_add(s, sh + 5, len - 5) || session_derive(s, sh + 95)) return REALITY_ERR_CRYPTO;
return REALITY_OK;
}

37
src/transport_layer/reality.h

@ -6,8 +6,8 @@
// (16 байт) несёт version(3) + reserved(1) + timestamp(4) + ShortId(8).
// Ключ AuthKey = HKDF-SHA256(X25519(eph, static), Random[0:20], "REALITY").
//
// Область применения — «hello-only»: обмениваемся только заголовками, реальную
// взаимную аутентификацию делает штатный STCP-хендшейк поверх.
// После Hello весь STCP передаётся в защищённых TLS-подобных записях.
// Это собственный протокол с TLS-фреймированием; ключи не совместимы с TLS.
#ifndef REALITY_H
#define REALITY_H
@ -17,6 +17,7 @@ extern "C" {
#include <stdint.h>
#include <stddef.h>
#include "reality_record.h"
// Размеры
#define REALITY_SHORT_ID_SIZE 8
@ -25,7 +26,7 @@ extern "C" {
#define REALITY_RANDOM_SIZE 32
#define REALITY_SESSION_ID_SIZE 32
#define REALITY_TAG_SIZE 16
#define REALITY_MAX_CH_SIZE 2048 // максимальный размер ClientHello (TLS record + handshake)
#define REALITY_MAX_CH_SIZE 65535 // предел нормализованного ClientHello после сборки записей
#define REALITY_MAX_SH_SIZE 512 // максимальный размер ServerHello
#define REALITY_MAX_SHORT_IDS 64
#define REALITY_SERVER_NAME_MAX 256
@ -38,9 +39,31 @@ extern "C" {
#define REALITY_ERR_FORMAT -4
// Отпечатки (reality_fingerprint.c)
#define REALITY_FP_CHROME 0
#define REALITY_FP_OPENSSL 0
#define REALITY_FP_COUNT 1
struct reality_client_config;
struct reality_server_config;
/* Контекст одноразового Hello и последующих защищённых записей. */
struct reality_session {
uint8_t ephemeral_private[32], auth_key[32], session_id[32];
uint8_t finished_send[32], finished_recv[32];
struct reality_traffic send, recv;
void *transcript; // EVP_MD_CTX, точные handshake-байты без заголовков записей
uint32_t auth_timestamp;
uint8_t is_server, keys_ready;
};
void reality_session_handshake_done(struct reality_session *s);
void reality_session_cleanup(struct reality_session *s);
int reality_session_client_start(struct reality_session *s, const struct reality_client_config *cfg, uint32_t now,
uint8_t *out, size_t cap, size_t *written);
int reality_session_server_start(struct reality_session *s, const struct reality_server_config *cfg, uint32_t now,
const uint8_t *ch, size_t len, uint8_t *out, size_t cap, size_t *written);
int reality_session_client_accept(struct reality_session *s, const uint8_t *sh, size_t len);
int reality_transcript_add(struct reality_session *s, const uint8_t *data, size_t len);
int reality_finished(struct reality_session *s, int sending, uint8_t out[32]);
// Конфигурация клиента
struct reality_client_config {
uint8_t server_static_pubkey[REALITY_AUTH_KEY_SIZE]; // X25519 static pubkey сервера
@ -56,7 +79,7 @@ struct reality_server_config {
uint8_t short_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE];
int short_id_count;
uint8_t version[REALITY_VERSION_SIZE]; // принимаемая версия
int64_t time_window_sec; // допуск timestamp (антиреплей)
int64_t time_window_sec; // допуск timestamp; replay-кеш находится в instance
int relay_idle_timeout_sec; // idle-timeout релея неавторизованных (0=выкл)
int fingerprint;
};
@ -81,10 +104,10 @@ struct reality_config {
};
// Заполняет cfg->version/fingerprint/time_window значениями по умолчанию
// (version=1.0.0, fingerprint=chrome, time_window=30). Вызывается при инициализации.
// (version=2.0.0, fingerprint=openssl, time_window=30). Вызывается при инициализации.
void reality_config_set_defaults(struct reality_config *cfg);
// Заполняет клиентскую конфигурацию (version=1.0.0, fingerprint=chrome).
// Заполняет клиентскую конфигурацию (version=2.0.0, fingerprint=openssl).
// Вызывается при парсинге reality-ключей в секции [client].
void reality_client_config_set_defaults(struct reality_client_config *cfg);

51
src/transport_layer/reality_fingerprint.c

@ -1,47 +1,16 @@
// reality_fingerprint.c — статические отпечатки для REALITY-камуфляжа
/* Профиль OpenSSL 3.5.5: TLS 1.3, X25519, AES-128-GCM, ALPN http/1.1.
* Эталон и команда воспроизведения: tests/fixtures/reality_openssl_hello.hex. */
#include "reality_fingerprint.h"
#include "reality.h"
#include <stddef.h>
// Chrome-like TLS 1.3 отпечаток (упрощённый, но валидный набор).
// Cipher suites: 3 × TLS 1.3 + несколько legacy ECDHE-суитов для правдоподобия.
static const uint16_t fp_chrome_cipher_suites[] = {
0x1301, 0x1302, 0x1303, // TLS_AES_128/256_GCM, CHACHA20
0xc02c, 0xc02b, 0xc030, 0xc02f, // ECDHE-ECDSA/RSA AES-256/128-GCM
0xcca9, 0xcca8, // ECDHE-ECDSA/RSA CHACHA20-POLY1305
0x0000
static const uint16_t suites[] = {0x1301, 0};
static const uint16_t groups[] = {0x001d, 0};
static const uint16_t signatures[] = {
0x0905, 0x0906, 0x0904, 0x0403, 0x0503, 0x0603, 0x0807, 0x0808, 0x081a, 0x081b,
0x081c, 0x0809, 0x080a, 0x080b, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601, 0
};
static const uint16_t fp_chrome_supported_groups[] = {
0x001d, // X25519
0x0017, 0x0018, 0x0019, // secp256r1/384r1/521r1
0x0000
};
static const uint16_t fp_chrome_sig_algs[] = {
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
0x0401, 0x0501, 0x0601, // rsa_pkcs1_sha256/384/512
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
0x0807, 0x0808, // ed25519, ed448
0x0000
};
static const char *const fp_chrome_alpn[] = { "h2", "http/1.1", NULL };
static const struct reality_fingerprint g_fingerprints[REALITY_FP_COUNT] = {
{
.id = REALITY_FP_CHROME,
.name = "chrome",
.cipher_suites = fp_chrome_cipher_suites,
.supported_groups = fp_chrome_supported_groups,
.sig_algs = fp_chrome_sig_algs,
.alpn = fp_chrome_alpn,
.server_cipher = 0x1301, // TLS_AES_128_GCM_SHA256
},
};
static const char *const alpn[] = {"http/1.1", NULL};
static const struct reality_fingerprint profile = {REALITY_FP_OPENSSL, "openssl", suites, groups, signatures, alpn, 0x1301};
const struct reality_fingerprint *reality_fingerprint_get(int id) {
for (int i = 0; i < REALITY_FP_COUNT; i++)
if (g_fingerprints[i].id == id) return &g_fingerprints[i];
return NULL;
return id == REALITY_FP_OPENSSL ? &profile : NULL;
}

394
src/transport_layer/reality_io.c

@ -0,0 +1,394 @@
#define OPENSSL_API_COMPAT 0x10100000L
#include "reality_io.h"
#include "stcp.h"
#include "reality_relay.h"
#include "etcp.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../lib/mem.h"
#include "../lib/platform_compat.h"
#include <string.h>
#include <errno.h>
#include <time.h>
#include <openssl/crypto.h>
#include <openssl/sha.h>
#define REPLAY_CAPACITY 1024
enum reality_phase { WAIT_HELLO, WAIT_SERVER_FLIGHT, WAIT_CLIENT_FINISHED, RECORD_DATA };
struct reality_replay_cache {
struct { uint8_t hash[32]; int64_t expires; } entries[REPLAY_CAPACITY];
};
struct reality_io {
struct reality_session session;
enum reality_phase phase;
struct ETCP_PADDING padding; // снимок для автономного клиента без instance
uint8_t *wire, *handshake;
size_t wire_len, hello_offset, handshake_len;
uint8_t flight_step, ccs_seen, close_sent;
void (*ready)(struct stcp_conn *);
};
static int replay_accept(struct reality_owner *owner, const uint8_t *ch, size_t len, uint32_t now,
uint32_t timestamp, int64_t window) {
if (!owner->replay) owner->replay = u_calloc(1, sizeof(*owner->replay));
if (!owner->replay) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "replay cache allocation failed"); return -1; }
uint8_t hash[32];
if (!SHA256(ch, len, hash)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "replay hash failed"); return -1; }
int slot = -1;
for (unsigned i = 0; i < REPLAY_CAPACITY; i++) {
if (owner->replay->entries[i].expires >= now) {
if (!CRYPTO_memcmp(hash, owner->replay->entries[i].hash, 32)) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "replayed ClientHello rejected");
return -1;
}
} else if (slot < 0) slot = (int)i;
}
if (slot < 0) { DEBUG_WARN(DEBUG_CATEGORY_REALITY, "replay cache full: capacity=%d", REPLAY_CAPACITY); return -1; }
memcpy(owner->replay->entries[slot].hash, hash, 32);
owner->replay->entries[slot].expires = (int64_t)timestamp + window;
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "replay entry stored: slot=%d expires=%lld", slot,
(long long)owner->replay->entries[slot].expires);
return 0;
}
void reality_owner_cleanup(struct reality_owner *owner) {
reality_relay_shutdown(owner);
u_free(owner->replay);
owner->replay = NULL;
}
void reality_io_cleanup(struct stcp_conn *c) {
struct reality_io *io = c->reality_io;
if (!io) return;
c->reality_io = NULL;
reality_session_cleanup(&io->session);
u_free(io->wire);
u_free(io->handshake);
u_free(io);
}
static struct reality_io *io_create(struct stcp_conn *c, void (*ready)(struct stcp_conn *)) {
struct reality_io *io = u_calloc(1, sizeof(*io));
if (!io) goto fail;
io->wire = u_malloc(STCP_RECV_BUF_MAX);
io->handshake = u_malloc(REALITY_MAX_CH_SIZE);
if (!io->wire || !io->handshake) {
u_free(io->wire); u_free(io->handshake); u_free(io);
goto fail;
}
io->ready = ready;
c->reality_io = io;
return io;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record I/O allocation failed");
return NULL;
}
int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now,
const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *)) {
struct reality_io *io = io_create(c, ready);
if (!io) return -1;
if (padding) { io->padding.min = padding->min; io->padding.max = padding->max; }
uint8_t *ch = u_malloc(REALITY_MAX_CH_SIZE);
size_t n = 0;
if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "ClientHello buffer allocation failed"); return -1; }
if (reality_session_client_start(&io->session, cfg, now, ch, REALITY_MAX_CH_SIZE, &n) || stcp_send_raw(c, ch, n) < 0) {
u_free(ch);
return -1;
}
return 0;
}
int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *)) {
struct reality_io *io = io_create(c, ready);
if (!io) return -1;
io->session.is_server = 1;
return 0;
}
static int send_record(struct stcp_conn *c, uint8_t type, const uint8_t *data, size_t len, size_t padding) {
uint8_t *wire = u_malloc(5 + len + 1 + padding + 16);
size_t n = 0;
if (!wire) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record tx allocation failed: content=%zu", len); return -1; }
if (reality_record_seal(&c->reality_io->session.send, type, data, len, padding, wire,
5 + len + 1 + padding + 16, &n) || stcp_send_raw(c, wire, n) < 0) {
u_free(wire);
return -1;
}
return c->send_buf ? 1 : 0;
}
static int send_key_update(struct stcp_conn *c) {
const uint8_t update[] = {24, 0, 0, 1, 0};
if (send_record(c, REALITY_TYPE_HANDSHAKE, update, sizeof(update), 0) < 0) return -1;
return reality_traffic_update(&c->reality_io->session.send);
}
int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len) {
struct reality_io *io = c->reality_io;
if (io->phase != RECORD_DATA || io->close_sent) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "application tx before Finished or after close: phase=%d", io->phase);
return -1;
}
const struct ETCP_PADDING *profile = c->etcp_conn ? &c->etcp_conn->padding :
(c->inst ? etcp_padding_for_peer(c->inst, c->peer_pubkey_set ? c->peer_pubkey : NULL) : &io->padding);
size_t offset = 0;
do {
if (io->session.send.sequence >= REALITY_KEY_LIMIT - 1 && send_key_update(c)) return -1;
int pad = etcp_padding_size(profile, ETCP_PADDING_MAX);
if (pad < 0) return -1;
size_t take = len - offset;
if (take > REALITY_INNER_MAX - 1 - (size_t)pad) take = REALITY_INNER_MAX - 1 - (size_t)pad;
if (send_record(c, REALITY_TYPE_APPLICATION, data + offset, take, pad) < 0) return -1;
offset += take;
} while (offset < len);
u_free(data);
return c->send_buf ? 1 : 0;
}
int reality_io_close_notify(struct stcp_conn *c) {
struct reality_io *io = c->reality_io;
if (io->phase != RECORD_DATA || io->close_sent) return 0;
io->close_sent = 1;
const uint8_t alert[] = {1, 0};
if (io->session.send.sequence >= REALITY_KEY_LIMIT - 1 && send_key_update(c)) return -1;
int rc = send_record(c, REALITY_TYPE_ALERT, alert, sizeof(alert), 0);
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "close_notify queued: pending=%d", rc);
return rc;
}
static int send_ccs(struct stcp_conn *c) {
const uint8_t ccs[] = {20, 3, 3, 0, 1, 1};
uint8_t *wire = u_malloc(sizeof(ccs));
if (!wire) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "CCS allocation failed"); return -1; }
memcpy(wire, ccs, sizeof(ccs));
if (stcp_send_raw(c, wire, sizeof(ccs)) < 0) { u_free(wire); return -1; }
return 0;
}
static int send_finished(struct stcp_conn *c) {
uint8_t finished[36] = {20, 0, 0, 32};
struct reality_session *s = &c->reality_io->session;
if (!s->is_server && send_ccs(c)) return -1;
if (reality_finished(s, 1, finished + 4) || send_record(c, REALITY_TYPE_HANDSHAKE, finished, 36, 0) < 0 ||
reality_transcript_add(s, finished, sizeof(finished))) return -1;
return 0;
}
static int send_server_flight(struct stcp_conn *c) {
/* Размеры записей эталонного single-certificate RSA flight OpenSSL: 43/832/286/58.
* Cover-данные не используются как PKI; их точные байты включаются в Finished. */
const uint8_t ee[] = {8, 0, 0, 17, 0, 15, 0, 16, 0, 11, 0, 9, 8, 'h', 't', 't', 'p', '/', '1', '.', '1'};
uint8_t certificate[810], verify[264];
if (random_bytes(certificate, sizeof(certificate)) || random_bytes(verify, sizeof(verify))) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "cover flight random failed");
return -1;
}
certificate[0] = 11; certificate[1] = 0; certificate[2] = 3; certificate[3] = 38;
verify[0] = 15; verify[1] = 0; verify[2] = 1; verify[3] = 4;
verify[4] = 8; verify[5] = 4; verify[6] = 1; verify[7] = 0;
struct reality_session *session = &c->reality_io->session;
if (send_ccs(c)) return -1;
if (reality_transcript_add(session, ee, sizeof(ee)) || send_record(c, REALITY_TYPE_HANDSHAKE, ee, sizeof(ee), 0) < 0 ||
reality_transcript_add(session, certificate, sizeof(certificate)) ||
send_record(c, REALITY_TYPE_HANDSHAKE, certificate, sizeof(certificate), 0) < 0 ||
reality_transcript_add(session, verify, sizeof(verify)) || send_record(c, REALITY_TYPE_HANDSHAKE, verify, sizeof(verify), 0) < 0 ||
send_finished(c)) return -1;
return 0;
}
static int fallback(struct stcp_conn *c) {
struct reality_io *io = c->reality_io;
if (!io->session.is_server || io->phase != WAIT_HELLO) return -1;
socket_t sock = c->sock;
c->sock = SOCKET_INVALID;
c->socket_id = NULL;
struct reality_owner *owner = c->reality_owner;
reality_relay_start(owner, c->ua, sock, c->reality_dest, io->wire, io->wire_len, c->reality_srv.relay_idle_timeout_sec);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "unauthorized connection transferred to relay: buffered=%zu", io->wire_len);
stcp_conn_do_close(c, ECANCELED);
return -1;
}
static int hello_process(struct stcp_conn *c) {
struct reality_io *io = c->reality_io;
while (io->wire_len - io->hello_offset >= 5) {
const uint8_t *record = io->wire + io->hello_offset;
size_t n = ((size_t)record[3] << 8) | record[4];
if (record[0] != 22 || record[1] != 3 || (record[2] != 3 && (!io->session.is_server || record[2] != 1)) || !n || n > 16384 ||
io->handshake_len + n > REALITY_MAX_CH_SIZE - 5) goto reject;
if (io->wire_len - io->hello_offset < n + 5) return 0;
memcpy(io->handshake + io->handshake_len, record + 5, n);
io->handshake_len += n;
io->hello_offset += n + 5;
if (io->handshake_len < 4) continue;
size_t want = 4 + ((size_t)io->handshake[1] << 16) + ((size_t)io->handshake[2] << 8) + io->handshake[3];
if (want > REALITY_MAX_CH_SIZE - 5 || io->handshake_len > want) goto reject;
if (io->handshake_len < want) continue;
uint8_t *full = u_malloc(want + 5);
if (!full) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "Hello normalization allocation failed"); return -1; }
full[0] = 22; full[1] = 3; full[2] = io->session.is_server ? 1 : 3;
full[3] = (uint8_t)(want >> 8); full[4] = (uint8_t)want;
memcpy(full + 5, io->handshake, want);
int rc;
if (io->session.is_server) {
uint8_t *sh = u_malloc(REALITY_MAX_SH_SIZE);
size_t written = 0;
uint32_t now = c->inst ? (uint32_t)ntp_time_get_seconds(c->inst) : (uint32_t)time(NULL);
if (!sh) DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "ServerHello output allocation failed");
rc = sh ? reality_session_server_start(&io->session, &c->reality_srv, now, full, want + 5,
sh, REALITY_MAX_SH_SIZE, &written) : REALITY_ERR_CRYPTO;
if (!rc && (!c->reality_owner || replay_accept(c->reality_owner, io->handshake, want, now,
io->session.auth_timestamp, c->reality_srv.time_window_sec))) rc = REALITY_ERR_AUTH;
if (rc) { u_free(sh); u_free(full); goto reject; }
io->phase = WAIT_CLIENT_FINISHED; // fallback запрещён с момента постановки собственного ответа
if (stcp_send_raw(c, sh, written) < 0) { u_free(sh); u_free(full); return -1; }
if (send_server_flight(c)) { u_free(full); return -1; }
} else {
rc = reality_session_client_accept(&io->session, full, want + 5);
if (rc) { u_free(full); return -1; }
io->phase = WAIT_SERVER_FLIGHT;
}
u_free(full);
memmove(io->wire, io->wire + io->hello_offset, io->wire_len - io->hello_offset);
io->wire_len -= io->hello_offset;
io->hello_offset = 0;
io->handshake_len = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "Hello complete: role=%s phase=%d", io->session.is_server ? "server" : "client", io->phase);
return 1;
}
return 0;
reject:
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "Hello rejected: buffered=%zu handshake=%zu", io->wire_len, io->handshake_len);
return fallback(c);
}
static int handshake_process(struct stcp_conn *c, const uint8_t *data, size_t len) {
struct reality_io *io = c->reality_io;
if (len > REALITY_MAX_CH_SIZE - io->handshake_len) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "protected handshake too large");
return -1;
}
memcpy(io->handshake + io->handshake_len, data, len);
io->handshake_len += len;
while (io->handshake_len >= 4) {
uint8_t type = io->handshake[0];
size_t n = 4 + ((size_t)io->handshake[1] << 16) + ((size_t)io->handshake[2] << 8) + io->handshake[3];
if (n > REALITY_MAX_CH_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_REALITY, "protected handshake length rejected: %zu", n); return -1; }
if (n > io->handshake_len) return 0;
if (io->phase == RECORD_DATA) {
if (n != 5 || io->handshake_len != 5 || type != 24 || io->handshake[4] != 0 || reality_traffic_update(&io->session.recv)) goto reject;
} else if (type == 20) {
uint8_t expected[32];
if (n != 36 || (io->phase == WAIT_SERVER_FLIGHT && io->flight_step != 3) ||
reality_finished(&io->session, 0, expected) || CRYPTO_memcmp(expected, io->handshake + 4, 32) ||
reality_transcript_add(&io->session, io->handshake, n)) goto reject;
if (io->phase == WAIT_SERVER_FLIGHT && send_finished(c)) return -1;
io->phase = RECORD_DATA;
reality_session_handshake_done(&io->session);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "Finished verified: role=%s; protected STCP enabled", io->session.is_server ? "server" : "client");
io->ready(c);
if (!c->reality_io || c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return -1;
} else {
const uint8_t steps[] = {8, 11, 15};
if (io->phase != WAIT_SERVER_FLIGHT || io->flight_step >= 3 || type != steps[io->flight_step] ||
reality_transcript_add(&io->session, io->handshake, n)) goto reject;
io->flight_step++;
}
memmove(io->handshake, io->handshake + n, io->handshake_len - n);
io->handshake_len -= n;
}
return 0;
reject:
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "protected handshake rejected: phase=%d step=%u type=%u size=%zu", io->phase,
io->flight_step, io->handshake[0], io->handshake_len);
return -1;
}
static int append_application(struct stcp_conn *c, const uint8_t *data, size_t len) {
if (len > STCP_RECV_BUF_MAX - c->recv_buf_len) { DEBUG_WARN(DEBUG_CATEGORY_REALITY, "STCP plaintext buffer full"); return -1; }
if (c->recv_buf_len + len > c->recv_buf_cap) {
size_t cap = c->recv_buf_len + len;
if (cap < STCP_RECV_BUF_INIT) cap = STCP_RECV_BUF_INIT;
uint8_t *p = u_realloc(c->recv_buf, cap);
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "STCP plaintext buffer allocation failed: %zu", cap); return -1; }
c->recv_buf = p; c->recv_buf_cap = cap;
}
if (len) memcpy(c->recv_buf + c->recv_buf_len, data, len);
c->recv_buf_len += len;
stcp_recv_try(c);
return c->reality_io ? 0 : -1;
}
static int records_process(struct stcp_conn *c) {
struct reality_io *io = c->reality_io;
if (io->phase == WAIT_HELLO) {
int rc = hello_process(c);
if (rc <= 0) return rc;
}
while (io->wire_len >= 5 && !c->rx_paused && !c->finish_requested) {
size_t n = 5 + ((size_t)io->wire[3] << 8) + io->wire[4];
if (n > REALITY_RECORD_MAX || n < 6) goto reject;
if (io->wire_len < n) return 0;
if (io->wire[0] == 20) {
const uint8_t ccs[] = {20, 3, 3, 0, 1, 1};
if (io->phase == RECORD_DATA || io->ccs_seen || n != sizeof(ccs) || memcmp(io->wire, ccs, sizeof(ccs))) goto reject;
io->ccs_seen = 1;
} else {
uint8_t plain[REALITY_INNER_MAX], type;
size_t len = 0;
if (reality_record_open(&io->session.recv, io->wire, n, plain, sizeof(plain), &len, &type)) goto reject;
if (type == REALITY_TYPE_HANDSHAKE) {
if (handshake_process(c, plain, len)) return -1;
} else if (type == REALITY_TYPE_APPLICATION) {
if (io->phase != RECORD_DATA || io->handshake_len) goto reject;
if (append_application(c, plain, len)) return -1;
} else {
if (io->phase != RECORD_DATA || len != 2 || plain[0] != 1 || plain[1] != 0) goto reject;
stcp_conn_finish(c);
return -2;
}
if (!c->reality_io) return -1;
}
memmove(io->wire, io->wire + n, io->wire_len - n);
io->wire_len -= n;
}
return 0;
reject:
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record stream rejected: phase=%d buffered=%zu", io->phase, io->wire_len);
return -1;
}
void reality_io_resume(struct stcp_conn *c) {
if (c->reality_io && records_process(c) < 0) stcp_conn_do_close(c, ECANCELED);
}
size_t reality_io_buffered(const struct stcp_conn *c) {
return c && c->reality_io ? c->reality_io->wire_len : 0;
}
int reality_io_read(struct stcp_conn *c) {
struct reality_io *io = c->reality_io;
if (io->wire_len == STCP_RECV_BUF_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record receive buffer limit exceeded");
stcp_conn_do_close(c, ENOBUFS);
return -1;
}
ssize_t n = recv(c->sock, io->wire + io->wire_len, STCP_RECV_BUF_MAX - io->wire_len, 0);
if (n < 0) {
int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) return 0;
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record socket read failed: err=%d", err);
stcp_conn_do_close(c, err);
return -1;
}
if (!n) {
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "record socket EOF: phase=%d incomplete=%zu", io->phase, io->wire_len);
stcp_conn_do_close(c, io->wire_len || io->phase != RECORD_DATA ? ECANCELED : 0);
return -1;
}
io->wire_len += (size_t)n;
int rc = records_process(c);
if (rc < 0) { if (rc == -1) stcp_conn_do_close(c, ECANCELED); return -1; }
return 1;
}

39
src/transport_layer/reality_io.h

@ -0,0 +1,39 @@
/* Поток записей отделён от внутреннего буфера STCP; все вызовы — в uasync-потоке владельца.
* CH -> SH + CCS + encrypted(EE/cover Certificate/cover CertificateVerify/Server Finished)
* -> CCS + encrypted(Client Finished) -> encrypted(STCP handshake и последующий поток).
* Проверка стороны использует Finished, cover Certificate/CertificateVerify не являются PKI.
* Root = HKDF-SHA256(AuthKey || X25519(client_ephemeral, server_ephemeral), SHA256(CH || SH),
* "uTun-reality-record-v2"); в transcript входят handshake-заголовки, но не record-заголовки.
* От root раздельно выводятся client-traffic/server-traffic и их key/iv/finished-ключи.
* После собственного SH fallback запрещён. До него relay получает весь исходный буфер без нормализации.
* Таймеры/списки/кеш replay принадлежат instance; destroy принудительно закрывает I/O без ожидания callbacks. */
#ifndef REALITY_IO_H
#define REALITY_IO_H
#include "reality.h"
#include "etcp_padding.h"
#ifdef __cplusplus
extern "C" {
#endif
struct stcp_conn;
struct reality_io;
struct reality_replay_cache;
struct reality_relay;
struct reality_owner {
struct reality_replay_cache *replay;
struct reality_relay *relays;
unsigned relay_count;
};
int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now,
const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *));
int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *));
size_t reality_io_buffered(const struct stcp_conn *c);
int reality_io_read(struct stcp_conn *c);
void reality_io_resume(struct stcp_conn *c);
int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len);
int reality_io_close_notify(struct stcp_conn *c);
void reality_io_cleanup(struct stcp_conn *c);
void reality_owner_cleanup(struct reality_owner *owner);
#ifdef __cplusplus
}
#endif
#endif

128
src/transport_layer/reality_record.c

@ -0,0 +1,128 @@
#define OPENSSL_API_COMPAT 0x10100000L
#include "reality_record.h"
#include "../lib/debug_config.h"
#include <string.h>
#include <limits.h>
#include <openssl/evp.h>
#include <openssl/kdf.h>
#include <openssl/crypto.h>
int reality_kdf(const uint8_t *ikm, size_t ikm_len, const uint8_t *salt, size_t salt_len,
const char *label, uint8_t *out, size_t out_len) {
if ((!ikm && ikm_len) || (!salt && salt_len) || !label || !out || !out_len || ikm_len > INT_MAX || salt_len > INT_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid KDF arguments");
return -1;
}
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
size_t n = out_len;
int ok = ctx && EVP_PKEY_derive_init(ctx) > 0 && EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) > 0 &&
(!salt_len || EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt, (int)salt_len) > 0) &&
EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm, (int)ikm_len) > 0 &&
EVP_PKEY_CTX_add1_hkdf_info(ctx, (const uint8_t *)label, (int)strlen(label)) > 0 &&
EVP_PKEY_derive(ctx, out, &n) > 0 && n == out_len;
EVP_PKEY_CTX_free(ctx);
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record KDF failed: label=%s", label);
return ok ? 0 : -1;
}
int reality_traffic_init(struct reality_traffic *t, const uint8_t secret[32]) {
if (!t || !secret) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid traffic key arguments"); return -1; }
memcpy(t->secret, secret, 32);
t->sequence = 0;
if (reality_kdf(secret, 32, NULL, 0, "uTun-reality-key", t->key, 16) ||
reality_kdf(secret, 32, NULL, 0, "uTun-reality-iv", t->iv, 12)) return -1;
return 0;
}
int reality_traffic_update(struct reality_traffic *t) {
if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "missing traffic state on KeyUpdate"); return -1; }
uint8_t next[32];
int rc = reality_kdf(t->secret, 32, NULL, 0, "uTun-reality-update", next, 32);
if (!rc) rc = reality_traffic_init(t, next);
OPENSSL_cleanse(next, sizeof(next));
if (rc) return -1;
t->generation++;
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "record KeyUpdate accepted: generation=%u", t->generation);
return 0;
}
static void record_nonce(const struct reality_traffic *t, uint8_t nonce[12]) {
memcpy(nonce, t->iv, 12);
for (unsigned i = 0; i < 8; i++) nonce[11 - i] ^= (uint8_t)(t->sequence >> (i * 8));
}
static int valid_type(uint8_t type) {
return type == REALITY_TYPE_HANDSHAKE || type == REALITY_TYPE_APPLICATION || type == REALITY_TYPE_ALERT;
}
int reality_record_seal(struct reality_traffic *t, uint8_t type, const uint8_t *data, size_t len,
size_t padding, uint8_t *out, size_t cap, size_t *written) {
if (!t || !out || !written || !valid_type(type) || (!data && len) ||
len > REALITY_INNER_MAX - 1 || padding > REALITY_INNER_MAX - 1 - len ||
cap < 5 + len + 1 + padding + 16 || t->sequence >= REALITY_KEY_LIMIT ||
(!len && type != REALITY_TYPE_APPLICATION)) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record seal rejected: type=%u content=%zu padding=%zu", type, len, padding);
return -1;
}
uint8_t plain[REALITY_INNER_MAX], nonce[12];
size_t n = len + 1 + padding, cipher_len = n + 16;
if (len) memcpy(plain, data, len);
plain[len] = type;
memset(plain + len + 1, 0, padding);
out[0] = 23; out[1] = 3; out[2] = 3; out[3] = (uint8_t)(cipher_len >> 8); out[4] = (uint8_t)cipher_len;
record_nonce(t, nonce);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int size = 0, final = 0;
int ok = ctx && EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, t->key, nonce) == 1 &&
EVP_EncryptUpdate(ctx, NULL, &size, out, 5) == 1 &&
EVP_EncryptUpdate(ctx, out + 5, &size, plain, (int)n) == 1 && (size_t)size == n &&
EVP_EncryptFinal_ex(ctx, out + 5 + size, &final) == 1 && final == 0 &&
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, out + 5 + n) == 1;
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(plain, n);
if (!ok) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record encryption failed: seq=%llu", (unsigned long long)t->sequence); return -1; }
DEBUG_TRACE(DEBUG_CATEGORY_REALITY, "record tx: generation=%u seq=%llu type=%u content=%zu padding=%zu",
t->generation, (unsigned long long)t->sequence, type, len, padding);
t->sequence++;
*written = 5 + cipher_len;
return 0;
}
int reality_record_open(struct reality_traffic *t, const uint8_t *record, size_t len,
uint8_t *out, size_t cap, size_t *written, uint8_t *type) {
if (!t || !record || !out || !written || !type || len < 22 || len > REALITY_RECORD_MAX ||
record[0] != 23 || record[1] != 3 || record[2] != 3 ||
(((size_t)record[3] << 8) | record[4]) != len - 5 || cap < len - 21 || t->sequence >= REALITY_KEY_LIMIT) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record header rejected: wire=%zu", len);
return -1;
}
uint8_t nonce[12];
record_nonce(t, nonce);
size_t n = len - 21;
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int size = 0, final = 0;
int ok = ctx && EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, t->key, nonce) == 1 &&
EVP_DecryptUpdate(ctx, NULL, &size, record, 5) == 1 &&
EVP_DecryptUpdate(ctx, out, &size, record + 5, (int)n) == 1 && (size_t)size == n &&
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void *)(record + 5 + n)) == 1 &&
EVP_DecryptFinal_ex(ctx, out + size, &final) == 1;
EVP_CIPHER_CTX_free(ctx);
if (!ok) {
OPENSSL_cleanse(out, n);
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record authentication failed: generation=%u seq=%llu", t->generation,
(unsigned long long)t->sequence);
return -1;
}
while (n && !out[n - 1]) n--;
if (!n || !valid_type(out[n - 1]) || (n == 1 && out[0] != REALITY_TYPE_APPLICATION)) {
OPENSSL_cleanse(out, len - 21);
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record inner type rejected");
return -1;
}
*type = out[--n];
*written = n;
DEBUG_TRACE(DEBUG_CATEGORY_REALITY, "record rx: generation=%u seq=%llu type=%u content=%zu", t->generation,
(unsigned long long)t->sequence, *type, n);
t->sequence++;
return 0;
}

41
src/transport_layer/reality_record.h

@ -0,0 +1,41 @@
/* AEAD-записи внешнего REALITY; состояние обнуляется при создании и принадлежит одному TCP-сеансу.
* Wire: 17 03 03 | uint16_be(ciphertext_len) | AES-128-GCM(content || inner_type || zero_padding) | tag[16].
* AAD — пять байт заголовка; nonce[12] = base_iv XOR (zero[4] || uint64_be(sequence)).
* Каждый успешный seal/open увеличивает свой sequence; повторно передаются только готовые wire-байты.
* Ошибка требует закрытия сеанса. KeyUpdate последним сообщением под старым ключом меняет traffic secret
* через HKDF("uTun-reality-update"), затем key/iv и sequence=0. Направления обновляются независимо. */
#ifndef REALITY_RECORD_H
#define REALITY_RECORD_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define REALITY_INNER_MAX 16385
#define REALITY_RECORD_MAX (5 + REALITY_INNER_MAX + 16)
#ifndef REALITY_KEY_LIMIT
#define REALITY_KEY_LIMIT (UINT64_C(1) << 20)
#endif
#define REALITY_TYPE_HANDSHAKE 22
#define REALITY_TYPE_APPLICATION 23
#define REALITY_TYPE_ALERT 21
struct reality_traffic {
uint8_t secret[32], key[16], iv[12];
uint64_t sequence;
uint32_t generation;
};
int reality_kdf(const uint8_t *ikm, size_t ikm_len, const uint8_t *salt, size_t salt_len,
const char *label, uint8_t *out, size_t out_len);
int reality_traffic_init(struct reality_traffic *t, const uint8_t secret[32]);
int reality_traffic_update(struct reality_traffic *t);
int reality_record_seal(struct reality_traffic *t, uint8_t type, const uint8_t *data, size_t len,
size_t padding, uint8_t *out, size_t cap, size_t *written);
int reality_record_open(struct reality_traffic *t, const uint8_t *record, size_t len,
uint8_t *out, size_t cap, size_t *written, uint8_t *type);
#ifdef __cplusplus
}
#endif
#endif

32
src/transport_layer/reality_relay.c

@ -5,6 +5,7 @@
// первым. Поддержан half-close: FIN с одной стороны закрывает только свою
// сторону записи на другой, чтобы ответ сайта дошёл до клиента полностью.
#include "reality_relay.h"
#include "reality_io.h"
#include "../lib/u_async.h"
#include "../lib/mem.h"
#include "../lib/debug_config.h"
@ -30,6 +31,7 @@
#endif
struct reality_relay {
struct reality_owner *owner;
struct UASYNC *ua;
socket_t client_sock;
void *client_sid;
@ -56,8 +58,6 @@ struct reality_relay {
struct reality_relay *next; // цепочка активных релеев (для shutdown)
};
static struct reality_relay *g_relays = NULL; // активные релеи
static int g_relay_count = 0; // число активных релеев (для warn)
static void relay_free(struct reality_relay *r);
static void relay_touch(struct reality_relay *r);
@ -76,9 +76,9 @@ static void relay_free_cb(void *arg) {
static void relay_free(struct reality_relay *r) {
if (!r || r->closed) return;
r->closed = 1;
g_relay_count--;
r->owner->relay_count--;
{ // вынуть из реестра активных релеев
struct reality_relay **pp = &g_relays;
struct reality_relay **pp = &r->owner->relays;
while (*pp && *pp != r) pp = &(*pp)->next;
if (*pp) *pp = r->next;
r->next = NULL;
@ -287,26 +287,29 @@ static void relay_dns_done_cb(const struct adns_result *res, void *arg) {
}
}
int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
int reality_relay_start(struct reality_owner *owner, struct UASYNC *ua, socket_t client_sock,
const char *dest,
const uint8_t *initial_data, size_t initial_len,
int idle_timeout_sec) {
if (!ua || client_sock == SOCKET_INVALID || !dest || !dest[0]) {
if (ua && client_sock != SOCKET_INVALID) uasync_remove_socket_t(ua, client_sock);
if (!owner || !ua || client_sock == SOCKET_INVALID || !dest || !dest[0] || (!initial_data && initial_len)) {
if (client_sock != SOCKET_INVALID) socket_close_wrapper(client_sock);
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: invalid args");
return -1;
}
struct reality_relay *r = u_calloc(1, sizeof(struct reality_relay));
if (!r) { socket_close_wrapper(client_sock); DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: calloc failed"); return -1; }
r->owner = owner;
r->ua = ua;
r->client_sock = client_sock;
r->dest_sock = SOCKET_INVALID;
r->connecting = 1;
r->idle_timeout_tb = idle_timeout_sec > 0 ? idle_timeout_sec * 10000 : 0;
r->next = g_relays; g_relays = r;
g_relay_count++;
if (g_relay_count > RELAY_WARN_CONNS)
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay: %d concurrent relays (over %d) — возможен скан-флуд", g_relay_count, RELAY_WARN_CONNS);
r->next = owner->relays; owner->relays = r;
owner->relay_count++;
if (owner->relay_count > RELAY_WARN_CONNS)
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay: %d concurrent relays (over %d) — возможен скан-флуд", owner->relay_count, RELAY_WARN_CONNS);
relay_touch(r); // таймер покрывает DNS + connect (застрявший connect тоже чистится)
// берём владение client_sock сразу (снимаем старую регистрацию) — до парсинга,
@ -330,7 +333,7 @@ int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
if (initial_len && initial_data) {
r->c2d = u_malloc(initial_len);
if (!r->c2d) { relay_free(r); return -1; }
if (!r->c2d) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "relay initial buffer allocation failed: %zu", initial_len); relay_free(r); return -1; }
memcpy(r->c2d, initial_data, initial_len);
r->c2d_len = initial_len;
r->c2d_off = 0;
@ -359,10 +362,11 @@ int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
return 0;
}
void reality_relay_shutdown(void) {
void reality_relay_shutdown(struct reality_owner *owner) {
if (!owner) return;
int count = 0;
while (g_relays) {
struct reality_relay *r = g_relays;
while (owner->relays) {
struct reality_relay *r = owner->relays;
relay_free(r); // сам вынимает r из g_relays и откладывает u_free через call_soon
count++;
}

5
src/transport_layer/reality_relay.h

@ -16,6 +16,7 @@ extern "C" {
#include "../lib/socket_compat.h"
struct UASYNC;
struct reality_owner;
// Начать релей. Забирает владение client_sock (non-blocking, зарегистрирован
// в uasync) и проксирует его на dest ("host:port").
@ -24,7 +25,7 @@ struct UASYNC;
// idle_timeout_sec — время простоя (без трафика в обе стороны), после которого
// релей принудительно закрывается; 0 = таймер выключен.
// Возвращает 0 = ok, -1 = ошибка (client_sock уже закрыт внутри).
int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
int reality_relay_start(struct reality_owner *owner, struct UASYNC *ua, socket_t client_sock,
const char *dest,
const uint8_t *initial_data, size_t initial_len,
int idle_timeout_sec);
@ -32,7 +33,7 @@ int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
// Принудительно завершить все активные релеи (вызывается при shutdown).
// Освобождение отложено через uasync_call_soon — после вызова нужно
// uasync_drain_immediate, чтобы реальный u_free прошёл.
void reality_relay_shutdown(void);
void reality_relay_shutdown(struct reality_owner *owner);
#ifdef __cplusplus
}

128
src/transport_layer/stcp.c

@ -30,6 +30,7 @@ void stcp_conn_set_on_close(struct stcp_conn *c, void (*cb)(struct stcp_conn *co
c->close_arg = arg;
}
static void stcp_wire_clear(struct stcp_conn *c);
static struct sockaddr_storage g_peer_storage;
static ip_str_t g_peer_ip;
const char* stcp_conn_peer_str(struct stcp_conn *c) {
@ -45,6 +46,14 @@ const char* stcp_conn_peer_str(struct stcp_conn *c) {
void stcp_conn_free(struct stcp_conn *c) {
if (!c) return;
if (c->recv_active || c->close_callback_active) {
if (!c->free_requested) {
c->free_requested = 1;
stcp_conn_do_close(c, ECANCELED);
}
return;
}
reality_io_cleanup(c);
callring_put(CR_CONN_FREE, (uintptr_t)c, 0);
stcp_server_remove_conn(c);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp_conn_free: c=%p sock=%d sock_id=%p recv_buf=%p state=%d allocated=%d",
@ -54,6 +63,7 @@ void stcp_conn_free(struct stcp_conn *c) {
if (c->recv_buf) { u_free(c->recv_buf); c->recv_buf = NULL; }
if (c->send_buf) { u_free(c->send_buf); c->send_buf = NULL; }
stcp_pending_clear(c);
stcp_wire_clear(c);
sc_stream_cleanup(&c->stream_send);
sc_stream_cleanup(&c->stream_recv);
if (c->allocated) u_free(c);
@ -103,6 +113,7 @@ int stcp_frame_decrypt(uint8_t *data, size_t len, struct sc_stream_state *stream
void stcp_pending_queue(struct stcp_conn *c, const uint8_t *data, size_t len) {
if (!c) return;
if (c->finish_requested) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "DATA rejected during graceful close"); return; }
struct pending_entry *pe = u_malloc(sizeof(*pe));
if (!pe) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_pending_queue malloc entry failed"); return; }
if (len && data) { pe->data = u_malloc(len); if (!pe->data) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_pending_queue malloc data(%zu) failed", len); u_free(pe); return; } memcpy(pe->data, data, len); }
@ -125,27 +136,50 @@ void stcp_pending_clear(struct stcp_conn *c) {
c->pending_tail = NULL;
}
int stcp_try_send(struct stcp_conn *c, uint8_t *data, size_t len) {
if (c->sock == SOCKET_INVALID) return -1;
if (c->send_buf) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_try_send: send_buf already busy"); return -1; }
static void stcp_wire_clear(struct stcp_conn *c) {
while (c->wire_head) {
struct pending_entry *e = c->wire_head;
c->wire_head = e->next;
u_free(e->data);
u_free(e);
}
c->wire_tail = NULL;
c->wire_bytes = 0;
}
int stcp_send_raw(struct stcp_conn *c, uint8_t *data, size_t len) {
if (c->sock == SOCKET_INVALID || c->wire_bytes + len > STCP_RECV_BUF_MAX * 2) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "wire queue rejected: socket=%d queued=%zu new=%zu", (int)c->sock, c->wire_bytes, len);
return -1;
}
if (c->send_buf) {
struct pending_entry *e = u_malloc(sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "wire queue allocation failed"); return -1; }
e->data = data; e->len = len; e->next = NULL;
if (c->wire_tail) c->wire_tail->next = e;
else c->wire_head = e;
c->wire_tail = e;
c->wire_bytes += len;
return 1;
}
ssize_t sent = send(c->sock, data, len, 0);
if (sent < 0) {
int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) {
c->send_buf = data; c->send_len = len; c->send_offset = 0;
uasync_set_socket_write(c->ua, c->socket_id, 1);
return 1;
if (err != ERR_AGAIN && err != ERR_WOULDBLOCK) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp send failed: err=%d", err);
return -1;
}
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_try_send send failed err=%d", err);
return -1;
sent = 0;
}
if ((size_t)sent < len) {
c->send_buf = data; c->send_len = len; c->send_offset = (size_t)sent;
uasync_set_socket_write(c->ua, c->socket_id, 1);
return 1;
}
u_free(data);
return 0;
if ((size_t)sent == len) { u_free(data); return 0; }
c->send_buf = data; c->send_len = len; c->send_offset = (size_t)sent;
uasync_set_socket_write(c->ua, c->socket_id, 1);
return 1;
}
int stcp_try_send(struct stcp_conn *c, uint8_t *data, size_t len) {
if (c->close_err || c->close_after_send) return -1;
return c->reality_io ? reality_io_send(c, data, len) : stcp_send_raw(c, data, len);
}
// Write-error больше не закрывает conn синхронно: do_close → on_close →
@ -166,10 +200,15 @@ static void stcp_schedule_close(struct stcp_conn *c, int err) {
if (c->close_call_soon) return; // уже запланирован — первый err остаётся
c->close_err = err;
c->close_call_soon = uasync_call_soon(c->ua, c, stcp_close_deferred);
if (!c->close_call_soon) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "write-error close scheduling failed; closing on socket write callback");
uasync_set_socket_write(c->ua, c->socket_id, 1);
}
}
void stcp_write_cb(socket_t sock, void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg;
if (c->close_err) { if (!c->close_call_soon) stcp_conn_do_close(c, c->close_err); return; }
if (!c->send_buf) { uasync_set_socket_write(c->ua, c->socket_id, 0); return; }
ssize_t sent = send(sock, c->send_buf + c->send_offset, c->send_len - c->send_offset, 0);
if (sent < 0) {
@ -182,14 +221,24 @@ void stcp_write_cb(socket_t sock, void *arg) {
c->send_offset += (size_t)sent;
if (c->send_offset >= c->send_len) {
u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; c->send_offset = 0;
if (c->wire_head) {
struct pending_entry *e = c->wire_head;
c->wire_head = e->next;
if (!c->wire_head) c->wire_tail = NULL;
c->wire_bytes -= e->len;
c->send_buf = e->data; c->send_len = e->len;
u_free(e);
return;
}
uasync_set_socket_write(c->ua, c->socket_id, 0);
if (c->close_after_send) { stcp_conn_do_close(c, 0); return; }
if (c->finish_requested) { stcp_conn_finish(c); return; }
stcp_flush_pending(c);
}
}
void stcp_flush_pending(struct stcp_conn *c) {
if (!c || c->state != STCP_STATE_DATA) return; // conn закрыт/не DATA — слать нечего
if (!c || c->state != STCP_STATE_DATA || c->close_after_send || c->close_err) return; // conn закрыт/не DATA — слать нечего
while (!c->send_buf && c->pending_head) {
struct pending_entry *pe = c->pending_head;
c->pending_head = pe->next;
@ -215,6 +264,7 @@ void stcp_flush_pending(struct stcp_conn *c) {
// ====== unified recv ======
int stcp_conn_read(struct stcp_conn *c) {
if (c->reality_io) return reality_io_read(c);
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return -1;
if (!c->recv_buf) {
c->recv_buf_cap = STCP_RECV_BUF_INIT;
@ -250,10 +300,10 @@ void stcp_recv_set(struct stcp_conn *c, size_t need, int streaming,
if (streaming) { c->recv_in_meta = 1; c->recv_need = 2; }
}
void stcp_recv_try(struct stcp_conn *c) {
static void stcp_recv_process(struct stcp_conn *c) {
if (!c || c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return;
while (c->recv_buf_len > 0) {
while (c->recv_buf_len > 0 && !c->rx_paused && !c->finish_requested) {
if (!c->recv_streaming) {
if (c->recv_buf_len < c->recv_need) return;
void (*cb)(struct stcp_conn*, uint8_t*, size_t) = c->recv_on_chunk;
@ -313,6 +363,13 @@ void stcp_recv_try(struct stcp_conn *c) {
}
}
void stcp_recv_try(struct stcp_conn *c) {
if (!c || c->recv_active) return;
c->recv_active = 1;
stcp_recv_process(c);
c->recv_active = 0;
}
// ====== unified close ======
static void stcp_conn_deferred_free(void *arg) {
@ -327,14 +384,33 @@ void hs_timeout_cb(void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg;
c->hs_timer = NULL;
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return;
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "stcp handshake timeout is_server=%d prev_state=%d", c->is_server, c->state);
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "stcp timeout is_server=%d state=%d closing=%u", c->is_server, c->state, c->close_after_send);
stcp_conn_do_close(c, ETIMEDOUT);
}
void stcp_conn_finish(struct stcp_conn *c) {
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR || c->close_after_send) return;
if (!c->finish_requested) {
c->finish_requested = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "graceful close: role=%u queued_wire=%zu pending_data=%u", c->is_server, c->wire_bytes, c->pending_head != NULL);
if (c->socket_id) uasync_set_socket_read(c->ua, c->socket_id, 0);
if (c->hs_timer) uasync_cancel_timeout(c->ua, c->hs_timer);
c->hs_timer = uasync_set_timeout(c->ua, 10000, c, hs_timeout_cb, "stcp_close");
if (!c->hs_timer) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "close timeout allocation failed"); stcp_conn_do_close(c, ENOMEM); return; }
}
stcp_flush_pending(c);
if (c->close_err) { stcp_conn_do_close(c, c->close_err); return; }
if (c->send_buf || c->pending_head) return;
if (c->reality_io && reality_io_close_notify(c) < 0) { stcp_conn_do_close(c, ECANCELED); return; }
if (!c->send_buf) { stcp_conn_do_close(c, 0); return; }
c->close_after_send = 1;
}
void stcp_conn_do_close(struct stcp_conn *c, int err) {
if (!c) return;
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return;
callring_put(CR_CONN_DOCLOSE_IN, (uintptr_t)c, 0);
reality_io_cleanup(c);
int prev = c->state;
c->state = STCP_STATE_CLOSED;
@ -350,6 +426,7 @@ void stcp_conn_do_close(struct stcp_conn *c, int err) {
if (c->recv_buf) { u_free(c->recv_buf); c->recv_buf = NULL; c->recv_buf_len = 0; c->recv_buf_cap = 0; }
if (c->send_buf) { u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; }
stcp_pending_clear(c);
stcp_wire_clear(c);
sc_stream_cleanup(&c->stream_send);
sc_stream_cleanup(&c->stream_recv);
@ -357,9 +434,12 @@ void stcp_conn_do_close(struct stcp_conn *c, int err) {
void (*cb)(struct stcp_conn*, int, void*) = c->on_close;
c->on_close = NULL;
callring_put(CR_CONN_DOCLOSE_CB, (uintptr_t)c, (uintptr_t)c->close_arg);
c->close_callback_active = 1;
cb(c, err, c->close_arg);
c->close_callback_active = 0;
}
if (c->free_on_close) {
c->free_scheduled = 1;
uasync_call_soon(c->ua, c->free_on_close, stcp_free_on_close_cb);
c->free_on_close = NULL;
}
@ -374,7 +454,7 @@ void stcp_tx_queue_cb(struct ll_queue *q, void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg;
struct ll_entry *e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
if (c->state == STCP_STATE_DATA && e->dgram)
if (c->state == STCP_STATE_DATA && !c->finish_requested && !c->close_after_send && !c->close_err && e->dgram)
stcp_pending_queue(c, e->dgram, e->len);
queue_dgram_free(e);
queue_entry_free(e);
@ -399,16 +479,18 @@ void stcp_rx_push(struct stcp_conn *c, uint8_t *data, size_t len) {
(c->rx_queue->count > STCP_RX_QUEUE_MAX_PACKETS || c->rx_queue->total_bytes > STCP_RX_QUEUE_MAX_BYTES)) {
c->rx_paused = 1;
uasync_set_socket_read(c->ua, c->socket_id, 0);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp rx paused count=%d bytes=%zu", c->rx_queue->count, c->rx_queue->total_bytes);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp rx paused count=%d bytes=%zu tls_buffered=%zu", c->rx_queue->count, c->rx_queue->total_bytes, reality_io_buffered(c));
}
}
void stcp_rx_resume_if_needed(struct stcp_conn *c) {
if (!c || !c->rx_paused || !c->rx_queue) return;
if (c->state != STCP_STATE_DATA) return;
if (c->state != STCP_STATE_DATA || c->finish_requested) return;
if (c->rx_queue->count < STCP_RX_QUEUE_MAX_PACKETS && c->rx_queue->total_bytes < STCP_RX_QUEUE_MAX_BYTES) {
c->rx_paused = 0;
if (c->socket_id) uasync_set_socket_read(c->ua, c->socket_id, 1);
stcp_recv_try(c);
if (c->reality_io && !c->rx_paused) reality_io_resume(c);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp rx resumed count=%d bytes=%zu", c->rx_queue->count, c->rx_queue->total_bytes);
}
}

14
src/transport_layer/stcp.h

@ -14,6 +14,7 @@ extern "C" {
#include "secure_channel.h"
#include "etcp_padding.h"
#include "reality.h"
#include "reality_io.h"
#include "crc32.h"
#include "../lib/u_async.h"
#include "../lib/socket_compat.h"
@ -107,11 +108,12 @@ struct stcp_conn {
uint8_t hs_flags; // client: handshake flags to send (STCP_HANDSHAKE_FLAG_*)
uint8_t peer_flags; // flags received from peer handshake
uint64_t hs_send_time; // client: handshake send time (0.1ms tb), for ping RTT
uint8_t finish_requested;
uint8_t close_after_send; // server: graceful close once pending send is flushed
// reality-камуфляж (только для серверной стороны accept-пути)
uint8_t reality_enabled; // 1 = перед STCP-хендшейком ждём/отвечаем reality ClientHello
uint8_t reality_hdr[5]; // сохранённый TLS record header при двухфазном чтении ClientHello
// Внешний поток REALITY и копия серверной конфигурации для accept/fallback.
struct reality_io *reality_io;
struct reality_owner *reality_owner;
struct reality_server_config reality_srv; // конфиг сервера (копия)
char reality_dest[REALITY_DEST_MAX]; // "host:port" для релея неавторизованных
@ -127,6 +129,10 @@ struct stcp_conn {
size_t send_len;
size_t send_offset;
struct pending_entry *wire_head, *wire_tail;
size_t wire_bytes;
uint8_t recv_active, close_callback_active, free_requested, free_scheduled;
struct pending_entry *pending_head;
struct pending_entry *pending_tail;
@ -169,6 +175,7 @@ void stcp_pending_queue(struct stcp_conn *c, const uint8_t *data, size_t len);
void stcp_pending_clear(struct stcp_conn *c);
// data из u_malloc: 0 — отправлен и освобождён, 1 — принят для досылки; -1 оставляет data вызывающему.
int stcp_send_raw(struct stcp_conn *c, uint8_t *data, size_t len);
int stcp_try_send(struct stcp_conn *c, uint8_t *data, size_t len);
void stcp_write_cb(socket_t sock, void *arg);
void stcp_flush_pending(struct stcp_conn *c);
@ -183,6 +190,7 @@ void stcp_recv_set(struct stcp_conn *c, size_t need, int streaming,
void stcp_recv_try(struct stcp_conn *c);
// unified close: closes socket, frees buffers, calls on_close. does NOT clean streams (stcp_conn_free does)
void stcp_conn_finish(struct stcp_conn *c);
void stcp_conn_do_close(struct stcp_conn *c, int err);
// снимает принятый коннект из списка владеющего сервера (реализация в stcp_server.c)

80
src/transport_layer/stcp_client.c

@ -38,6 +38,9 @@ struct stcp_client {
struct reality_client_config reality_cfg;
// SOCKS5-прокси: подключение к прокси + CONNECT выполняется до STCP-хендшейка
void *socks_handle; // handle socks_dial (NULL = не через прокси)
uint32_t reality_time_sec;
uint64_t reality_time_tb;
struct ETCP_PADDING outer_padding;
int hs_timeout_tb; // таймаут хендшейка (0.1ms), для установки после dial
};
@ -46,8 +49,7 @@ static void client_conn_read_cb(socket_t sock, void *arg);
static void client_hs_cb(struct stcp_conn *c, uint8_t *data, size_t len);
static void client_hs_padding_cb(struct stcp_conn *c, uint8_t *data, size_t len);
static void client_data_cb(struct stcp_conn *c, uint8_t *plain_data, size_t data_len);
static void reality_client_sh_hdr_cb(struct stcp_conn *c, uint8_t *data, size_t len);
static void reality_client_sh_body_cb(struct stcp_conn *c, uint8_t *data, size_t len);
static void client_start_stcp(struct stcp_conn *c);
static int client_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey) {
struct secure_channel sc;
@ -96,8 +98,7 @@ static void client_send_handshake(struct stcp_conn *c, const uint8_t *server_pub
c->hs_send_time = get_time_tb();
c->state = STCP_STATE_HS_CLIENT_SENT;
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: handshake sent (%zu bytes) gop=%d rid=%016llx flags=%02x, entering HS_CLIENT_SENT", total, gop, (unsigned long long)rid, c->hs_flags);
c->send_buf = hs; c->send_len = total; c->send_offset = 0;
uasync_set_socket_write(c->ua, c->socket_id, 1);
if (stcp_try_send(c, hs, total) < 0) { u_free(hs); stcp_conn_do_close(c, ECANCELED); }
}
static void client_hs_cb(struct stcp_conn *c, uint8_t *data, size_t len) {
@ -154,7 +155,8 @@ static void client_hs_padding_cb(struct stcp_conn *c, uint8_t *data, size_t len)
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: ping OK rtt=%u sock=%d", (unsigned)rtt, (int)c->sock);
cli->ping_done = 1;
if (cli->ping_cb) cli->ping_cb(1, rtt, cli->ping_arg);
stcp_client_destroy(cli);
if (c->reality_io) stcp_conn_finish(c);
else stcp_client_destroy(cli);
return;
}
c->state = STCP_STATE_DATA;
@ -167,55 +169,23 @@ static void client_data_cb(struct stcp_conn *c, uint8_t *plain_data, size_t data
stcp_rx_push(c, plain_data, data_len);
}
// ─── reality-камуфляж (клиентская сторона) ───
static void reality_client_sh_hdr_cb(struct stcp_conn *c, uint8_t *data, size_t len) {
(void)len;
if (data[0] != 0x16) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "stcp_client: first byte 0x%02x — not TLS record from server", data[0]);
stcp_conn_do_close(c, 1); return;
}
uint16_t rec_len = (uint16_t)((data[3] << 8) | data[4]);
if (rec_len < 4 || rec_len > REALITY_MAX_SH_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "stcp_client: bad ServerHello record len %u", rec_len);
stcp_conn_do_close(c, 1); return;
}
stcp_recv_set(c, rec_len, 0, reality_client_sh_body_cb);
}
static void reality_client_sh_body_cb(struct stcp_conn *c, uint8_t *data, size_t len) {
(void)data; (void)len;
static void client_start_stcp(struct stcp_conn *c) {
struct stcp_client *cli = (struct stcp_client *)c;
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_client: ServerHello received, continue STCP handshake");
if (client_derive_session(c, cli->peer_pubkey)) { stcp_conn_do_close(c, 1); return; }
if (client_derive_session(c, cli->peer_pubkey)) { stcp_conn_do_close(c, ECANCELED); return; }
client_send_handshake(c, cli->peer_pubkey, cli->my_ed25519_pubkey);
stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER, 0, client_hs_cb);
if (c->state != STCP_STATE_CLOSED) stcp_recv_set(c, STCP_HS_SERVER_MIN, 0, client_hs_cb);
}
// После завершения TCP-connect: либо reality-фаза (ClientHello→ServerHello), либо обычный STCP-хендшейк
static void client_after_connect(struct stcp_conn *c) {
struct stcp_client *cli = (struct stcp_client *)c;
int opt = 1; setsockopt(c->sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
if (cli->reality_enabled) {
uint8_t ch[REALITY_MAX_CH_SIZE]; size_t ch_len = 0;
uint32_t now_sec = (c->etcp_conn && c->etcp_conn->instance)
? (uint32_t)ntp_time_get_seconds(c->etcp_conn->instance) : (uint32_t)time(NULL);
if (reality_client_hello_build_at(&cli->reality_cfg, now_sec, ch, sizeof(ch), &ch_len) != REALITY_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "stcp_client: ClientHello build failed");
stcp_conn_do_close(c, 1); return;
}
uint8_t *chbuf = u_malloc(ch_len);
if (!chbuf) { stcp_conn_do_close(c, ENOMEM); return; }
memcpy(chbuf, ch, ch_len);
c->send_buf = chbuf; c->send_len = ch_len; c->send_offset = 0;
uasync_set_socket_write(c->ua, c->socket_id, 1);
stcp_recv_set(c, 5, 0, reality_client_sh_hdr_cb);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_client: ClientHello sent (%zu bytes), waiting ServerHello", ch_len);
uint32_t now = cli->reality_time_sec + (uint32_t)((get_time_tb() - cli->reality_time_tb) / 10000);
if (reality_io_client_start(c, &cli->reality_cfg, now, &cli->outer_padding, client_start_stcp))
stcp_conn_do_close(c, ECANCELED);
return;
}
if (client_derive_session(c, cli->peer_pubkey)) { stcp_conn_do_close(c, 1); return; }
client_send_handshake(c, cli->peer_pubkey, cli->my_ed25519_pubkey);
stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER, 0, client_hs_cb);
client_start_stcp(c);
}
static void client_conn_read_cb(socket_t sock, void *arg) {
@ -282,13 +252,20 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
cli->ping_cb = ping_cb; cli->ping_arg = ping_arg;
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 (reality) {
cli->reality_enabled = 1; cli->reality_cfg = *reality;
cli->reality_time_sec = inst ? (uint32_t)ntp_time_get_seconds(inst) : (uint32_t)time(NULL);
cli->reality_time_tb = get_time_tb();
const struct ETCP_PADDING *profile = etcp_padding_for_peer(inst, peer_pubkey);
if (profile) { cli->outer_padding.min = profile->min; cli->outer_padding.max = profile->max; }
}
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->sock = SOCKET_INVALID;
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;
c->on_close = close_cb; c->close_arg = close_arg;
@ -346,6 +323,8 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
return cli;
}
static void client_free_cb(void *arg) { u_free(arg); }
void stcp_client_destroy(struct stcp_client *cli) {
if (!cli) return;
if (cli->socks_handle) {
@ -354,6 +333,13 @@ void stcp_client_destroy(struct stcp_client *cli) {
u_free(cli);
return;
}
if (cli->conn.free_scheduled) return;
if (cli->conn.state == STCP_STATE_CLOSED || cli->conn.state == STCP_STATE_ERROR) {
if (cli->conn.free_on_close) return;
cli->conn.free_scheduled = 1;
if (!uasync_call_soon(cli->ua, cli, client_free_cb)) DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client free scheduling failed");
return;
}
cli->conn.free_on_close = cli;
stcp_conn_do_close(&cli->conn, 0);
}
@ -378,11 +364,11 @@ struct stcp_client *stcp_ping_send(struct UASYNC *ua, const char *addr, uint16_t
uint8_t device_type, uint16_t keepalive_interval,
int timeout_ms, stcp_ping_cb cb, void *arg,
const struct socks_cfg *socks,
struct UTUN_INSTANCE *inst) {
struct UTUN_INSTANCE *inst, const struct reality_client_config *reality) {
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, inst);
NULL, NULL, cb, arg, ping_close_cb, NULL, NULL, timeout_ms, reality, socks, inst);
if (cli) cli->conn.free_on_close = cli;
return cli;
}

2
src/transport_layer/stcp_client.h

@ -41,7 +41,7 @@ struct stcp_client *stcp_ping_send(struct UASYNC *ua, const char *addr, uint16_t
uint8_t device_type, uint16_t keepalive_interval,
int timeout_ms, stcp_ping_cb cb, void *arg,
const struct socks_cfg *socks,
struct UTUN_INSTANCE *inst);
struct UTUN_INSTANCE *inst, const struct reality_client_config *reality);
void stcp_client_destroy(struct stcp_client *cli);
struct stcp_conn *stcp_client_get_conn(struct stcp_client *cli);

81
src/transport_layer/stcp_server.c

@ -41,6 +41,7 @@ struct stcp_server {
struct reality_server_config reality_srv;
char reality_dest[REALITY_DEST_MAX];
struct reality_owner local_reality_owner;
struct stcp_conn *conns; // список принятых (accept) коннектов — закрываются при destroy
};
@ -57,8 +58,7 @@ static void server_conn_read_cb(socket_t sock, void *arg);
static void server_hs_phase1_cb(struct stcp_conn *c, uint8_t *data, size_t len);
static void server_hs_phase2_cb(struct stcp_conn *c, uint8_t *data, size_t len);
static void server_data_cb(struct stcp_conn *c, uint8_t *plain_data, size_t data_len);
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 void server_start_stcp(struct stcp_conn *c);
static int server_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey, const uint8_t *salt) {
struct secure_channel sc;
@ -179,8 +179,7 @@ static void server_hs_phase2_cb(struct stcp_conn *c, uint8_t *data, size_t len)
standby_log_rx("tcp ping", stcp_conn_peer_str(c));
#endif
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: ping answered, graceful close sock=%d", (int)c->sock);
if (r == 0) { stcp_conn_do_close(c, 0); return; }
c->close_after_send = 1;
stcp_conn_finish(c);
return;
}
c->state = STCP_STATE_DATA;
@ -210,73 +209,8 @@ void stcp_server_set_reality(struct stcp_server *srv, const struct reality_confi
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_server_set_reality: enabled, short_ids=%d dest=%s", rc->short_id_count, rc->dest);
}
// Передать неавторизованное соединение в релей: initial = head[head_len] + tail[tail_len]
static void reality_server_start_relay(struct stcp_conn *c,
const uint8_t *head, size_t head_len,
const uint8_t *tail, size_t tail_len) {
size_t init_len = head_len + tail_len;
uint8_t *init = u_malloc(init_len ? init_len : 1);
if (head_len) memcpy(init, head, head_len);
if (tail_len) memcpy(init + head_len, tail, tail_len);
socket_t cli_sock = c->sock;
// отсоединяем сокет от stcp_conn (не закрывая): reality_relay_start сам
// снимет регистрацию из uasync и возьмёт владение сокетом.
c->socket_id = NULL;
c->sock = SOCKET_INVALID;
reality_relay_start(c->ua, cli_sock, c->reality_dest, init, init_len, c->reality_srv.relay_idle_timeout_sec);
u_free(init); /* reality_relay_start копирует данные в свой буфер — владение остаётся у нас */
stcp_conn_do_close(c, 0);
}
static void reality_ch_hdr_cb(struct stcp_conn *c, uint8_t *data, size_t len) {
(void)len;
memcpy(c->reality_hdr, data, 5);
if (data[0] != 0x16) {
#ifdef UTUN_HAVE_STANDBY
standby_log_rx("reality undecryptable", stcp_conn_peer_str(c));
#endif
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "stcp_server: first byte 0x%02x — not TLS, relay", data[0]);
reality_server_start_relay(c, data, 5, c->recv_buf + 5, c->recv_buf_len - 5);
return;
}
uint16_t rec_len = (uint16_t)((data[3] << 8) | data[4]);
if (rec_len < 4 || rec_len > REALITY_MAX_CH_SIZE) {
#ifdef UTUN_HAVE_STANDBY
standby_log_rx("reality undecryptable", stcp_conn_peer_str(c));
#endif
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "stcp_server: bad TLS record len %u, relay", rec_len);
reality_server_start_relay(c, data, 5, c->recv_buf + 5, c->recv_buf_len - 5);
return;
}
stcp_recv_set(c, rec_len, 0, reality_ch_body_cb);
}
static void reality_ch_body_cb(struct stcp_conn *c, uint8_t *data, size_t len) {
// data = тело TLS-записи (rec_len байт); заголовок в c->reality_hdr
uint8_t full[5 + REALITY_MAX_CH_SIZE];
memcpy(full, c->reality_hdr, 5);
memcpy(full + 5, data, len);
uint8_t sh[REALITY_MAX_SH_SIZE];
size_t sh_len = 0;
uint32_t now_sec = c->inst ? (uint32_t)ntp_time_get_seconds(c->inst) : (uint32_t)time(NULL);
int rc = reality_server_hello_build_at(&c->reality_srv, now_sec, full, 5 + len, sh, sizeof(sh), &sh_len);
if (rc == REALITY_OK) {
uint8_t *shbuf = u_malloc(sh_len);
if (!shbuf) { stcp_conn_do_close(c, ENOMEM); return; }
memcpy(shbuf, sh, sh_len);
if (stcp_try_send(c, shbuf, sh_len) < 0) { u_free(shbuf); stcp_conn_do_close(c, 1); return; }
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_server: reality auth OK, continue STCP handshake");
stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT, 0, server_hs_phase1_cb);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_server: reality auth failed rc=%d, relay", rc);
#ifdef UTUN_HAVE_STANDBY
standby_log_rx("reality undecryptable (auth)", stcp_conn_peer_str(c));
#endif
reality_server_start_relay(c, full, 5 + len, c->recv_buf + len, c->recv_buf_len - len);
static void server_start_stcp(struct stcp_conn *c) {
stcp_recv_set(c, STCP_HS_CLIENT_MIN, 0, server_hs_phase1_cb);
}
static void server_conn_read_cb(socket_t sock, void *arg) {
@ -322,13 +256,13 @@ static void server_accept_cb(socket_t listen_sock, void *arg) {
return;
}
c->inst = srv->inst;
c->reality_owner = srv->inst ? &srv->inst->reality_owner : &srv->local_reality_owner;
c->device_type = srv->inst ? srv->inst->client_type : 0;
c->keepalive_interval = srv->inst ? srv->inst->keepalive_interval : 200;
if (srv->reality_enabled) {
c->reality_enabled = 1;
c->reality_srv = srv->reality_srv;
snprintf(c->reality_dest, sizeof(c->reality_dest), "%s", srv->reality_dest);
stcp_recv_set(c, 5, 0, reality_ch_hdr_cb);
if (reality_io_server_start(c, server_start_stcp)) { stcp_conn_do_close(c, ENOMEM); return; }
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_server: reality enabled on accepted conn, waiting ClientHello");
} else {
stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT, 0, server_hs_phase1_cb);
@ -408,6 +342,7 @@ void stcp_server_destroy(struct stcp_server *srv) {
struct stcp_conn *c = srv->conns;
stcp_conn_do_close(c, 0); // снимает c из списка (c->srv) и каскадно закрывает серверный ETCP_LINK
}
reality_owner_cleanup(&srv->local_reality_owner);
u_free(srv);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server destroyed");
}

2
src/utun_instance.c

@ -468,7 +468,7 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
/* Phase J: TCP connections + sockets — MUST be before ETCP connections */
stcp_server_list_destroy_all(instance);
reality_relay_shutdown(); /* активные reality-релеи откладывают u_free через call_soon */
reality_owner_cleanup(&instance->reality_owner); /* активные reality-релеи откладывают u_free через call_soon */
if (instance->tcp_connections) {
struct ll_entry* entry = instance->tcp_connections->head;
int tc = 0;

2
src/utun_instance.h

@ -18,6 +18,7 @@ extern "C" {
#include "../lib/sqlite3.h"
#include "secure_channel.h"
#include "etcp_padding.h"
#include "reality_io.h"
#include "etcp_api.h"
#include "config_parser.h"
#include "stcp_link.h"
@ -119,6 +120,7 @@ struct peer_sleep_cbk_entry {
// Состояние одного узла. Вложенные модули освобождаются через их lifecycle API.
struct UTUN_INSTANCE {
struct reality_owner reality_owner;
uint8_t core_started, utun_started, chat_started; // успешный запуск ядра / UTUN / чата
// Identification

28
tests/Makefile.am

@ -1,6 +1,6 @@
# Tests Makefile.am for utun - all tests with new ll_queue library
EXTRA_DIST = data/silero_speech.pcm data/silero_speech.txt
EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt
# All available tests (check_PROGRAMS runs via automake check-TESTS)
check_PROGRAMS = \
@ -120,6 +120,8 @@ check_PROGRAMS = \
test_media_delivery_full \
test_media_delivery_chat \
test_etcp_link_stress \
test_reality_tls \
test_reality_record \
test_reality_hello \
test_reality_bgp \
test_reality_config \
@ -130,7 +132,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 test_etcp_padding
check_PROGRAMS += test_reality_fallback test_stcp_reality_rotation test_uasync_regressions test_ll_queue_regressions test_etcp_padding
endif
# Silero VAD: только при включённом ONNX Runtime (--with-silero-vad)
@ -164,7 +166,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
test_stcp_LDFLAGS = -Wl,--wrap=send -Wl,--wrap=recv -Wl,--wrap=random_bytes -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl
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)
@ -776,3 +778,23 @@ check_PROGRAMS += test_chat_poll_network
test_chat_poll_network_SOURCES = test_chat_poll_network.c
test_chat_poll_network_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib
test_chat_poll_network_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_reality_record_SOURCES = test_reality_record.c
test_reality_record_LDADD = $(LIBUTUN) $(COMMON_LIBS)
if OS_LINUX
# Низкий лимит проверяет реальную смену ключей в обе стороны без миллиона пакетов.
test_stcp_reality_rotation_SOURCES = test_stcp.c ../src/transport_layer/reality_io.c ../src/transport_layer/reality_record.c
test_stcp_reality_rotation_CFLAGS = $(AM_CFLAGS) -DREALITY_KEY_LIMIT=8
test_stcp_reality_rotation_LDFLAGS = -Wl,--wrap=send -Wl,--wrap=recv -Wl,--wrap=random_bytes -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl
test_stcp_reality_rotation_LDADD = $(LIBUTUN) $(COMMON_LIBS)
endif
if OS_LINUX
test_reality_fallback_SOURCES = test_reality_fallback.c
test_reality_fallback_LDFLAGS = -Wl,--wrap=ntp_time_get_seconds
test_reality_fallback_LDADD = $(LIBUTUN) $(COMMON_LIBS)
endif
test_reality_tls_SOURCES = test_reality_tls.c
test_reality_tls_LDADD = $(LIBUTUN) -lssl $(COMMON_LIBS)

3
tests/fixtures/reality_openssl_hello.hex vendored

@ -0,0 +1,3 @@
# OpenSSL 3.5.5, 27 Jan 2026; local capture 2026-10-07.
# openssl s_client -tls1_3 -groups X25519 -ciphersuites TLS_AES_128_GCM_SHA256 -alpn http/1.1 -servername www.microsoft.com -connect 127.0.0.1:PORT
1603010102010000fe0303495c3aad4d7fbc7f0ea12245795f052c33c7700a56c9f15bd58374992a75f617204f5583134eaae74da0bd4cf1ed233297a41f0ff7bd0118d3b0c418098ca5542e00021301010000b30000001600140000117777772e6d6963726f736f66742e636f6d000b000403000102000a00040002001d002300000010000b000908687474702f312e310016000000170000000d002a002809050906090404030503060308070808081a081b081c0809080a080b080408050806040105010601002b0003020304002d00020101003300260024001d002016a64663eb5b32977d0fbbe63eea3498ee62dd67428c7c25d26ac567b4d06904001b00050400010003

112
tests/test_reality_fallback.c

@ -0,0 +1,112 @@
/* Реальный relay: нормализация replay, сохранение хвоста, независимое владение instance. */
#include "stcp_server.h"
#include "utun_instance.h"
#include "../lib/mem.h"
#include "../lib/debug_config.h"
#include <string.h>
#include <time.h>
#include <errno.h>
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL line=%d: %s", __LINE__, #x); return 1; } } while (0)
static uint32_t fake_now;
time_t __wrap_ntp_time_get_seconds(struct UTUN_INSTANCE *inst) { (void)inst; return fake_now; }
struct destination_peer { socket_t sock; struct UASYNC *ua; uint8_t data[4096]; size_t len; };
struct destination { socket_t sock; struct UASYNC *ua; unsigned count; struct destination_peer peers[4]; };
static void destination_read(socket_t sock, void *arg) {
struct destination_peer *p = arg;
ssize_t n = recv(sock, p->data + p->len, sizeof(p->data) - p->len, 0);
if (n > 0) p->len += (size_t)n;
else if (!n) { uasync_remove_socket_t(p->ua, sock); socket_close_wrapper(sock); p->sock = SOCKET_INVALID; }
}
static void destination_accept(socket_t sock, void *arg) {
struct destination *d = arg;
socket_t peer = accept(sock, NULL, NULL);
if (peer == SOCKET_INVALID || d->count >= 4) return;
struct destination_peer *p = &d->peers[d->count++];
p->sock = peer; p->ua = d->ua;
socket_set_nonblocking(peer);
uasync_add_socket_t(d->ua, peer, destination_read, NULL, NULL, "reality_test_dest", p);
}
static void unexpected_stcp(struct stcp_conn *c, void *arg) {
(void)c; (void)arg;
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "unexpected STCP before Client Finished");
}
static socket_t connect_to(uint16_t port) {
socket_t s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address = {0};
address.sin_family = AF_INET; address.sin_port = htons(port); address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(s, (struct sockaddr *)&address, sizeof(address))) { socket_close_wrapper(s); return SOCKET_INVALID; }
socket_set_nonblocking(s);
return s;
}
static void poll_some(struct UASYNC *ua) { for (unsigned i = 0; i < 100; i++) uasync_poll(ua, 0); }
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_WARN);
size_t allocated = u_get_allocated_count();
struct UASYNC *ua = uasync_create(); CHECK(ua);
struct UTUN_INSTANCE a = {0}, b = {0}; a.ua = b.ua = ua;
struct SC_MYKEYS keys; CHECK(sc_generate_keypair(&keys) == SC_OK);
struct reality_config cfg; reality_config_set_defaults(&cfg);
struct reality_client_config cc; reality_client_config_set_defaults(&cc);
cfg.enabled = 1; cfg.short_id_count = 1;
CHECK(!reality_generate_keypair(cfg.private_key, cc.server_static_pubkey));
strcpy(cc.server_name, "www.microsoft.com");
struct destination dest = {0}; dest.ua = ua;
dest.sock = socket(AF_INET, SOCK_STREAM, 0); CHECK(dest.sock != SOCKET_INVALID);
struct sockaddr_in address = {0}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
CHECK(!bind(dest.sock, (struct sockaddr *)&address, sizeof(address)) && !listen(dest.sock, 4));
socklen_t size = sizeof(address); CHECK(!getsockname(dest.sock, (struct sockaddr *)&address, &size));
socket_set_nonblocking(dest.sock);
CHECK(uasync_add_socket_t(ua, dest.sock, destination_accept, NULL, NULL, "reality_test_listener", &dest));
snprintf(cfg.dest, sizeof(cfg.dest), "127.0.0.1:%u", ntohs(address.sin_port));
uint16_t port = 32000 + (uint16_t)(time(NULL) % 1000);
struct stcp_server *sa = stcp_server_create(ua, port, &keys, NULL, &a, unexpected_stcp, NULL, NULL, NULL, AF_INET);
struct stcp_server *sb = stcp_server_create(ua, port + 1, &keys, NULL, &b, unexpected_stcp, NULL, NULL, NULL, AF_INET);
CHECK(sa && sb); stcp_server_set_reality(sa, &cfg); stcp_server_set_reality(sb, &cfg);
uint8_t ch[REALITY_MAX_CH_SIZE], answer[4096]; size_t cn;
fake_now = (uint32_t)time(NULL);
CHECK(!reality_client_hello_build_at(&cc, fake_now + 30, ch, sizeof(ch), &cn));
socket_t first = connect_to(port); CHECK(first != SOCKET_INVALID && send(first, ch, cn, 0) == (ssize_t)cn);
poll_some(ua);
CHECK(recv(first, answer, sizeof(answer), 0) >= 127 && answer[0] == 22);
/* Через 31 секунду тот же future timestamp ещё допустим: replay обязан оставаться в кеше. */
fake_now += 31;
uint8_t split[4096]; size_t first_part = 11, rest = cn - 5 - first_part;
memcpy(split, ch, 5); split[3] = 0; split[4] = first_part;
memcpy(split + 5, ch + 5, first_part);
size_t pos = 5 + first_part;
memcpy(split + pos, ch, 5); split[pos + 2] = 3; split[pos + 3] = (uint8_t)(rest >> 8); split[pos + 4] = (uint8_t)rest;
memcpy(split + pos + 5, ch + 5 + first_part, rest);
pos += 5 + rest;
const uint8_t tail[] = {20, 3, 3, 0, 1, 1, 23, 3, 3, 0, 4, 1, 2, 3, 4};
memcpy(split + pos, tail, sizeof(tail)); pos += sizeof(tail);
socket_t replay = connect_to(port); CHECK(replay != SOCKET_INVALID);
CHECK(send(replay, split, 7, 0) == 7); poll_some(ua);
CHECK(send(replay, split + 7, pos - 7, 0) == (ssize_t)(pos - 7)); poll_some(ua);
CHECK(dest.count == 1 && dest.peers[0].len == pos && !memcmp(dest.peers[0].data, split, pos));
CHECK(a.reality_owner.relay_count == 1);
socket_t other = connect_to(port + 1); CHECK(other != SOCKET_INVALID && send(other, ch, cn, 0) == (ssize_t)cn);
poll_some(ua);
CHECK(recv(other, answer, sizeof(answer), 0) >= 127 && answer[0] == 22 && dest.count == 1);
cc.short_id[0] = 1;
CHECK(!reality_client_hello_build_at(&cc, fake_now, ch, sizeof(ch), &cn));
socket_t unauthorized = connect_to(port + 1);
CHECK(unauthorized != SOCKET_INVALID && send(unauthorized, ch, cn, 0) == (ssize_t)cn);
poll_some(ua); CHECK(dest.count == 2 && b.reality_owner.relay_count == 1);
stcp_server_destroy(sb); reality_owner_cleanup(&b.reality_owner); poll_some(ua);
CHECK(a.reality_owner.relay_count == 1 && dest.peers[1].sock == SOCKET_INVALID);
CHECK(send(replay, tail, sizeof(tail), 0) == sizeof(tail)); poll_some(ua);
CHECK(dest.peers[0].len == pos + sizeof(tail) && !memcmp(dest.peers[0].data + pos, tail, sizeof(tail)));
stcp_server_destroy(sa); reality_owner_cleanup(&a.reality_owner); poll_some(ua);
socket_close_wrapper(first); socket_close_wrapper(replay); socket_close_wrapper(other); socket_close_wrapper(unauthorized);
for (unsigned i = 0; i < dest.count; i++) if (dest.peers[i].sock != SOCKET_INVALID) {
uasync_remove_socket_t(ua, dest.peers[i].sock); socket_close_wrapper(dest.peers[i].sock);
}
uasync_remove_socket_t(ua, dest.sock); socket_close_wrapper(dest.sock); uasync_destroy(ua, 1);
CHECK(u_get_allocated_count() == allocated);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "fragmented replay, future expiry, exact fallback bytes, instance isolation and teardown passed");
return 0;
}

24
tests/test_reality_hello.c

@ -43,7 +43,7 @@ static void init_client_cfg(struct reality_client_config *cc,
memcpy(cc->short_id, short_id, REALITY_SHORT_ID_SIZE);
memcpy(cc->version, version, REALITY_VERSION_SIZE);
snprintf(cc->server_name, sizeof(cc->server_name), "www.microsoft.com");
cc->fingerprint = REALITY_FP_CHROME;
cc->fingerprint = REALITY_FP_OPENSSL;
}
static void init_server_cfg(struct reality_server_config *sc,
@ -57,7 +57,7 @@ static void init_server_cfg(struct reality_server_config *sc,
sc->short_id_count = 1;
memcpy(sc->version, version, REALITY_VERSION_SIZE);
sc->time_window_sec = window;
sc->fingerprint = REALITY_FP_CHROME;
sc->fingerprint = REALITY_FP_OPENSSL;
}
// Собирает структурно-валидный ClientHello (TLS record + handshake), тело которого
@ -196,7 +196,7 @@ int main(void) {
}
// ── Сценарий 8: fingerprint-геттер ──
CHECK(reality_fingerprint_get(REALITY_FP_CHROME) != NULL, "fingerprint chrome found");
CHECK(reality_fingerprint_get(REALITY_FP_OPENSSL) != NULL, "fingerprint openssl found");
CHECK(reality_fingerprint_get(999) == NULL, "unknown fingerprint = NULL");
// ── Сценарий 9: невалидные аргументы ──
@ -230,8 +230,8 @@ int main(void) {
ks[6] = 0x00; ks[7] = 0x1d; // group = X25519
ks[8] = 0x00; ks[9] = 0x20; // klen = 32 (тела нет)
ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, sizeof(ks));
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_AUTH,
"key_share truncated X25519 entry → AUTH");
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_FORMAT,
"key_share truncated X25519 entry → FORMAT");
// цепочка записей не-X25519 с klen=0 (в границах) → AUTH, без OOB
memset(ks, 0, sizeof(ks));
@ -241,8 +241,8 @@ int main(void) {
ks[6] = 0x00; ks[7] = 0x17; // group = secp256r1
ks[8] = 0x00; ks[9] = 0x00; // klen = 0
ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, sizeof(ks));
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_AUTH,
"key_share without X25519 → AUTH");
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_FORMAT,
"key_share zero-length entry → FORMAT");
}
// ── Сценарий 10: парсинг секции [reality] из конфига ──
@ -259,7 +259,7 @@ int main(void) {
"private_key=2222222222222222222222222222222222222222222222222222222222222222\n"
"version=2.3.4\n"
"time_window=45\n"
"fingerprint=chrome\n";
"fingerprint=openssl\n";
struct utun_config *uc = parse_config_from_buf(cfg_text, strlen(cfg_text), "mem");
CHECK(uc != NULL, "parse config with [reality]");
if (uc) {
@ -270,7 +270,7 @@ int main(void) {
CHECK(uc->global.reality.short_id_count == 2, "short_ids parsed");
CHECK(uc->global.reality.version[0] == 2 && uc->global.reality.version[1] == 3 && uc->global.reality.version[2] == 4, "version parsed");
CHECK(uc->global.reality.time_window_sec == 45, "time_window parsed");
CHECK(uc->global.reality.fingerprint == REALITY_FP_CHROME, "fingerprint parsed");
CHECK(uc->global.reality.fingerprint == REALITY_FP_OPENSSL, "fingerprint parsed");
free_config(uc);
}
}
@ -288,7 +288,7 @@ int main(void) {
"short_id=0102030405060708\n"
"public_key=2222222222222222222222222222222222222222222222222222222222222222\n"
"version=2.3.4\n"
"fingerprint=chrome\n"
"fingerprint=openssl\n"
"[client: c2]\n"
"peer_public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"link=bind_srv:5.6.7.8:1444\n"
@ -306,7 +306,7 @@ int main(void) {
if (c1) {
CHECK(c1->reality_enabled == 1, "c1 reality enabled");
CHECK(strcmp(c1->reality.server_name, "www.microsoft.com") == 0, "c1 server_name");
CHECK(c1->reality.fingerprint == REALITY_FP_CHROME, "c1 fingerprint");
CHECK(c1->reality.fingerprint == REALITY_FP_OPENSSL, "c1 fingerprint");
CHECK(c1->reality.version[0] == 2 && c1->reality.version[1] == 3 && c1->reality.version[2] == 4, "c1 version parsed");
{ uint8_t sid[8] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
CHECK(memcmp(c1->reality.short_id, sid, 8) == 0, "c1 short_id"); }
@ -320,7 +320,7 @@ int main(void) {
}
if (c2) {
CHECK(c2->reality_enabled == 1, "c2 reality enabled");
CHECK(c2->reality.version[0] == 1 && c2->reality.version[1] == 0 && c2->reality.version[2] == 0, "c2 version default 1.0.0");
CHECK(c2->reality.version[0] == 2 && c2->reality.version[1] == 0 && c2->reality.version[2] == 0, "c2 version default 2.0.0");
CHECK(c2->links != NULL && c2->links->local_srv != NULL, "c2 link с bind (local_srv set)");
if (c2->links && c2->links->local_srv)
CHECK(strcmp(c2->links->local_srv->name, "bind_srv") == 0, "c2 bind socket name");

118
tests/test_reality_record.c

@ -0,0 +1,118 @@
/* Независимые HKDF/GCM-векторы и проверка привязки записей к обоим Hello. */
#include "reality.h"
#include "../lib/debug_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/crypto.h>
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL line=%d: %s", __LINE__, #x); return 1; } } while (0)
static size_t unhex(const char *s, uint8_t *out) {
size_t n = 0;
while (s[0] && s[1]) {
unsigned value;
if (sscanf(s, "%2x", &value) != 1) break;
out[n++] = (uint8_t)value;
s += 2;
}
return n;
}
static int vectors(void) {
struct reality_traffic send = {0}, recv = {0};
uint8_t secret[32], wire[REALITY_RECORD_MAX], plain[REALITY_INNER_MAX], expected[64], type;
for (unsigned i = 0; i < 32; i++) secret[i] = i;
CHECK(!reality_traffic_init(&send, secret));
CHECK(unhex("1519da96243cb6965fbfa3b28ae25a27", expected) == 16 && !memcmp(send.key, expected, 16));
CHECK(unhex("16e5473250aa12b61fbacec7", expected) == 12 && !memcmp(send.iv, expected, 12));
const char *golden[] = {
"1703030018a128808be00c4bdf042cf5ae940649c8f0d4701075afa09b",
"170303001811fa393d1f1db1a3ee840870bedcc6f5c9387a00c9b41c84"
};
const uint8_t data[] = {1, 2, 3, 0};
size_t n, decoded;
for (unsigned i = 0; i < 2; i++) {
CHECK(!reality_traffic_init(&send, secret));
recv = send;
send.sequence = recv.sequence = i ? 7 : 0;
CHECK(!reality_record_seal(&send, 23, data, sizeof(data), 3, wire, sizeof(wire), &n));
CHECK(unhex(golden[i], expected) == n && !memcmp(wire, expected, n));
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type));
CHECK(decoded == sizeof(data) && type == 23 && !memcmp(plain, data, decoded));
CHECK(reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) < 0); // повтор sequence
recv.sequence--;
wire[n - 1] ^= 1;
CHECK(reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) < 0);
CHECK(recv.sequence == (i ? 7 : 0));
}
CHECK(!reality_traffic_init(&send, secret)); recv = send;
memset(plain, 0xaa, sizeof(plain));
CHECK(!reality_record_seal(&send, 23, plain, 16384, 0, wire, sizeof(wire), &n));
CHECK(n == REALITY_RECORD_MAX);
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) && decoded == 16384);
CHECK(reality_record_seal(&send, 23, plain, 16384, 1, wire, sizeof(wire), &n) < 0);
CHECK(reality_record_seal(&send, 22, NULL, 0, 0, wire, sizeof(wire), &n) < 0);
CHECK(!reality_record_seal(&send, 23, NULL, 0, 0, wire, sizeof(wire), &n));
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) && decoded == 0);
send.sequence = REALITY_KEY_LIMIT;
CHECK(reality_record_seal(&send, 23, data, sizeof(data), 0, wire, sizeof(wire), &n) < 0);
uint8_t old_key[16]; memcpy(old_key, send.key, 16);
CHECK(!reality_traffic_update(&send) && send.sequence == 0 && send.generation == 1 && memcmp(old_key, send.key, 16));
recv.sequence = REALITY_KEY_LIMIT;
CHECK(!reality_traffic_update(&recv));
CHECK(!reality_record_seal(&send, 23, data, sizeof(data), 0, wire, sizeof(wire), &n));
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type));
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "HKDF, GCM nonce/AAD, replay, limits, empty DATA and KeyUpdate vectors passed");
return 0;
}
static int hello_session(void) {
struct reality_session client = {0}, server = {0};
struct reality_client_config cc;
struct reality_server_config sc = {0};
reality_client_config_set_defaults(&cc);
CHECK(!reality_generate_keypair(sc.static_privkey, cc.server_static_pubkey));
strcpy(cc.server_name, "www.microsoft.com");
sc.version[0] = 2; sc.short_id_count = 1; sc.time_window_sec = 30;
uint8_t ch[REALITY_MAX_CH_SIZE], sh[REALITY_MAX_SH_SIZE], finished[32], check[32];
size_t cn, sn;
CHECK(!reality_session_client_start(&client, &cc, 1000, ch, sizeof(ch), &cn));
CHECK(!reality_session_server_start(&server, &sc, 1000, ch, cn, sh, sizeof(sh), &sn));
const size_t offsets[] = {0, 4, 5, 8, 10, 43, 44, 76, 77, 78, 80, 84, 86, 88, 92, 94};
for (unsigned i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) {
sh[offsets[i]] ^= 1;
CHECK(reality_session_client_accept(&client, sh, sn) != REALITY_OK);
sh[offsets[i]] ^= 1;
}
CHECK(!reality_session_client_accept(&client, sh, sn));
CHECK(!memcmp(client.send.key, server.recv.key, 16) && !memcmp(client.recv.key, server.send.key, 16));
CHECK(memcmp(client.send.key, client.recv.key, 16));
CHECK(!reality_finished(&server, 1, finished) && !reality_finished(&client, 0, check) && !memcmp(finished, check, 32));
const uint8_t ee[] = {8, 0, 0, 2, 0, 0};
CHECK(!reality_transcript_add(&client, ee, sizeof(ee)));
CHECK(!reality_finished(&client, 0, check) && memcmp(finished, check, 32));
CHECK(!reality_transcript_add(&server, ee, sizeof(ee)));
CHECK(!reality_finished(&server, 1, finished) && !reality_finished(&client, 0, check) && !memcmp(finished, check, 32));
for (unsigned i = 0; i < 32; i++) CHECK(!client.auth_key[i] && !server.auth_key[i] && !client.ephemeral_private[i]);
reality_session_cleanup(&client); reality_session_cleanup(&server);
/* Все открытые байты совпадают с локальным эталоном OpenSSL; динамические поля исключены. */
FILE *f = fopen("fixtures/reality_openssl_hello.hex", "r");
if (!f) f = fopen("tests/fixtures/reality_openssl_hello.hex", "r");
CHECK(f);
char line[2048]; uint8_t reference[1024]; size_t rn = 0;
while (fgets(line, sizeof(line), f)) if (line[0] != '#') { rn = unhex(line, reference); break; }
fclose(f);
CHECK(rn == cn && cn == 263);
memset(ch + 11, 0, 32); memset(reference + 11, 0, 32);
memset(ch + 44, 0, 32); memset(reference + 44, 0, 32);
memset(ch + cn - 41, 0, 32); memset(reference + cn - 41, 0, 32);
CHECK(!memcmp(ch, reference, cn));
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "Hello validation, independent traffic keys, transcript Finished and OpenSSL profile passed");
return 0;
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
return vectors() || hello_session();
}

147
tests/test_reality_tls.c

@ -0,0 +1,147 @@
/* Обычный TLS 1.3 и HTTP-запрос проходят через REALITY fallback на локальный HTTPS. */
#define OPENSSL_API_COMPAT 0x10100000L
#include "stcp_server.h"
#include "utun_instance.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/err.h>
#include <string.h>
#include <time.h>
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL line=%d: %s", __LINE__, #x); return 1; } } while (0)
struct https_peer {
struct UASYNC *ua;
SSL *ssl;
void *socket_id;
socket_t sock;
uint8_t ready, done, failed;
uint8_t response[256]; size_t response_len;
};
struct https_server { struct UASYNC *ua; SSL_CTX *ctx; socket_t sock; struct https_peer peer; };
static const char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello";
static void https_progress(socket_t sock, void *arg) {
struct https_peer *p = arg;
int rc;
if (!p->ready) {
rc = SSL_accept(p->ssl);
if (rc == 1) p->ready = 1;
else {
int error = SSL_get_error(p->ssl, rc);
if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
uasync_set_socket_write(p->ua, p->socket_id, error == SSL_ERROR_WANT_WRITE);
return;
}
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS handshake failed: ssl=%d", error);
p->failed = 1;
return;
}
}
if (!p->done) {
uint8_t request[512];
rc = SSL_read(p->ssl, request, sizeof(request));
if (rc > 0) {
if (rc < 4 || memcmp(request, "GET ", 4)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid HTTP request"); p->failed = 1; return; }
rc = SSL_write(p->ssl, response, sizeof(response) - 1);
if (rc != sizeof(response) - 1) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS response write failed"); p->failed = 1; return; }
p->done = 1;
uasync_set_socket_write(p->ua, p->socket_id, 0);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "local HTTPS received HTTP request through relay");
} else {
int error = SSL_get_error(p->ssl, rc);
if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS read failed: ssl=%d openssl=%lu", error, ERR_get_error());
p->failed = 1;
}
uasync_set_socket_write(p->ua, p->socket_id, error == SSL_ERROR_WANT_WRITE);
}
}
(void)sock;
}
static void https_accept(socket_t sock, void *arg) {
struct https_server *s = arg;
struct https_peer *p = &s->peer;
p->sock = accept(sock, NULL, NULL); p->ua = s->ua;
if (p->sock == SOCKET_INVALID) { p->failed = 1; return; }
socket_set_nonblocking(p->sock);
p->ssl = SSL_new(s->ctx);
if (!p->ssl || SSL_set_fd(p->ssl, p->sock) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS SSL setup failed"); p->failed = 1; return; }
p->socket_id = uasync_add_socket_t(s->ua, p->sock, https_progress, https_progress, NULL, "test_https", p);
if (!p->socket_id) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS socket registration failed"); p->failed = 1; }
}
static void unexpected_stcp(struct stcp_conn *c, void *arg) {
(void)c; (void)arg;
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "ordinary HTTPS client entered STCP");
}
static int client_step(SSL *client, int rc) {
if (rc > 0) return rc;
int error = SSL_get_error(client, rc);
return error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE ? 0 : -1;
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
size_t allocated = u_get_allocated_count();
struct UASYNC *ua = uasync_create(); CHECK(ua);
struct UTUN_INSTANCE inst = {0}; inst.ua = ua;
struct SC_MYKEYS keys; CHECK(sc_generate_keypair(&keys) == SC_OK);
EVP_PKEY_CTX *key_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); EVP_PKEY *key = NULL;
CHECK(key_ctx && EVP_PKEY_keygen_init(key_ctx) == 1 && EVP_PKEY_keygen(key_ctx, &key) == 1);
X509 *cert = X509_new(); CHECK(cert);
CHECK(X509_set_version(cert, 2) && ASN1_INTEGER_set(X509_get_serialNumber(cert), 1));
CHECK(X509_gmtime_adj(X509_getm_notBefore(cert), 0) && X509_gmtime_adj(X509_getm_notAfter(cert), 3600));
X509_NAME *name = X509_get_subject_name(cert);
CHECK(X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char *)"localhost", -1, -1, 0));
CHECK(X509_set_issuer_name(cert, name) && X509_set_pubkey(cert, key) && X509_sign(cert, key, NULL) > 0);
struct https_server server = {0}; server.ua = ua; server.peer.sock = SOCKET_INVALID;
server.ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX *client_ctx = SSL_CTX_new(TLS_client_method());
CHECK(server.ctx && client_ctx && SSL_CTX_use_certificate(server.ctx, cert) && SSL_CTX_use_PrivateKey(server.ctx, key));
CHECK(SSL_CTX_set_min_proto_version(server.ctx, TLS1_3_VERSION) && SSL_CTX_set_min_proto_version(client_ctx, TLS1_3_VERSION));
CHECK(SSL_CTX_set1_groups_list(client_ctx, "X25519") && SSL_CTX_set_ciphersuites(client_ctx, "TLS_AES_128_GCM_SHA256"));
SSL_CTX_set_num_tickets(server.ctx, 0); SSL_CTX_set_verify(client_ctx, SSL_VERIFY_NONE, NULL);
server.sock = socket(AF_INET, SOCK_STREAM, 0); CHECK(server.sock != SOCKET_INVALID);
struct sockaddr_in address = {0}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
CHECK(!bind(server.sock, (struct sockaddr *)&address, sizeof(address)) && !listen(server.sock, 1));
socklen_t size = sizeof(address); CHECK(!getsockname(server.sock, (struct sockaddr *)&address, &size));
socket_set_nonblocking(server.sock);
CHECK(uasync_add_socket_t(ua, server.sock, https_accept, NULL, NULL, "test_https_listener", &server));
struct reality_config cfg; reality_config_set_defaults(&cfg);
cfg.enabled = 1; cfg.short_id_count = 1;
uint8_t pub[32]; CHECK(!reality_generate_keypair(cfg.private_key, pub));
snprintf(cfg.dest, sizeof(cfg.dest), "127.0.0.1:%u", ntohs(address.sin_port));
uint16_t port = 34000 + (uint16_t)(time(NULL) % 1000);
struct stcp_server *reality = stcp_server_create(ua, port, &keys, NULL, &inst, unexpected_stcp, NULL, NULL, NULL, AF_INET);
CHECK(reality); stcp_server_set_reality(reality, &cfg);
socket_t sock = socket(AF_INET, SOCK_STREAM, 0); CHECK(sock != SOCKET_INVALID);
address.sin_port = htons(port); CHECK(!connect(sock, (struct sockaddr *)&address, sizeof(address)));
socket_set_nonblocking(sock);
SSL *client = SSL_new(client_ctx); CHECK(client && SSL_set_fd(client, sock));
CHECK(SSL_set_tlsext_host_name(client, "localhost"));
int ready = 0;
for (unsigned i = 0; !ready && !server.peer.failed && i < 1000; i++) {
ready = client_step(client, SSL_connect(client)); CHECK(ready >= 0); uasync_poll(ua, 1);
}
CHECK(ready == 1 && SSL_version(client) == TLS1_3_VERSION);
const char request[] = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
CHECK(SSL_write(client, request, sizeof(request) - 1) == sizeof(request) - 1);
uint8_t data[256]; size_t received = 0;
for (unsigned i = 0; received < sizeof(response) - 1 && !server.peer.failed && i < 1000; i++) {
int n = client_step(client, SSL_read(client, data + received, sizeof(data) - received));
CHECK(n >= 0); received += n; uasync_poll(ua, 1);
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "HTTPS result: ready=%u done=%u failed=%u received=%zu", server.peer.ready, server.peer.done, server.peer.failed, received);
CHECK(!server.peer.failed && received == sizeof(response) - 1 && !memcmp(data, response, received));
CHECK(inst.reality_owner.relay_count == 1);
SSL_free(client); socket_close_wrapper(sock);
stcp_server_destroy(reality); reality_owner_cleanup(&inst.reality_owner);
if (server.peer.sock != SOCKET_INVALID) {
uasync_remove_socket_t(ua, server.peer.sock); SSL_free(server.peer.ssl); socket_close_wrapper(server.peer.sock);
}
uasync_remove_socket_t(ua, server.sock); socket_close_wrapper(server.sock);
SSL_CTX_free(client_ctx); SSL_CTX_free(server.ctx); X509_free(cert); EVP_PKEY_free(key); EVP_PKEY_CTX_free(key_ctx);
uasync_drain_immediate(ua); uasync_destroy(ua, 1); CHECK(u_get_allocated_count() == allocated);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "real TLS 1.3 + HTTP over fallback passed without leaked resources");
return 0;
}

209
tests/test_stcp.c

@ -12,6 +12,7 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
static int tests_passed = 0, tests_total = 0;
static struct SC_MYKEYS s_keys, c_keys;
@ -22,6 +23,9 @@ static struct SC_MYKEYS s_keys, c_keys;
#define WIRE_PADDING 32
#define WIRE_HANDSHAKE_SIZE (STCP_HS_CLIENT_MIN + WIRE_PADDING)
static int capture_wire, salt_number;
static int reality_capture, reality_send_calls, reality_tamper, reality_fault, reality_block_recv;
static size_t reality_wire_len[2];
static uint8_t reality_wire[2][1600000];
static uint16_t capture_port;
static size_t wire_count;
static struct {
@ -30,6 +34,21 @@ static struct {
uint8_t bytes[2][WIRE_HANDSHAKE_SIZE];
} wire[2];
void *__real_u_malloc_impl(uint32_t, const char *);
void *__real_u_calloc_impl(uint32_t, uint32_t, const char *);
static int reality_allocation_fail(const char *location) {
if (!reality_fault || !strstr(location, "reality_io.c:")) return 0;
if (--reality_fault) return 0;
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "injected allocation failure: %s", location);
return 1;
}
void *__wrap_u_malloc_impl(uint32_t size, const char *location) {
return reality_allocation_fail(location) ? NULL : __real_u_malloc_impl(size, location);
}
void *__wrap_u_calloc_impl(uint32_t count, uint32_t size, const char *location) {
return reality_allocation_fail(location) ? NULL : __real_u_calloc_impl(count, size, location);
}
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) {
@ -39,8 +58,37 @@ int __wrap_random_bytes(uint8_t* out, size_t len) {
return __real_random_bytes(out, len);
}
ssize_t __real_recv(socket_t, void *, size_t, int);
ssize_t __wrap_recv(socket_t sock, void *buf, size_t len, int flags) {
if (reality_block_recv) {
struct sockaddr_in local; socklen_t size = sizeof(local);
if (!getsockname(sock, (struct sockaddr *)&local, &size) && ntohs(local.sin_port) == capture_port) {
errno = EAGAIN; return -1;
}
}
return __real_recv(sock, buf, len, flags);
}
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 (reality_capture) {
struct sockaddr_in local; socklen_t alen = sizeof(local);
if (getsockname(sock, (struct sockaddr *)&local, &alen)) return -1;
int direction = ntohs(local.sin_port) == capture_port;
if (++reality_send_calls % 17 == 0) { errno = EAGAIN; return -1; }
uint8_t part[37]; size_t take = len > sizeof(part) ? sizeof(part) : len;
memcpy(part, buf, take);
if (reality_tamper && direction && len >= 7 && part[0] == 23 && part[1] == 3 && part[2] == 3) {
part[6] ^= 1;
reality_tamper = 0;
}
ssize_t n = __real_send(sock, part, take, flags);
if (n > 0 && reality_wire_len[direction] + (size_t)n <= sizeof(reality_wire[direction])) {
memcpy(reality_wire[direction] + reality_wire_len[direction], part, (size_t)n);
reality_wire_len[direction] += (size_t)n;
}
return n;
}
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) {
@ -102,6 +150,7 @@ static void peer_rx_cb(struct ll_queue *q, void *arg) {
if (need > p->accum_cap) { p->accum_cap = need + 4096; p->accum = u_realloc(p->accum, p->accum_cap); }
memcpy(p->accum + p->accum_len, e->dgram, e->len);
p->accum_len += e->len;
queue_dgram_free(e);
queue_entry_free(e);
queue_resume_callback(q);
if (p->conn) stcp_rx_resume_if_needed(p->conn);
@ -187,7 +236,7 @@ static int test1_sizes(void) {
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);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -225,7 +274,7 @@ static int test2_many(void) {
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);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -253,7 +302,7 @@ static int test3_wrong_key(void) {
peer_cleanup(&srv); peer_cleanup(&cli);
stcp_client_destroy(sc); stcp_server_destroy(ss);
uasync_destroy(ua, 1);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -284,7 +333,7 @@ static int test4_close(void) {
peer_cleanup(&srv); peer_cleanup(&cli);
stcp_client_destroy(sc); stcp_server_destroy(ss);
uasync_destroy(ua, 1);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -352,7 +401,7 @@ static int test5_multi(void) {
stcp_client_destroy(clients[i]);
}
stcp_server_destroy(ss);
uasync_destroy(ua, 1);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -386,7 +435,7 @@ static int test6_interleaved(void) {
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);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -425,7 +474,7 @@ static int test7_bulk_4mb(void) {
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);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -455,7 +504,7 @@ static int test8_srv_recv_close(void) {
peer_cleanup(&srv); peer_cleanup(&cli);
stcp_client_destroy(sc); stcp_server_destroy(ss);
uasync_destroy(ua, 1); // flush deferred → stcp_conn_free(srv_conn)
uasync_drain_immediate(ua); uasync_destroy(ua, 1); // flush deferred → stcp_conn_free(srv_conn)
return 0;
}
@ -543,7 +592,7 @@ static int test9_raw_handshake_ok(void) {
peer_cleanup(&srv);
if (srv.conn) stcp_conn_free(srv.conn);
stcp_server_destroy(ss);
uasync_destroy(ua, 1);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -570,7 +619,7 @@ static int test10_oversized_padding(void) {
peer_cleanup(&srv);
srv.conn = NULL; // уже освобождён deferred-close (allocated=1)
stcp_server_destroy(ss);
uasync_destroy(ua, 1);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -597,7 +646,7 @@ static int test11_garbage_handshake(void) {
peer_cleanup(&srv);
srv.conn = NULL;
stcp_server_destroy(ss);
uasync_destroy(ua, 1);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
@ -627,7 +676,7 @@ static int test12_padding_boundaries(void) {
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);
stcp_client_destroy(sc); stcp_server_destroy(ss); uasync_drain_immediate(ua); uasync_destroy(ua, 1);
}
return 0;
}
@ -706,7 +755,7 @@ static int test13_salted_handshakes(void) {
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);
stcp_server_destroy(ss); uasync_drain_immediate(ua); uasync_destroy(ua, 1);
}
return 0;
}
@ -726,16 +775,141 @@ static int test14_modified_salt(void) {
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);
socket_close_wrapper(sock); stcp_server_destroy(ss); uasync_drain_immediate(ua); uasync_destroy(ua, 1);
return 0;
}
static int test15_reality_records(void) {
for (int round = 0; round < 4; round++) {
struct UASYNC *ua = uasync_create(); TASSERT(ua);
struct test_peer srv = {0}, cli = {0};
struct UTUN_INSTANCE si = {0}, ci = {0};
si.ua = ci.ua = ua; si.my_keys = s_keys; ci.my_keys = c_keys;
si.padding.min = si.padding.max = round ? ETCP_PADDING_MAX : 0;
ci.padding.min = ci.padding.max = round ? 0 : ETCP_PADDING_MAX;
struct reality_config cfg; reality_config_set_defaults(&cfg);
struct reality_client_config cc; reality_client_config_set_defaults(&cc);
cfg.enabled = 1; cfg.short_id_count = 1;
TASSERT(!reality_generate_keypair(cfg.private_key, cc.server_static_pubkey));
strcpy(cc.server_name, "www.microsoft.com");
uint16_t port = BASE_PORT + 17;
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);
stcp_server_set_reality(ss, &cfg);
#ifdef __linux__
capture_port = port; reality_capture = 1; reality_send_calls = 0; reality_tamper = round == 2;
memset(reality_wire_len, 0, sizeof(reality_wire_len));
#endif
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, 3000, &cc, NULL, &ci); TASSERT(sc);
for (int j = 0; !cli.closed && (!srv.ready || !cli.ready) && j < 10000; j++) uasync_poll(ua, 1);
#ifdef __linux__
if (round == 2) {
TASSERT(cli.closed && !cli.ready && !srv.ready);
reality_capture = 0;
stcp_client_destroy(sc); stcp_server_destroy(ss);
reality_owner_cleanup(&si.reality_owner); uasync_drain_immediate(ua); uasync_destroy(ua, 1);
continue;
}
#endif
TASSERT(srv.ready && cli.ready && !srv.closed && !cli.closed);
uint8_t payload[65535];
for (unsigned i = 0; i < sizeof(payload); i++) payload[i] = (uint8_t)(i * 7);
int messages = round == 3 ? 100 : 1;
size_t payload_len = round == 3 ? 1024 : sizeof(payload);
#ifdef __linux__
if (round == 3) reality_capture = 0;
#endif
if (round == 3) queue_set_callback(srv.rx, NULL, NULL);
for (int i = 0; i < messages; i++) {
TASSERT(!peer_send(&cli, payload, payload_len) && !peer_send(&srv, payload, payload_len));
}
TASSERT(!peer_send(&cli, payload, 0) && !peer_send(&srv, payload, 0));
if (round == 3) {
for (int j = 0; !srv.conn->rx_paused && j < 100000; j++) uasync_poll(ua, 0);
TASSERT(srv.conn->rx_paused && srv.rx->count > STCP_RX_QUEUE_MAX_PACKETS);
#ifdef __linux__
reality_block_recv = 1;
#endif
queue_set_callback(srv.rx, peer_rx_cb, &srv); queue_resume_callback(srv.rx);
for (unsigned i = 0; i < 100; i++) uasync_poll(ua, 0);
#ifdef __linux__
TASSERT(srv.msg_count > STCP_RX_QUEUE_MAX_PACKETS + 1);
reality_block_recv = 0;
#endif
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "backpressure resume delivered %d frames with socket recv blocked", srv.msg_count);
}
for (int j = 0; (srv.msg_count < messages + 1 || cli.msg_count < messages + 1) && j < 100000; j++) uasync_poll(ua, 0);
TASSERT(srv.accum_len == payload_len * messages && cli.accum_len == payload_len * messages);
for (int i = 0; i < messages; i++) {
TASSERT(!memcmp(srv.accum + i * payload_len, payload, payload_len));
TASSERT(!memcmp(cli.accum + i * payload_len, payload, payload_len));
}
#ifdef __linux__
reality_capture = 0;
for (int direction = 0; round != 3 && direction < 2; direction++) {
size_t offset = 0, records = 0, ccs = 0, updates = 0;
while (offset < reality_wire_len[direction]) {
const uint8_t *record = reality_wire[direction] + offset;
TASSERT(reality_wire_len[direction] - offset >= 5);
size_t len = 5 + ((size_t)record[3] << 8) + record[4];
TASSERT(len <= reality_wire_len[direction] - offset);
if (!records) TASSERT(record[0] == 22 && len == (direction ? 127 : 263));
else if (record[0] == 20) { TASSERT(len == 6 && record[5] == 1 && !ccs++); }
else TASSERT(record[0] == 23 && record[1] == 3 && record[2] == 3 && len >= 22 && len <= REALITY_RECORD_MAX);
if (record[0] == 23 && len == 27) updates++;
offset += len; records++;
}
TASSERT(records >= 8 && ccs == 1);
#if REALITY_KEY_LIMIT < 16
TASSERT(updates >= 1);
#endif
}
#endif
peer_cleanup(&srv); peer_cleanup(&cli);
stcp_conn_free(srv.conn); stcp_client_destroy(sc); stcp_server_destroy(ss);
reality_owner_cleanup(&si.reality_owner); uasync_drain_immediate(ua); uasync_destroy(ua, 1);
}
return 0;
}
#ifdef __linux__
static int test16_reality_allocation_failures(void) {
for (int failure = 1; failure <= 18; failure++) {
size_t allocated = u_get_allocated_count();
struct UASYNC *ua = uasync_create(); TASSERT(ua);
struct test_peer srv = {0}, cli = {0};
struct UTUN_INSTANCE si = {0}; si.ua = ua; si.my_keys = s_keys;
struct reality_config cfg; reality_config_set_defaults(&cfg);
struct reality_client_config cc; reality_client_config_set_defaults(&cc);
cfg.enabled = 1; cfg.short_id_count = 1;
TASSERT(!reality_generate_keypair(cfg.private_key, cc.server_static_pubkey));
strcpy(cc.server_name, "www.microsoft.com");
struct stcp_server *ss = stcp_server_create(ua, BASE_PORT + 18, &s_keys, NULL, &si,
server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
stcp_server_set_reality(ss, &cfg);
reality_fault = failure;
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", BASE_PORT + 18, &c_keys, s_keys.public_key,
NULL, 0, NULL, 0, 200, 0, client_ready_cb, &cli,
NULL, NULL, peer_close_cb, &cli, NULL, 3000, &cc, NULL, NULL); TASSERT(sc);
for (unsigned i = 0; !cli.closed && !srv.closed && i < 1000; i++) uasync_poll(ua, 1);
TASSERT(!reality_fault && !cli.ready && !srv.ready && (cli.closed || srv.closed));
stcp_client_destroy(sc); stcp_server_destroy(ss);
reality_owner_cleanup(&si.reality_owner); uasync_drain_immediate(ua); uasync_destroy(ua, 1);
TASSERT(u_get_allocated_count() == allocated);
}
return 0;
}
#endif
// ======================= main =======================
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_GENERAL | DEBUG_CATEGORY_SOCKET | DEBUG_CATEGORY_CRYPTO);
debug_set_categories(DEBUG_CATEGORY_GENERAL | DEBUG_CATEGORY_SOCKET | DEBUG_CATEGORY_CRYPTO | DEBUG_CATEGORY_REALITY);
size_t allocated = u_get_allocated_count();
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "============================================");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== STCP Integration Tests ===");
@ -759,8 +933,13 @@ int main(void) {
TRUN(test13_salted_handshakes);
#endif
TRUN(test14_modified_salt);
TRUN(test15_reality_records);
#ifdef __linux__
TRUN(test16_reality_allocation_failures);
#endif
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "============================================");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Results: %d/%d passed", tests_passed, tests_total);
TASSERT(u_get_allocated_count() == allocated);
return tests_passed == tests_total ? 0 : 1;
}

27
tests/test_stcp_ping.c

@ -1,5 +1,6 @@
// test_stcp_ping.c — TCP-ping (etcp_send_tcp_ping / stcp_ping_send) end-to-end check
#include "stcp_client.h"
#include "stcp_server.h"
#include "etcp_connections.h"
#include "etcp.h"
#include "topo_group.h"
@ -21,6 +22,12 @@ static void ping_cb(int success, uint16_t rtt, void *arg, uint64_t nonce,
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "TCP ping cb: success=%d rtt=%u", success, rtt);
}
static void unexpected_ready(struct stcp_conn *c, void *arg) {
(void)c; (void)arg;
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "TCP ping unexpectedly entered DATA callback");
ping_ok = 0;
}
static char *build_config(const char *node_id, const char *priv, const char *pub, int port) {
static char buf[512];
snprintf(buf, sizeof(buf),
@ -71,7 +78,7 @@ int main(void) {
struct sockaddr_in *sin = (struct sockaddr_in *)&addr;
sin->sin_family = AF_INET; sin->sin_addr.s_addr = htonl(0x7f000001); sin->sin_port = htons(port);
int rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL);
int rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL, NULL);
if (rc != 0) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "etcp_send_tcp_ping failed rc=%d", rc); return 1; }
int ticks = 0;
@ -83,7 +90,7 @@ int main(void) {
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);
rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL, NULL);
if (rc != 0) return 1;
ticks = 0;
while (ping_ok < 0 && ticks < 5000) { uasync_poll(ua, 10); ticks++; }
@ -95,21 +102,33 @@ int main(void) {
struct sockaddr_storage bad_addr; memset(&bad_addr, 0, sizeof(bad_addr));
struct sockaddr_in *bsin = (struct sockaddr_in *)&bad_addr;
bsin->sin_family = AF_INET; bsin->sin_addr.s_addr = htonl(0x7f000001); bsin->sin_port = htons(port + 5000);
rc = etcp_send_tcp_ping(cli, srv_pub, &bad_addr, 1000, ping_cb, NULL, NULL);
rc = etcp_send_tcp_ping(cli, srv_pub, &bad_addr, 1000, ping_cb, NULL, NULL, NULL);
ticks = 0;
while (ping_ok < 0 && ticks < 3000) { uasync_poll(ua, 10); ticks++; }
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);
struct reality_config reality; reality_config_set_defaults(&reality);
struct reality_client_config cc; reality_client_config_set_defaults(&cc);
reality.enabled = 1; reality.short_id_count = 1;
if (reality_generate_keypair(reality.private_key, cc.server_static_pubkey)) return 1;
strcpy(cc.server_name, "www.microsoft.com");
struct stcp_server *rs = stcp_server_create(ua, port + 2, &srv->my_keys, srv->my_ed25519_pubkey,
srv, unexpected_ready, NULL, NULL, NULL, AF_INET);
if (!rs) return 1;
stcp_server_set_reality(rs, &reality);
sin->sin_port = htons(port + 2);
/* Одноразовый TCP-пинг не удерживает instance: длина выбирается до асинхронного connect. */
ping_ok = -1;
rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL);
rc = etcp_send_tcp_ping(cli, srv_pub, &addr, 2000, ping_cb, NULL, NULL, &cc);
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 ===");
stcp_server_destroy(rs);
srv->running = 0;
utun_instance_destroy(srv);
uasync_destroy(ua, 0);

Loading…
Cancel
Save