Browse Source

reality: TLS-камуфляж для STCP (ClientHello/ServerHello + релей на реальный сайт)

v2
evgeny 3 weeks ago
parent
commit
edb3bb36e1
  1. 1
      lib/debug_config.c
  2. 3
      lib/debug_config.h
  3. 6
      src/Makefile.am
  4. 82
      src/config_parser.c
  5. 5
      src/config_parser.h
  6. 3
      src/transport_layer/etcp_connections.c
  7. 616
      src/transport_layer/reality.c
  8. 116
      src/transport_layer/reality.h
  9. 47
      src/transport_layer/reality_fingerprint.c
  10. 32
      src/transport_layer/reality_fingerprint.h
  11. 263
      src/transport_layer/reality_relay.c
  12. 32
      src/transport_layer/reality_relay.h
  13. 7
      src/transport_layer/stcp.h
  14. 71
      src/transport_layer/stcp_client.c
  15. 3
      src/transport_layer/stcp_client.h
  16. 18
      src/transport_layer/stcp_link.c
  17. 1
      src/transport_layer/stcp_link.h
  18. 93
      src/transport_layer/stcp_server.c
  19. 4
      src/transport_layer/stcp_server.h
  20. 5
      tests/Makefile.am
  21. 214
      tests/test_reality_hello.c
  22. 16
      tests/test_stcp.c
  23. 4
      tests/test_stcp_link.c

1
lib/debug_config.c

@ -130,6 +130,7 @@ static const struct {
{"member_sync", DEBUG_CATEGORY_MEMBER_SYNC},
{"proxy", DEBUG_CATEGORY_PROXY},
{"video", DEBUG_CATEGORY_VIDEO},
{"reality", DEBUG_CATEGORY_REALITY},
{"all", DEBUG_CATEGORY_ALL},
{NULL, DEBUG_CATEGORY_NONE}
};

3
lib/debug_config.h

@ -68,7 +68,8 @@ typedef int debug_category_t;
#define DEBUG_CATEGORY_MEMBER_SYNC 27 // Member + address + merkle sync
#define DEBUG_CATEGORY_PROXY 28 // Proxy modules (SOCKS5, TCP, UDP, ICMP)
#define DEBUG_CATEGORY_VIDEO 29 // Video module (probe/transcode)
#define DEBUG_CATEGORY_COUNT 30 // Total number of categories
#define DEBUG_CATEGORY_REALITY 30 // REALITY TLS camouflage
#define DEBUG_CATEGORY_COUNT 31 // Total number of categories
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
/* Debug configuration structure */

6
src/Makefile.am

@ -36,6 +36,9 @@ utun_CORE_SOURCES = \
transport_layer/etcp_debug.c \
transport_layer/etcp_dump.c \
transport_layer/secure_channel.c \
transport_layer/reality.c \
transport_layer/reality_fingerprint.c \
transport_layer/reality_relay.c \
transport_layer/crc32.c \
transport_layer/stcp_link.c \
transport_layer/stcp.c \
@ -122,6 +125,9 @@ libutun_a_SOURCES = \
transport_layer/etcp_debug.c \
transport_layer/etcp_dump.c \
transport_layer/secure_channel.c \
transport_layer/reality.c \
transport_layer/reality_fingerprint.c \
transport_layer/reality_relay.c \
transport_layer/crc32.c \
transport_layer/stcp_link.c \
transport_layer/stcp.c \

82
src/config_parser.c

@ -52,7 +52,8 @@ typedef enum {
SECTION_NETWORK,
SECTION_GUI,
SECTION_NTP,
SECTION_LOG_UDP
SECTION_LOG_UDP,
SECTION_REALITY
} section_type_t;
static char* trim(char *str) {
@ -698,7 +699,11 @@ static int parse_server(const char *key, const char *value, struct CFG_SERVER *s
else srv->transport = 0; // default udp
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown server option '%s'. Valid: addr, so_mark, fib, netif, type, mtu, only_local, transport", filename, line_num, key);
if (strcmp(key, "reality") == 0) {
srv->reality_enabled = atoi(value) ? 1 : 0;
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown server option '%s'. Valid: addr, so_mark, fib, netif, type, mtu, only_local, transport, reality", filename, line_num, key);
return -1;
}
@ -808,6 +813,72 @@ static int parse_network(const char *key, const char *value, struct CFG_NETWORK
return -1;
}
static int parse_reality(const char *key, const char *value, struct reality_config *rc, const char *filename, int line_num) {
if (strcmp(key, "enabled") == 0) { rc->enabled = atoi(value) ? 1 : 0; return 0; }
if (strcmp(key, "server_name") == 0) { return assign_string(rc->server_name, sizeof(rc->server_name), value); }
if (strcmp(key, "dest") == 0) { return assign_string(rc->dest, sizeof(rc->dest), value); }
if (strcmp(key, "short_id") == 0) {
if (hex_to_binary(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2);
return -1;
}
return 0;
}
if (strcmp(key, "short_ids") == 0) {
rc->short_id_count = 0;
char buf[MAX_LINE_LEN];
assign_string(buf, sizeof(buf), value);
char *tok = strtok(buf, ",");
while (tok) {
char *t = trim(tok);
if (rc->short_id_count >= REALITY_MAX_SHORT_IDS) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: too many reality short_ids (max %d)", filename, line_num, REALITY_MAX_SHORT_IDS);
return -1;
}
if (hex_to_binary(t, rc->short_ids[rc->short_id_count], REALITY_SHORT_ID_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad reality short_id '%s'", filename, line_num, t);
return -1;
}
rc->short_id_count++;
tok = strtok(NULL, ",");
}
return 0;
}
if (strcmp(key, "public_key") == 0) {
if (hex_to_binary(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
return -1;
}
rc->has_public_key = 1;
return 0;
}
if (strcmp(key, "private_key") == 0) {
if (hex_to_binary(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality private_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
return -1;
}
rc->has_private_key = 1;
return 0;
}
if (strcmp(key, "version") == 0) {
int x = 0, y = 0, z = 0;
if (sscanf(value, "%d.%d.%d", &x, &y, &z) != 3 || x < 0 || x > 255 || y < 0 || y > 255 || z < 0 || z > 255) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad reality version '%s' (expected x.y.z)", filename, line_num, value);
return -1;
}
rc->version[0] = (uint8_t)x; rc->version[1] = (uint8_t)y; rc->version[2] = (uint8_t)z;
return 0;
}
if (strcmp(key, "time_window") == 0) { rc->time_window_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);
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, fingerprint", filename, line_num, key);
return -1;
}
static section_type_t parse_section_header(const char *line, char *name, size_t name_len) {
if (line[0] != '[') return SECTION_UNKNOWN;
@ -835,6 +906,7 @@ static section_type_t parse_section_header(const char *line, char *name, size_t
if (strcasecmp(section, "ntp") == 0) return SECTION_NTP;
if (strcasecmp(section, "gui") == 0) return SECTION_GUI;
if (strcasecmp(section, "log_udp") == 0) return SECTION_LOG_UDP;
if (strcasecmp(section, "reality") == 0) return SECTION_REALITY;
char *colon = strchr(section, ':');
if (!colon) return SECTION_UNKNOWN;
@ -887,6 +959,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
cfg->global.chatserver_allowed_groups[0] = '\0';
cfg->global.chatserver_storage_total_size = 0;
cfg->global.chatserver_storage_unit_size = 0;
reality_config_set_defaults(&cfg->global.reality);
section_type_t cur_section = SECTION_UNKNOWN;
struct CFG_SERVER *cur_server = NULL;
@ -1098,6 +1171,9 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
break;
case SECTION_GUI:
break;
case SECTION_REALITY:
parse_reality(key, value, &cfg->global.reality, filename, line_num);
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key);
break;
@ -1187,6 +1263,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
cfg->global.chatserver_allowed_groups[0] = '\0';
cfg->global.chatserver_storage_total_size = 0;
cfg->global.chatserver_storage_unit_size = 0;
reality_config_set_defaults(&cfg->global.reality);
section_type_t cur_section = SECTION_UNKNOWN;
struct CFG_SERVER *cur_server = NULL;
@ -1247,6 +1324,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set(key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break;
case SECTION_GUI: break;
case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
case SECTION_LOG_UDP:
if (strcmp(key, "ip") == 0) strncpy(cfg->global.log_udp_ip, value, sizeof(cfg->global.log_udp_ip) - 1);
else if (strcmp(key, "port") == 0) cfg->global.log_udp_port = atoi(value);

5
src/config_parser.h

@ -6,6 +6,7 @@
#include <stddef.h>
#include "../lib/platform_compat.h"
#include "secure_channel.h"
#include "transport_layer/reality.h"
#ifdef __cplusplus
extern "C" {
@ -48,6 +49,7 @@ struct CFG_SERVER {
int fib; // FreeBSD FIB (routing table), -1 = don't set
uint8_t type; // public/nat/private
uint8_t transport; // 0=udp (default), 1=tcp
uint8_t reality_enabled; // 1 = TCP-порт с REALITY-камуфляжем (только transport=1)
uint8_t ipv6_mode; // CFG_IPV6_MODE_*
int mtu;
uint8_t only_local; // 1 = only local connections, no forwarding
@ -216,6 +218,9 @@ struct global_config {
uint64_t chatserver_storage_total_size; // bytes, общий лимит хранилища (0 = безлимит)
uint64_t chatserver_storage_unit_size; // bytes, лимит одного файла (0 = отключено)
char headless_control_bind[64]; // ip:port для headless chat CLI (пусто = отключено)
// Reality-камуфляж ([reality] section)
struct reality_config reality;
};
struct utun_config {

3
src/transport_layer/etcp_connections.c

@ -2348,7 +2348,8 @@ int etcp_create_server_socket(struct UTUN_INSTANCE* instance, struct CFG_SERVER*
struct ETCP_SOCKET* ts = tcp_socket_add(instance, server);
if (!ts) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "tcp_socket_add failed for %s", server->name); return -1; }
struct stcp_link_config scfg = {.inst = instance, .listen_family = server->ip.ss_family};
struct stcp_link_config scfg = {.inst = instance, .listen_family = server->ip.ss_family,
.reality_enabled = server->reality_enabled};
struct stcp_server* tsrv = stcp_server_listen(&scfg, port, tcp_server_on_link, ts);
if (!tsrv) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create TCP server for %s", server->name);

616
src/transport_layer/reality.c

@ -0,0 +1,616 @@
/* reality.c — REALITY-style TLS ClientHello/ServerHello camouflage
*
* Реализует формирование и разбор ClientHello/ServerHello по схеме REALITY
* (см. 3.go): аутентификация в SessionId ClientHello.
*
* открытый текст (16 байт) = version[3] + reserved(1) + timestamp(4, BE) + ShortId[8]
* SessionId[0:32] = AES-256-GCM(открытый текст) = ciphertext(16) + tag(16)
*
* AuthKey = HKDF-SHA256(X25519(eph, static), Random[0:20], "REALITY")
* nonce = Random[20:32]
* AAD = весь ClientHello handshake-сообщение с обнулённым SessionId
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#define OPENSSL_API_COMPAT 0x10100000L
#include "reality.h"
#include "reality_fingerprint.h"
#include "../lib/debug_config.h"
#include "../lib/platform_compat.h"
#include <string.h>
#include <time.h>
#include <openssl/evp.h>
#include <openssl/kdf.h>
// TLS-константы
#define TLS_RECORD_HANDSHAKE 0x16
#define TLS_HANDSHAKE_CLIENT_HELLO 0x01
#define TLS_HANDSHAKE_SERVER_HELLO 0x02
#define TLS_EXT_SERVER_NAME 0x0000
#define TLS_EXT_SUPPORTED_GROUPS 0x000a
#define TLS_EXT_SIG_ALGS 0x000d
#define TLS_EXT_ALPN 0x0010
#define TLS_EXT_SUPPORTED_VERSIONS 0x002b
#define TLS_EXT_PSK_KEY_EXCHANGE 0x002d
#define TLS_EXT_KEY_SHARE 0x0033
#define TLS_GROUP_X25519 0x001d
#define TLS_VERSION_1_3 0x0304
#define TLS_CIPHER_AES_128_GCM 0x1301
#define REALITY_HKDF_INFO "REALITY"
// ─── Буфер записи с контролем границ ───
struct rbuf {
uint8_t *p;
size_t cap;
size_t len;
};
static int rbuf_put(struct rbuf *b, const void *data, size_t n) {
if (b->len + n > b->cap) return -1;
memcpy(b->p + b->len, data, n);
b->len += n;
return 0;
}
static int rbuf_put_u8(struct rbuf *b, uint8_t v) { return rbuf_put(b, &v, 1); }
static int rbuf_put_u16(struct rbuf *b, uint16_t v) {
uint8_t t[2] = { (uint8_t)(v >> 8), (uint8_t)(v & 0xff) };
return rbuf_put(b, t, 2);
}
// Вписывает 2-байтную длину по уже известной позиции (для extension-блоков).
static void rbuf_patch_u16(struct rbuf *b, size_t pos, uint16_t v) {
if (pos + 2 <= b->cap) {
b->p[pos] = (uint8_t)(v >> 8);
b->p[pos + 1] = (uint8_t)(v & 0xff);
}
}
// ─── Крипто-хелперы (OpenSSL) ───
static int reality_gen_keypair(uint8_t priv[32], uint8_t pub[32]) {
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL);
if (!ctx) return -1;
EVP_PKEY *pkey = NULL;
int rc = -1;
if (EVP_PKEY_keygen_init(ctx) <= 0) goto out;
if (EVP_PKEY_keygen(ctx, &pkey) <= 0) goto out;
size_t plen = 32, qlen = 32;
if (EVP_PKEY_get_raw_private_key(pkey, priv, &plen) <= 0 || plen != 32) goto out;
if (EVP_PKEY_get_raw_public_key(pkey, pub, &qlen) <= 0 || qlen != 32) goto out;
rc = 0;
out:
EVP_PKEY_free(pkey);
EVP_PKEY_CTX_free(ctx);
return rc;
}
int reality_generate_keypair(uint8_t priv[32], uint8_t pub[32]) {
return reality_gen_keypair(priv, pub) == 0 ? REALITY_OK : REALITY_ERR_CRYPTO;
}
int reality_pubkey_from_priv(const uint8_t priv[32], uint8_t pub[32]) {
if (!priv || !pub) return REALITY_ERR_ARG;
EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32);
if (!pkey) return REALITY_ERR_CRYPTO;
size_t len = 32;
int rc = (EVP_PKEY_get_raw_public_key(pkey, pub, &len) <= 0 || len != 32) ? REALITY_ERR_CRYPTO : REALITY_OK;
EVP_PKEY_free(pkey);
return rc;
}
void reality_config_set_defaults(struct reality_config *cfg) {
if (!cfg) return;
cfg->enabled = 0;
cfg->server_name[0] = '\0';
cfg->dest[0] = '\0';
memset(cfg->short_id, 0, sizeof(cfg->short_id));
memset(cfg->short_ids, 0, sizeof(cfg->short_ids));
cfg->short_id_count = 0;
memset(cfg->public_key, 0, sizeof(cfg->public_key));
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->time_window_sec = 30;
cfg->fingerprint = REALITY_FP_CHROME;
}
static int reality_x25519(const uint8_t priv[32], const uint8_t pub[32], uint8_t shared[32]) {
EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32);
if (!pkey) return -1;
EVP_PKEY *peer = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, pub, 32);
if (!peer) { EVP_PKEY_free(pkey); return -1; }
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!ctx) { EVP_PKEY_free(pkey); EVP_PKEY_free(peer); return -1; }
int rc = -1;
if (EVP_PKEY_derive_init(ctx) <= 0) goto out;
if (EVP_PKEY_derive_set_peer(ctx, peer) <= 0) goto out;
size_t len = 32;
if (EVP_PKEY_derive(ctx, shared, &len) <= 0 || len != 32) goto out;
rc = 0;
out:
EVP_PKEY_CTX_free(ctx);
EVP_PKEY_free(pkey);
EVP_PKEY_free(peer);
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,
uint8_t *cipher, uint8_t tag[16]) {
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) return -1;
int rc = -1, len = 0, tmplen = 0;
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) goto out;
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL) != 1) goto out;
if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) goto out;
if (aad_len && EVP_EncryptUpdate(ctx, NULL, &len, aad, (int)aad_len) != 1) goto out;
if (EVP_EncryptUpdate(ctx, cipher, &len, plain, (int)plain_len) != 1) goto out;
if (EVP_EncryptFinal_ex(ctx, cipher + len, &tmplen) != 1) goto out;
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, tag) != 1) goto out;
rc = 0;
out:
EVP_CIPHER_CTX_free(ctx);
return rc;
}
static int reality_aes_gcm_open(const uint8_t key[32], const uint8_t nonce[12],
const uint8_t *aad, size_t aad_len,
const uint8_t *cipher, size_t cipher_len,
const uint8_t tag[16], uint8_t *plain) {
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) return -1;
int rc = -1, len = 0, tmplen = 0;
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) goto out;
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL) != 1) goto out;
if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) goto out;
if (aad_len && EVP_DecryptUpdate(ctx, NULL, &len, aad, (int)aad_len) != 1) goto out;
if (EVP_DecryptUpdate(ctx, plain, &len, cipher, (int)cipher_len) != 1) goto out;
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void *)tag) != 1) goto out;
if (EVP_DecryptFinal_ex(ctx, plain + len, &tmplen) != 1) goto out; // проверка тега
rc = 0;
out:
EVP_CIPHER_CTX_free(ctx);
return rc;
}
// Сборка ClientHello body (после 4-байтного handshake-заголовка).
// SessionId пишется нулями (32 байта) — далее по нему считается AAD.
static int reality_build_client_hello_body(const struct reality_client_config *cfg,
const struct reality_fingerprint *fp,
const uint8_t *random32,
const uint8_t *eph_pub,
uint8_t *body, size_t body_cap, size_t *body_len) {
struct rbuf b = { body, body_cap, 0 };
if (rbuf_put_u16(&b, 0x0303) < 0) return REALITY_ERR_ARG; // legacy_version
if (rbuf_put(&b, random32, 32) < 0) return REALITY_ERR_ARG; // random
if (rbuf_put_u8(&b, 32) < 0) return REALITY_ERR_ARG; // session id len
uint8_t zeros[32] = {0};
if (rbuf_put(&b, zeros, 32) < 0) return REALITY_ERR_ARG; // session id (нули)
// cipher suites
int cs_count = 0;
for (const uint16_t *cs = fp->cipher_suites; *cs != 0; cs++) cs_count++;
if (rbuf_put_u16(&b, (uint16_t)(cs_count * 2)) < 0) return REALITY_ERR_ARG;
for (const uint16_t *cs = fp->cipher_suites; *cs != 0; cs++)
if (rbuf_put_u16(&b, *cs) < 0) return REALITY_ERR_ARG;
// compression
if (rbuf_put_u8(&b, 1) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u8(&b, 0) < 0) return REALITY_ERR_ARG;
// extensions
size_t ext_len_pos = b.len;
if (rbuf_put_u16(&b, 0) < 0) return REALITY_ERR_ARG; // placeholder
// server_name (SNI)
size_t name_len = strlen(cfg->server_name);
{
uint16_t ext_data_len = (uint16_t)(5 + name_len);
if (rbuf_put_u16(&b, TLS_EXT_SERVER_NAME) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, ext_data_len) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, (uint16_t)(3 + name_len)) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u8(&b, 0) < 0) return REALITY_ERR_ARG; // name_type = host_name
if (rbuf_put_u16(&b, (uint16_t)name_len) < 0) return REALITY_ERR_ARG;
if (rbuf_put(&b, cfg->server_name, name_len) < 0) 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)(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;
}
// 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)(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;
}
// supported_versions
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_VERSIONS) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 3) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u8(&b, 2) < 0) return REALITY_ERR_ARG; // длина списка
if (rbuf_put_u16(&b, TLS_VERSION_1_3) < 0) return REALITY_ERR_ARG;
// psk_key_exchange_modes
if (rbuf_put_u16(&b, TLS_EXT_PSK_KEY_EXCHANGE) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 2) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u8(&b, 1) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u8(&b, 1) < 0) return REALITY_ERR_ARG; // psk_dhe_ke
// key_share (X25519)
if (rbuf_put_u16(&b, TLS_EXT_KEY_SHARE) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 38) < 0) return REALITY_ERR_ARG;
if (rbuf_put_u16(&b, 36) < 0) return REALITY_ERR_ARG; // client_shares_len
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;
// 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;
}
}
rbuf_patch_u16(&b, ext_len_pos, (uint16_t)(b.len - ext_len_pos - 2));
*body_len = b.len;
return REALITY_OK;
}
int reality_client_hello_build(const struct reality_client_config *cfg,
uint8_t *out, size_t out_cap, size_t *out_len) {
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,
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");
return REALITY_ERR_ARG;
}
const struct reality_fingerprint *fp = reality_fingerprint_get(cfg->fingerprint);
if (!fp) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: unknown fingerprint %d", cfg->fingerprint);
return REALITY_ERR_ARG;
}
if (cfg->server_name[0] == '\0') {
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];
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;
}
if (random_bytes(random32, sizeof(random32)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: random_bytes failed");
return REALITY_ERR_CRYPTO;
}
// 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;
}
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) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: HKDF failed");
return REALITY_ERR_CRYPTO;
}
// 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);
plaintext[5] = (uint8_t)(now >> 16);
plaintext[6] = (uint8_t)(now >> 8);
plaintext[7] = (uint8_t)(now);
memcpy(plaintext + 8, cfg->short_id, REALITY_SHORT_ID_SIZE);
// 4. собрать body (SessionId = нули)
uint8_t body[REALITY_MAX_CH_SIZE];
size_t body_len = 0;
int 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;
}
// 5. handshake-сообщение = 0x01 + length + body (SessionId на позиции 39..71)
uint8_t hs[REALITY_MAX_CH_SIZE];
size_t hs_len = 4 + body_len;
hs[0] = TLS_HANDSHAKE_CLIENT_HELLO;
hs[1] = (uint8_t)(body_len >> 16);
hs[2] = (uint8_t)(body_len >> 8);
hs[3] = (uint8_t)(body_len);
memcpy(hs + 4, body, body_len);
// 6. AAD = hs с обнулённым SessionId (уже нули), seal → ciphertext(16)+tag(16)=32 байта
uint8_t seal_out[32];
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;
}
memcpy(hs + 39, seal_out, 32); // SessionId = ciphertext || tag
// 7. TLS record = 0x16 0x0301 length + hs
size_t total = 5 + hs_len;
out[0] = TLS_RECORD_HANDSHAKE;
out[1] = 0x03;
out[2] = 0x01;
out[3] = (uint8_t)(hs_len >> 8);
out[4] = (uint8_t)(hs_len);
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;
}
// ─── Сервер: разбор ClientHello ───
struct reality_ch_parsed {
const uint8_t *random; // 32 байта
const uint8_t *session_id; // 32 байта
const uint8_t *key_share; // 32 байта X25519 ключ (NULL, если нет)
};
// Возвращает REALITY_OK / REALITY_ERR_FORMAT.
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;
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]);
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;
}
s += 4 + klen;
sl -= 4 + klen;
}
}
ext += 4 + len;
remaining -= 4 + len;
}
*hs_out = hs;
*hs_len_out = hs_len;
return REALITY_OK;
}
int reality_server_hello_build(const struct reality_server_config *cfg,
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) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: invalid args");
return REALITY_ERR_ARG;
}
struct reality_ch_parsed p;
const uint8_t *hs;
size_t hs_len;
int rc = reality_parse_client_hello(ch, ch_len, &hs, &hs_len, &p);
if (rc != REALITY_OK) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: parse failed rc=%d", rc);
return rc;
}
if (!p.key_share) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: no X25519 key_share in ClientHello");
return REALITY_ERR_AUTH;
}
// shared + AuthKey
uint8_t shared[32], auth_key[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;
}
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) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: HKDF failed");
return REALITY_ERR_CRYPTO;
}
// AAD = hs с обнулённым SessionId [39:71]
uint8_t aad[REALITY_MAX_CH_SIZE];
if (hs_len > sizeof(aad)) return REALITY_ERR_FORMAT;
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;
}
// сверка версии
if (memcmp(plain, cfg->version, 3) != 0 || plain[3] != 0) {
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;
}
// сверка timestamp
uint32_t ts = ((uint32_t)plain[4] << 24) | ((uint32_t)plain[5] << 16) |
((uint32_t)plain[6] << 8) | plain[7];
int64_t now = (int64_t)time(NULL);
int64_t diff = now - (int64_t)ts;
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;
}
// сверка 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 (!sid_ok) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: short_id not in list");
return REALITY_ERR_AUTH;
}
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);
// ─── собрать 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; }
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;
}
(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;
}
// 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
size_t ext_len_pos = b.len;
if (rbuf_put_u16(&b, 0) < 0) return REALITY_ERR_ARG; // 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;
// 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;
rbuf_patch_u16(&b, ext_len_pos, (uint16_t)(b.len - ext_len_pos - 2));
size_t sh_body_len = b.len;
// ServerHello handshake-сообщение + TLS record
size_t sh_hs_len = 4 + sh_body_len;
out[0] = TLS_RECORD_HANDSHAKE;
out[1] = 0x03;
out[2] = 0x03;
out[3] = (uint8_t)(sh_hs_len >> 8);
out[4] = (uint8_t)(sh_hs_len);
out[5] = TLS_HANDSHAKE_SERVER_HELLO;
out[6] = (uint8_t)(sh_body_len >> 16);
out[7] = (uint8_t)(sh_body_len >> 8);
out[8] = (uint8_t)(sh_body_len);
memcpy(out + 9, sh_body, sh_body_len);
*out_len = 5 + sh_hs_len;
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "server hello built: total=%zu", *out_len);
return REALITY_OK;
}

116
src/transport_layer/reality.h

@ -0,0 +1,116 @@
// reality.h — REALITY-style TLS ClientHello/ServerHello camouflage для STCP
//
// Модуль формирует/разбирает ClientHello и ServerHello так же, как REALITY
// (см. 3.go): аутентификация прячется в SessionId ClientHello — весь SessionId
// (32 байта) это AES-256-GCM(ciphertext(16) + tag(16)), а открытый текст
// (16 байт) несёт version(3) + reserved(1) + timestamp(4) + ShortId(8).
// Ключ AuthKey = HKDF-SHA256(X25519(eph, static), Random[0:20], "REALITY").
//
// Область применения — «hello-only»: обмениваемся только заголовками, реальную
// взаимную аутентификацию делает штатный STCP-хендшейк поверх.
#ifndef REALITY_H
#define REALITY_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
// Размеры
#define REALITY_SHORT_ID_SIZE 8
#define REALITY_VERSION_SIZE 3
#define REALITY_AUTH_KEY_SIZE 32
#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_SH_SIZE 512 // максимальный размер ServerHello
#define REALITY_MAX_SHORT_IDS 64
#define REALITY_SERVER_NAME_MAX 256
// Коды возврата
#define REALITY_OK 0
#define REALITY_ERR_ARG -1
#define REALITY_ERR_CRYPTO -2
#define REALITY_ERR_AUTH -3
#define REALITY_ERR_FORMAT -4
// Отпечатки (reality_fingerprint.c)
#define REALITY_FP_CHROME 0
#define REALITY_FP_COUNT 1
// Конфигурация клиента
struct reality_client_config {
uint8_t server_static_pubkey[REALITY_AUTH_KEY_SIZE]; // X25519 static pubkey сервера
uint8_t short_id[REALITY_SHORT_ID_SIZE];
uint8_t version[REALITY_VERSION_SIZE]; // версия протокола utun
char server_name[REALITY_SERVER_NAME_MAX]; // SNI-таргет (напр. "www.microsoft.com")
int fingerprint; // REALITY_FP_*
};
// Конфигурация сервера
struct reality_server_config {
uint8_t static_privkey[REALITY_AUTH_KEY_SIZE]; // X25519 static privkey сервера
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 (антиреплей)
int fingerprint;
};
// Общая конфигурация reality (секция [reality] в конфиге utun)
#define REALITY_DEST_MAX 256
struct reality_config {
int enabled; // 1 = камуфляж включён
char server_name[REALITY_SERVER_NAME_MAX]; // SNI-таргет
char dest[REALITY_DEST_MAX]; // "host:port" для релея (fallback)
uint8_t short_id[REALITY_SHORT_ID_SIZE]; // клиентский short_id
uint8_t short_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE]; // сервер: список
int short_id_count;
uint8_t public_key[REALITY_AUTH_KEY_SIZE]; // клиент: static pubkey сервера
uint8_t private_key[REALITY_AUTH_KEY_SIZE]; // сервер: static privkey
uint8_t has_public_key;
uint8_t has_private_key;
uint8_t version[REALITY_VERSION_SIZE]; // версия протокола
int time_window_sec;
int fingerprint;
};
// Заполняет cfg->version/fingerprint/time_window значениями по умолчанию
// (version=1.0.0, fingerprint=chrome, time_window=30). Вызывается при инициализации.
void reality_config_set_defaults(struct reality_config *cfg);
// Клиент: собрать ClientHello (TLS record + handshake) с REALITY-авторизацией.
// Генерирует эфемерный X25519 и Random внутри. out_cap >= REALITY_MAX_CH_SIZE.
// Возвращает REALITY_OK / REALITY_ERR_*, в *out_len — итоговый размер.
int reality_client_hello_build(const struct reality_client_config *cfg,
uint8_t *out, size_t out_cap, size_t *out_len);
// То же, но с явным timestamp (unix-секунды) — для тестов антиреплей-проверки.
int reality_client_hello_build_at(const struct reality_client_config *cfg, uint32_t ts,
uint8_t *out, size_t out_cap, size_t *out_len);
// Сервер: распарсить ClientHello, проверить авторизацию, собрать ServerHello.
// ch/ch_len — принятые байты (TLS record + handshake, допускается одна запись).
// REALITY_OK — авторизация валидна, out/out_len = ServerHello.
// REALITY_ERR_AUTH — авторизация не прошла (вызывающий код запускает релей).
// REALITY_ERR_* — формат/аргумент/крипто-ошибка.
int reality_server_hello_build(const struct reality_server_config *cfg,
const uint8_t *ch, size_t ch_len,
uint8_t *out, size_t out_cap, size_t *out_len);
// Сгенерировать X25519 пару ключей (для конфигурации: сервер хранит privkey,
// клиент — pubkey сервера). Возвращает REALITY_OK / REALITY_ERR_CRYPTO.
int reality_generate_keypair(uint8_t priv[REALITY_AUTH_KEY_SIZE],
uint8_t pub[REALITY_AUTH_KEY_SIZE]);
// Вычислить X25519 pubkey из privkey. Возвращает REALITY_OK / REALITY_ERR_CRYPTO.
int reality_pubkey_from_priv(const uint8_t priv[REALITY_AUTH_KEY_SIZE],
uint8_t pub[REALITY_AUTH_KEY_SIZE]);
#ifdef __cplusplus
}
#endif
#endif // REALITY_H

47
src/transport_layer/reality_fingerprint.c

@ -0,0 +1,47 @@
// reality_fingerprint.c — статические отпечатки для REALITY-камуфляжа
#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 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
},
};
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;
}

32
src/transport_layer/reality_fingerprint.h

@ -0,0 +1,32 @@
// reality_fingerprint.h — статические отпечатки (fingerprints) для REALITY-камуфляжа
//
// Отпечаток описывает статический скелет ClientHello/ServerHello: список cipher
// suites, supported groups, signature algorithms, ALPN и выбранный cipher suite
// для ServerHello. Билдер (reality.c) подставляет динамические поля (Random,
// SessionId, key_share, SNI) в этот скелет.
#ifndef REALITY_FINGERPRINT_H
#define REALITY_FINGERPRINT_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
struct reality_fingerprint {
int id;
const char *name;
const uint16_t *cipher_suites; // список, терминирован 0x0000
const uint16_t *supported_groups; // список, терминирован 0x0000
const uint16_t *sig_algs; // список, терминирован 0x0000
const char *const *alpn; // NULL-терминированный список
uint16_t server_cipher; // выбранный cipher suite для ServerHello
};
// Возвращает отпечаток по id (REALITY_FP_*) или NULL, если не найден.
const struct reality_fingerprint *reality_fingerprint_get(int id);
#ifdef __cplusplus
}
#endif
#endif // REALITY_FINGERPRINT_H

263
src/transport_layer/reality_relay.c

@ -0,0 +1,263 @@
// reality_relay.c — живой релей неавторизованных клиентов на реальный HTTPS-сайт
//
// Двухсторонний TCP-пайп: клиент (приславший невалидный ClientHello) ↔ реальный
// сайт (dest). initial_data (уже прочитанный ClientHello) пересылается на dest
// первым. Поддержан half-close: FIN с одной стороны закрывает только свою
// сторону записи на другой, чтобы ответ сайта дошёл до клиента полностью.
#include "reality_relay.h"
#include "../lib/u_async.h"
#include "../lib/mem.h"
#include "../lib/debug_config.h"
#include "../lib/platform_compat.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#ifndef _WIN32
#include <netinet/tcp.h>
#endif
#define RELAY_IO_BUF 65536
#ifdef _WIN32
#define RELAY_SHUT_WR SD_SEND
#else
#define RELAY_SHUT_WR SHUT_WR
#endif
struct reality_relay {
struct UASYNC *ua;
socket_t client_sock;
void *client_sid;
socket_t dest_sock;
void *dest_sid;
int connecting; // 1 = ждём завершения connect() к dest
uint8_t *c2d; size_t c2d_len, c2d_off; // pending client→dest
uint8_t *d2c; size_t d2c_len, d2c_off; // pending dest→client
int client_read_eof; // клиент закрыл свою сторону записи
int dest_read_eof; // dest закрыл свою сторону записи
int closed;
size_t bytes_c2d, bytes_d2c;
};
static void relay_free(struct reality_relay *r);
static void relay_flush_c2d(struct reality_relay *r);
static void relay_flush_d2c(struct reality_relay *r);
static void relay_client_read_cb(socket_t sock, void *arg);
static void relay_dest_read_cb(socket_t sock, void *arg);
static void relay_client_write_cb(socket_t sock, void *arg);
static void relay_dest_write_cb(socket_t sock, void *arg);
static void relay_free_cb(void *arg) {
u_free(arg);
}
static void relay_free(struct reality_relay *r) {
if (!r || r->closed) return;
r->closed = 1;
if (r->client_sid) { uasync_remove_socket_t(r->ua, r->client_sock); r->client_sid = NULL; }
if (r->dest_sid) { uasync_remove_socket_t(r->ua, r->dest_sock); r->dest_sid = NULL; }
if (r->client_sock != SOCKET_INVALID) { socket_close_wrapper(r->client_sock); r->client_sock = SOCKET_INVALID; }
if (r->dest_sock != SOCKET_INVALID) { socket_close_wrapper(r->dest_sock); r->dest_sock = SOCKET_INVALID; }
if (r->c2d) { u_free(r->c2d); r->c2d = NULL; }
if (r->d2c) { u_free(r->d2c); r->d2c = NULL; }
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "reality_relay closed: c2d=%zu d2c=%zu", r->bytes_c2d, r->bytes_d2c);
uasync_call_soon(r->ua, r, relay_free_cb);
}
static void relay_flush_c2d(struct reality_relay *r) {
if (!r->c2d) return;
while (r->c2d_off < r->c2d_len) {
ssize_t sent = send(r->dest_sock, r->c2d + r->c2d_off, r->c2d_len - r->c2d_off, 0);
if (sent < 0) {
int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { uasync_set_socket_write(r->ua, r->dest_sid, 1); return; }
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay send to dest failed err=%d", err);
relay_free(r); return;
}
if (sent == 0) { relay_free(r); return; }
r->c2d_off += (size_t)sent;
}
r->bytes_c2d += r->c2d_len;
u_free(r->c2d); r->c2d = NULL;
uasync_set_socket_write(r->ua, r->dest_sid, 0);
if (!r->client_read_eof) uasync_set_socket_read(r->ua, r->client_sid, 1);
}
static void relay_flush_d2c(struct reality_relay *r) {
if (!r->d2c) return;
while (r->d2c_off < r->d2c_len) {
ssize_t sent = send(r->client_sock, r->d2c + r->d2c_off, r->d2c_len - r->d2c_off, 0);
if (sent < 0) {
int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { uasync_set_socket_write(r->ua, r->client_sid, 1); return; }
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay send to client failed err=%d", err);
relay_free(r); return;
}
if (sent == 0) { relay_free(r); return; }
r->d2c_off += (size_t)sent;
}
r->bytes_d2c += r->d2c_len;
u_free(r->d2c); r->d2c = NULL;
uasync_set_socket_write(r->ua, r->client_sid, 0);
if (!r->dest_read_eof) uasync_set_socket_read(r->ua, r->dest_sid, 1);
}
static void relay_client_read_cb(socket_t sock, void *arg) {
struct reality_relay *r = (struct reality_relay *)arg;
(void)sock;
if (r->c2d) return;
uint8_t *buf = u_malloc(RELAY_IO_BUF);
if (!buf) { relay_free(r); return; }
ssize_t n = recv(r->client_sock, buf, RELAY_IO_BUF, 0);
if (n < 0) {
int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { u_free(buf); return; }
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay recv from client failed err=%d", err);
u_free(buf); relay_free(r); return;
}
if (n == 0) {
u_free(buf);
r->client_read_eof = 1;
uasync_set_socket_read(r->ua, r->client_sid, 0);
shutdown(r->dest_sock, RELAY_SHUT_WR);
if (r->dest_read_eof) relay_free(r);
return;
}
r->c2d = buf; r->c2d_len = (size_t)n; r->c2d_off = 0;
uasync_set_socket_read(r->ua, r->client_sid, 0); // backpressure
relay_flush_c2d(r);
}
static void relay_dest_read_cb(socket_t sock, void *arg) {
struct reality_relay *r = (struct reality_relay *)arg;
(void)sock;
if (r->d2c) return;
uint8_t *buf = u_malloc(RELAY_IO_BUF);
if (!buf) { relay_free(r); return; }
ssize_t n = recv(r->dest_sock, buf, RELAY_IO_BUF, 0);
if (n < 0) {
int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { u_free(buf); return; }
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay recv from dest failed err=%d", err);
u_free(buf); relay_free(r); return;
}
if (n == 0) {
u_free(buf);
r->dest_read_eof = 1;
uasync_set_socket_read(r->ua, r->dest_sid, 0);
shutdown(r->client_sock, RELAY_SHUT_WR);
if (r->client_read_eof) relay_free(r);
return;
}
r->d2c = buf; r->d2c_len = (size_t)n; r->d2c_off = 0;
uasync_set_socket_read(r->ua, r->dest_sid, 0); // backpressure
relay_flush_d2c(r);
}
static void relay_client_write_cb(socket_t sock, void *arg) {
struct reality_relay *r = (struct reality_relay *)arg;
(void)sock;
if (!r->d2c) { uasync_set_socket_write(r->ua, r->client_sid, 0); return; }
relay_flush_d2c(r);
}
static void relay_dest_write_cb(socket_t sock, void *arg) {
struct reality_relay *r = (struct reality_relay *)arg;
if (r->connecting) {
int err = 0; socklen_t elen = sizeof(err);
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&err, &elen) < 0 || err != 0) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay connect to dest failed err=%d", err);
relay_free(r); return;
}
r->connecting = 0;
int opt = 1; setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
if (r->c2d) relay_flush_c2d(r);
else { uasync_set_socket_write(r->ua, r->dest_sid, 0); uasync_set_socket_read(r->ua, r->client_sid, 1); }
return;
}
if (!r->c2d) { uasync_set_socket_write(r->ua, r->dest_sid, 0); return; }
relay_flush_c2d(r);
}
int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
const char *dest,
const uint8_t *initial_data, size_t initial_len) {
if (!ua || client_sock == SOCKET_INVALID || !dest || !dest[0]) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: invalid args");
return -1;
}
// парсим "host:port"
char host[256];
uint16_t port = 443;
{
size_t dl = strlen(dest);
if (dl >= sizeof(host)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: dest too long"); goto fail; }
memcpy(host, dest, dl + 1);
char *colon = strrchr(host, ':');
if (colon) { *colon = '\0'; port = (uint16_t)atoi(colon + 1); }
if (!host[0] || port == 0) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: bad dest '%s'", dest); goto fail; }
}
struct addrinfo hints = {0};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%u", port);
struct addrinfo *res;
if (getaddrinfo(host, port_str, &hints, &res) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay: getaddrinfo %s failed", host);
goto fail;
}
socket_t dest_sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (dest_sock == SOCKET_INVALID) { freeaddrinfo(res); DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay: socket failed"); goto fail; }
socket_set_nonblocking(dest_sock);
struct reality_relay *r = u_calloc(1, sizeof(struct reality_relay));
if (!r) { socket_close_wrapper(dest_sock); freeaddrinfo(res); DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay: calloc failed"); goto fail; }
r->ua = ua;
r->client_sock = client_sock;
r->dest_sock = dest_sock;
r->connecting = 1;
// берём владение client_sock: снимаем прежнюю регистрацию и ставим свою
uasync_remove_socket_t(ua, client_sock);
r->client_sid = uasync_add_socket_t(ua, client_sock, relay_client_read_cb, relay_client_write_cb, NULL, r);
if (!r->client_sid) { relay_free(r); freeaddrinfo(res); return -1; }
if (initial_len && initial_data) {
r->c2d = u_malloc(initial_len);
if (!r->c2d) { relay_free(r); freeaddrinfo(res); return -1; }
memcpy(r->c2d, initial_data, initial_len);
r->c2d_len = initial_len;
r->c2d_off = 0;
}
int cr = connect(dest_sock, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
if (cr < 0) {
int err = socket_get_error();
if (err != EINPROGRESS && err != ERR_WOULDBLOCK) {
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay connect %s:%u failed err=%d", host, port, err);
relay_free(r); return -1;
}
} else {
r->connecting = 0;
int opt = 1; setsockopt(dest_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
}
r->dest_sid = uasync_add_socket_t(ua, dest_sock, relay_dest_read_cb, relay_dest_write_cb, NULL, r);
if (!r->dest_sid) { relay_free(r); return -1; }
if (!r->connecting && r->c2d) relay_flush_c2d(r);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "reality_relay started: client→%s:%u initial=%zu", host, port, initial_len);
return 0;
fail:
socket_close_wrapper(client_sock);
return -1;
}

32
src/transport_layer/reality_relay.h

@ -0,0 +1,32 @@
// reality_relay.h — живой релей неавторизованных клиентов на реальный HTTPS-сайт
//
// Используется сервером STCP при включённом reality: если авторизация ClientHello
// не прошла (не utun-клиент — DPI-проба, случайный посетитель), соединение
// прозрачно проксируется на реальный сайт (dest), чтобы проба видела настоящий
// HTTPS-сеанс, а не ошибку/разрыв.
#ifndef REALITY_RELAY_H
#define REALITY_RELAY_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include "../lib/socket_compat.h"
struct UASYNC;
// Начать релей. Забирает владение client_sock (non-blocking, зарегистрирован
// в uasync) и проксирует его на dest ("host:port").
// initial_data/initial_len — уже прочитанные байты от клиента (ClientHello),
// отправляются на dest первыми (буфер переходит во владение модуля).
// Возвращает 0 = ok, -1 = ошибка (client_sock уже закрыт внутри).
int reality_relay_start(struct UASYNC *ua, socket_t client_sock,
const char *dest,
const uint8_t *initial_data, size_t initial_len);
#ifdef __cplusplus
}
#endif
#endif // REALITY_RELAY_H

7
src/transport_layer/stcp.h

@ -8,6 +8,7 @@ extern "C" {
#include "secure_channel.h"
#include "reality.h"
#include "crc32.h"
#include "../lib/u_async.h"
#include "../lib/socket_compat.h"
@ -104,6 +105,12 @@ struct stcp_conn {
uint64_t hs_send_time; // client: handshake send time (0.1ms tb), for ping RTT
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
struct reality_server_config reality_srv; // конфиг сервера (копия)
char reality_dest[REALITY_DEST_MAX]; // "host:port" для релея неавторизованных
struct ll_queue *rx_queue;
struct ll_queue *tx_queue;
void (*tx_cb)(struct ll_queue *q, void *arg);

71
src/transport_layer/stcp_client.c

@ -28,6 +28,9 @@ struct stcp_client {
uint8_t ping_done;
uint8_t peer_pubkey[SC_PUBKEY_SIZE];
uint8_t my_ed25519_pubkey[SC_PUBKEY_SIZE];
// reality-камуфляж (клиент): перед STCP-хендшейком шлём ClientHello и читаем ServerHello
uint8_t reality_enabled;
struct reality_client_config reality_cfg;
};
static void client_connect_write_cb(socket_t sock, void *arg);
@ -35,6 +38,8 @@ 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 int client_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey) {
struct secure_channel sc;
@ -137,6 +142,55 @@ 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;
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; }
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);
}
// После завершения 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;
if (reality_client_hello_build(&cli->reality_cfg, 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);
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);
}
static void client_conn_read_cb(socket_t sock, void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg;
(void)sock;
@ -159,11 +213,7 @@ static void client_connect_write_cb(socket_t sock, void *arg) {
uasync_remove_socket_t(cli->ua, sock);
c->socket_id = uasync_add_socket_t(cli->ua, sock, client_conn_read_cb, stcp_write_cb, NULL, c);
if (!c->socket_id) { stcp_conn_do_close(c, ENOMEM); return; }
int opt = 1; setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
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);
// after handshake sent, wait for server response
stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER, 0, client_hs_cb);
client_after_connect(c);
}
struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uint16_t port,
@ -177,7 +227,8 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
stcp_ping_cb ping_cb, void *ping_arg,
stcp_close_cb close_cb, void *close_arg,
const struct sockaddr_storage *local_addr,
int timeout_ms) {
int timeout_ms,
const struct reality_client_config *reality) {
if (!ua || !addr || !keys || !peer_pubkey || (!ready_cb && !ping_cb)) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "invalid args"); return NULL; }
struct stcp_client *cli = u_calloc(1, sizeof(struct stcp_client));
if (!cli) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "calloc failed"); return NULL; }
@ -185,6 +236,7 @@ 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; }
struct stcp_conn *c = &cli->conn;
c->ua = ua; c->state = STCP_STATE_INIT; c->is_server = 0; c->my_keys = *keys;
c->on_ready = ready_cb; c->ready_arg = arg;
@ -229,13 +281,10 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
if (!c->socket_id) { socket_close_wrapper(c->sock); u_free(cli); return NULL; }
c->hs_timer = uasync_set_timeout(ua, hs_tb, c, hs_timeout_cb, "stcp_hs");
} else {
int opt = 1; setsockopt(c->sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
c->socket_id = uasync_add_socket_t(ua, c->sock, client_conn_read_cb, stcp_write_cb, NULL, c);
if (!c->socket_id) { socket_close_wrapper(c->sock); u_free(cli); return NULL; }
c->hs_timer = uasync_set_timeout(ua, hs_tb, c, hs_timeout_cb, "stcp_hs");
if (client_derive_session(c, cli->peer_pubkey)) { c->free_on_close = cli; stcp_conn_do_close(c, 1); return NULL; }
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_after_connect(c);
}
return cli;
}
@ -268,7 +317,7 @@ struct stcp_client *stcp_ping_send(struct UASYNC *ua, const char *addr, uint16_t
struct stcp_client *cli = stcp_client_connect(ua, addr, port, keys, peer_pubkey, my_ed25519_pubkey,
0, 0, NULL, device_type, keepalive_interval,
STCP_HANDSHAKE_FLAG_PING,
NULL, NULL, cb, arg, ping_close_cb, NULL, NULL, timeout_ms);
NULL, NULL, cb, arg, ping_close_cb, NULL, NULL, timeout_ms, NULL);
if (cli) cli->conn.free_on_close = cli;
return cli;
}

3
src/transport_layer/stcp_client.h

@ -23,7 +23,8 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
stcp_ping_cb ping_cb, void *ping_arg,
stcp_close_cb close_cb, void *close_arg,
const struct sockaddr_storage *local_addr,
int timeout_ms);
int timeout_ms,
const struct reality_client_config *reality);
/* TCP-ping: подключается и шлёт хендшейк с флагом STCP_HANDSHAKE_FLAG_PING.
* Ответчик отвечает обычным хендшейк-ответом и мягко закрывает соединение.
* По завершении хендшейка вызывается cb(success=1, rtt, arg); при ошибке/таймауте — cb(0, 0, arg). */

18
src/transport_layer/stcp_link.c

@ -183,6 +183,8 @@ struct stcp_server *stcp_server_listen(struct stcp_link_config *cfg, uint16_t po
ss->srv = stcp_server_create(cfg->inst->ua, port, &cfg->inst->my_keys, cfg->inst->my_ed25519_pubkey,
cfg->inst, server_accept_cb, ss, NULL, NULL, cfg->listen_family);
if (!ss->srv) { u_free(ss); return NULL; }
if (cfg->reality_enabled && cfg->inst && cfg->inst->config)
stcp_server_set_reality(ss->srv, &cfg->inst->config->global.reality);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "port=%u", port);
return ss;
}
@ -234,12 +236,26 @@ struct stcp_link *stcp_link_connect(struct ETCP_LINK *etcp_link,
const uint8_t *pubkey = etcp->crypto_ctx.peer_public_key;
// reality-камуфляж на клиенте: включается глобально + есть pubkey сервера + SNI
const struct reality_client_config *reality = NULL;
struct reality_client_config rcc;
if (inst->config && inst->config->global.reality.enabled &&
inst->config->global.reality.has_public_key &&
inst->config->global.reality.server_name[0]) {
memcpy(rcc.server_static_pubkey, inst->config->global.reality.public_key, REALITY_AUTH_KEY_SIZE);
memcpy(rcc.short_id, inst->config->global.reality.short_id, REALITY_SHORT_ID_SIZE);
memcpy(rcc.version, inst->config->global.reality.version, REALITY_VERSION_SIZE);
snprintf(rcc.server_name, sizeof(rcc.server_name), "%s", inst->config->global.reality.server_name);
rcc.fingerprint = inst->config->global.reality.fingerprint;
reality = &rcc;
}
link->cli = stcp_client_connect(inst->ua, addr_str, rport, &inst->my_keys, pubkey,
inst->my_ed25519_pubkey,
etcp->got_initial_pkt, etcp->session_id, etcp,
inst->client_type, inst->keepalive_interval,
0,
client_ready_cb, link, NULL, NULL, NULL, NULL, local_addr, 0);
client_ready_cb, link, NULL, NULL, NULL, NULL, local_addr, 0, reality);
if (!link->cli) { u_free(link); return NULL; }
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_link: connecting to %s:%u pubkey=%016llx bind=%s",

1
src/transport_layer/stcp_link.h

@ -24,6 +24,7 @@ struct UTUN_INSTANCE;
struct stcp_link_config {
struct UTUN_INSTANCE *inst; // rx-диспатч + instance_find_conn; ua/my_keys/ed25519 выводятся отсюда
int listen_family; // AF_INET или AF_INET6 для сервера (0 = v4)
uint8_t reality_enabled; // 1 = включить REALITY-камуфляж на TCP-сервере
};
// ====== TCP server ======

93
src/transport_layer/stcp_server.c

@ -1,6 +1,8 @@
// stcp_server.c — STCP server implementation
#include "stcp_server.h"
#include "secure_channel.h"
#include "reality.h"
#include "reality_relay.h"
#include "crc32.h"
#include "../lib/u_async.h"
#include "../lib/socket_compat.h"
@ -30,6 +32,10 @@ struct stcp_server {
void *cb_arg;
stcp_close_cb close_cb;
void *close_arg;
// reality-камуфляж
uint8_t reality_enabled;
struct reality_server_config reality_srv;
char reality_dest[REALITY_DEST_MAX];
};
static void server_accept_cb(socket_t sock, void *arg);
@ -37,6 +43,8 @@ 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 int derive_session_and_streams(struct stcp_conn *c, const uint8_t *peer_pubkey) {
struct secure_channel sc;
@ -157,6 +165,81 @@ static void server_data_cb(struct stcp_conn *c, uint8_t *plain_data, size_t data
stcp_rx_push(c, plain_data, data_len);
}
// ─── reality-камуфляж (серверная сторона) ───
void stcp_server_set_reality(struct stcp_server *srv, const struct reality_config *rc) {
if (!srv || !rc) return;
srv->reality_enabled = rc->enabled;
if (!rc->enabled) return;
memcpy(srv->reality_srv.static_privkey, rc->private_key, REALITY_AUTH_KEY_SIZE);
memcpy(srv->reality_srv.short_ids, rc->short_ids, sizeof(rc->short_ids));
srv->reality_srv.short_id_count = rc->short_id_count;
memcpy(srv->reality_srv.version, rc->version, REALITY_VERSION_SIZE);
srv->reality_srv.time_window_sec = rc->time_window_sec;
srv->reality_srv.fingerprint = rc->fingerprint;
snprintf(srv->reality_dest, sizeof(srv->reality_dest), "%s", rc->dest);
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;
if (reality_relay_start(c->ua, cli_sock, c->reality_dest, init, init_len) != 0)
u_free(init);
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) {
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) {
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;
int rc = reality_server_hello_build(&c->reality_srv, 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) { 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);
reality_server_start_relay(c, full, 5 + len, c->recv_buf + len, c->recv_buf_len - len);
}
static void server_conn_read_cb(socket_t sock, void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg;
(void)sock;
@ -197,7 +280,15 @@ static void server_accept_cb(socket_t listen_sock, void *arg) {
c->inst = srv->inst;
c->device_type = srv->inst ? srv->inst->client_type : 0;
c->keepalive_interval = srv->inst ? srv->inst->keepalive_interval : 200;
stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT, 0, server_hs_phase1_cb);
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);
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);
}
c->hs_timer = uasync_set_timeout(c->ua, STCP_HS_TIMEOUT, c, hs_timeout_cb, "stcp_hs");
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: accepted connection fd=%d", (int)cli_sock);
}

4
src/transport_layer/stcp_server.h

@ -23,6 +23,10 @@ struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port,
int family);
void stcp_server_destroy(struct stcp_server *srv);
// Включить REALITY-камуфляж на этом TCP-сервере: перед STCP-хендшейком сервер
// ждёт ClientHello, проверяет авторизацию; неавторизованных проксирует на dest.
void stcp_server_set_reality(struct stcp_server *srv, const struct reality_config *rc);
#ifdef __cplusplus
}

5
tests/Makefile.am

@ -75,6 +75,7 @@ check_PROGRAMS = \
test_media_delivery_integration \
test_media_delivery_full \
test_etcp_link_stress \
test_reality_hello \
bench_timeout_heap \
bench_uasync_timeouts
@ -122,6 +123,10 @@ test_stream_sign_SOURCES = test_stream_sign.c
test_stream_sign_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_stream_sign_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_reality_hello_SOURCES = test_reality_hello.c
test_reality_hello_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_reality_hello_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_transport_SOURCES = test_stcp_link.c
test_transport_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_transport_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)

214
tests/test_reality_hello.c

@ -0,0 +1,214 @@
// test_reality_hello.c — тесты модуля reality (REALITY-style ClientHello/ServerHello)
//
// Сценарии:
// 1. Round-trip: клиент собирает ClientHello → сервер проверяет авторизацию и
// собирает ServerHello; проверка структуры обоих (TLS record + handshake,
// echo SessionId).
// 2. Неверный short_id → REALITY_ERR_AUTH.
// 3. Неверный static privkey сервера → REALITY_ERR_AUTH (AES-GCM не сходится).
// 4. Порча байта ClientHello → REALITY_ERR_AUTH (AAD не совпадает).
// 5. Несовпадение версии протокола → REALITY_ERR_AUTH.
// 6. Timestamp вне окна (антиреплей) → REALITY_ERR_AUTH; в окне → OK.
// 7. Ошибки формата (мусор/пусто/неверный тип) → REALITY_ERR_FORMAT.
// 8. Fingerprint-геттер и keygen/pubkey_from_priv.
#include "reality.h"
#include "reality_fingerprint.h"
#include "config_parser.h"
#include "../lib/debug_config.h"
#include <stdio.h>
#include <string.h>
static int test_failed = 0;
#define CHECK(expr, msg) do { \
if (!(expr)) { \
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL: %s", msg); \
test_failed = 1; \
} else { \
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "PASS: %s", msg); \
} \
} while (0)
static uint8_t g_short_id[REALITY_SHORT_ID_SIZE] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
static uint8_t g_short_id_wrong[REALITY_SHORT_ID_SIZE] = { 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8 };
static uint8_t g_version[REALITY_VERSION_SIZE] = { 1, 2, 3 };
static uint8_t g_version_wrong[REALITY_VERSION_SIZE] = { 9, 9, 9 };
static void init_client_cfg(struct reality_client_config *cc,
const uint8_t *server_pub,
const uint8_t *short_id,
const uint8_t *version) {
memset(cc, 0, sizeof(*cc));
memcpy(cc->server_static_pubkey, server_pub, REALITY_AUTH_KEY_SIZE);
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;
}
static void init_server_cfg(struct reality_server_config *sc,
const uint8_t *server_priv,
const uint8_t *short_id,
const uint8_t *version,
int64_t window) {
memset(sc, 0, sizeof(*sc));
memcpy(sc->static_privkey, server_priv, REALITY_AUTH_KEY_SIZE);
memcpy(sc->short_ids[0], short_id, REALITY_SHORT_ID_SIZE);
sc->short_id_count = 1;
memcpy(sc->version, version, REALITY_VERSION_SIZE);
sc->time_window_sec = window;
sc->fingerprint = REALITY_FP_CHROME;
}
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_REALITY);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test ===");
uint8_t srv_priv[REALITY_AUTH_KEY_SIZE], srv_pub[REALITY_AUTH_KEY_SIZE];
CHECK(reality_generate_keypair(srv_priv, srv_pub) == REALITY_OK, "generate server keypair");
{
uint8_t pub2[REALITY_AUTH_KEY_SIZE];
CHECK(reality_pubkey_from_priv(srv_priv, pub2) == REALITY_OK, "pubkey_from_priv");
CHECK(memcmp(srv_pub, pub2, REALITY_AUTH_KEY_SIZE) == 0, "pubkey_from_priv matches");
}
uint8_t other_priv[REALITY_AUTH_KEY_SIZE], other_pub[REALITY_AUTH_KEY_SIZE];
CHECK(reality_generate_keypair(other_priv, other_pub) == REALITY_OK, "generate wrong keypair");
struct reality_client_config cc;
struct reality_server_config sc;
uint8_t ch[REALITY_MAX_CH_SIZE], sh[REALITY_MAX_SH_SIZE];
size_t ch_len = 0, sh_len = 0;
// ── Сценарий 1: round-trip ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build");
CHECK(ch_len > 5, "client hello non-trivial size");
CHECK(ch[0] == 0x16 && ch[1] == 0x03, "client hello TLS record header");
CHECK(ch[5] == 0x01, "client hello handshake type");
{
// проверка согласованности длины: record length == handshake length + 4
size_t rec_len = ((size_t)ch[3] << 8) | ch[4];
CHECK(rec_len + 5 == ch_len, "client hello record length matches");
}
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_OK, "server hello build (auth OK)");
CHECK(sh[0] == 0x16 && sh[5] == 0x02, "server hello record + handshake type");
{
// echo SessionId: client SessionId в ch[44..76], server echo в sh[44..76]
CHECK(memcmp(ch + 44, sh + 44, REALITY_SESSION_ID_SIZE) == 0, "server echoes client SessionId");
}
// ── Сценарий 2: неверный short_id ──
init_client_cfg(&cc, srv_pub, g_short_id_wrong, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (wrong sid)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "wrong short_id rejected");
// ── Сценарий 3: неверный static privkey сервера ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, other_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (wrong key)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "wrong server key rejected");
// ── Сценарий 4: порча байта ClientHello ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (tamper)");
ch[50] ^= 0x01; // портим байт внутри handshake-сообщения
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "tampered hello rejected");
// ── Сценарий 5: несовпадение версии ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version_wrong);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ver mismatch)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "version mismatch rejected");
// ── Сценарий 6: timestamp вне окна ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
{
uint32_t now = (uint32_t)time(NULL);
CHECK(reality_client_hello_build_at(&cc, now, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=now)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_OK, "ts=now accepted");
CHECK(reality_client_hello_build_at(&cc, now - 3600, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=past)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "ts=past rejected (anti-replay)");
CHECK(reality_client_hello_build_at(&cc, now + 3600, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=future)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "ts=future rejected");
}
// ── Сценарий 7: ошибки формата ──
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
{
uint8_t garbage[REALITY_MAX_CH_SIZE];
memset(garbage, 0, sizeof(garbage));
CHECK(reality_server_hello_build(&sc, garbage, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "empty input rejected");
CHECK(reality_server_hello_build(&sc, garbage, 100, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "all-zero input rejected");
memset(garbage, 0, sizeof(garbage));
garbage[0] = 0x17; // не handshake-запись
CHECK(reality_server_hello_build(&sc, garbage, 100, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "wrong record type rejected");
// валидная запись, но не ClientHello
memset(garbage, 0, sizeof(garbage));
garbage[0] = 0x16; garbage[1] = 0x03; garbage[2] = 0x01;
garbage[3] = 0x00; garbage[4] = 0x04; // record length 4
garbage[5] = 0x02; // handshake type = server_hello
CHECK(reality_server_hello_build(&sc, garbage, 9, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "non-client-hello rejected");
}
// ── Сценарий 8: fingerprint-геттер ──
CHECK(reality_fingerprint_get(REALITY_FP_CHROME) != NULL, "fingerprint chrome found");
CHECK(reality_fingerprint_get(999) == NULL, "unknown fingerprint = NULL");
// ── Сценарий 9: невалидные аргументы ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(NULL, ch, sizeof(ch), &ch_len) == REALITY_ERR_ARG, "client build NULL cfg rejected");
CHECK(reality_client_hello_build(&cc, ch, 16, &ch_len) == REALITY_ERR_ARG, "client build small buffer rejected");
CHECK(reality_server_hello_build(&sc, NULL, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_ARG, "server build NULL input rejected");
// ── Сценарий 10: парсинг секции [reality] из конфига ──
{
const char *cfg_text =
"[global]\nname=test\n"
"[reality]\n"
"enabled=1\n"
"server_name=www.microsoft.com\n"
"dest=www.microsoft.com:443\n"
"short_id=0102030405060708\n"
"short_ids=0102030405060708,aabbccddeeff0011\n"
"public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"private_key=2222222222222222222222222222222222222222222222222222222222222222\n"
"version=2.3.4\n"
"time_window=45\n"
"fingerprint=chrome\n";
struct utun_config *uc = parse_config_from_buf(cfg_text, strlen(cfg_text), "mem");
CHECK(uc != NULL, "parse config with [reality]");
if (uc) {
CHECK(uc->global.reality.enabled == 1, "reality enabled parsed");
CHECK(strcmp(uc->global.reality.server_name, "www.microsoft.com") == 0, "server_name parsed");
CHECK(strcmp(uc->global.reality.dest, "www.microsoft.com:443") == 0, "dest parsed");
CHECK(uc->global.reality.has_public_key == 1 && uc->global.reality.has_private_key == 1, "pub/priv keys parsed");
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");
free_config(uc);
}
}
if (test_failed) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: FAILED ===");
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: PASSED ===");
return 0;
}

16
tests/test_stcp.c

@ -104,7 +104,7 @@ static int test1_sizes(void) {
uint16_t port = BASE_PORT + 1;
struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
size_t sizes[] = {0, 1, 16, 17, 255, 256, 1000, 65535};
int n_sizes = 8;
@ -144,7 +144,7 @@ static int test2_many(void) {
uint16_t port = BASE_PORT + 2;
struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
int sent = 0, ticks = 0;
while (srv.msg_count < 200 && ticks < 200) {
@ -185,7 +185,7 @@ static int test3_wrong_key(void) {
struct SC_MYKEYS rogue;
TASSERT(sc_generate_keypair(&rogue) == SC_OK);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, rogue.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, rogue.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
int ticks = 0;
while (ticks < 200) {
@ -210,7 +210,7 @@ static int test4_close(void) {
uint16_t port = BASE_PORT + 4;
struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
int closed = 0, ticks = 0;
while (!srv.closed && ticks < 200) {
@ -260,7 +260,7 @@ static int test5_multi(void) {
struct stcp_client *clients[NCLI] = {0};
for (int i = 0; i < NCLI; i++) {
clients[i] = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &clip[i], NULL, NULL, peer_close_cb, &clip[i], NULL, 0);
clients[i] = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &clip[i], NULL, NULL, peer_close_cb, &clip[i], NULL, 0, NULL);
TASSERT(clients[i]);
}
@ -309,7 +309,7 @@ static int test6_interleaved(void) {
uint16_t port = BASE_PORT + 6;
struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
int round = 0, ticks = 0;
while (srv.msg_count < 50 || cli.msg_count < 50) {
@ -343,7 +343,7 @@ static int test7_bulk_4mb(void) {
uint16_t port = BASE_PORT + 7;
struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
#define N_BULK 64
#define SZ_BULK 65535
@ -382,7 +382,7 @@ static int test8_srv_recv_close(void) {
uint16_t port = BASE_PORT + 8;
struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0); TASSERT(sc);
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", port, &c_keys, s_keys.public_key, NULL, 0, 0, NULL, 0, 200, 0, client_ready_cb, &cli, NULL, NULL, peer_close_cb, &cli, NULL, 0, NULL); TASSERT(sc);
int ticks = 0;
while ((!srv.ready || !cli.ready) && ticks < 200) { uasync_poll(ua, 10); ticks++; }

4
tests/test_stcp_link.c

@ -52,7 +52,7 @@ static int test1_basic(void) {
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", BASE_PORT + 1, &c_keys,
s_keys.public_key, NULL,
0, 0, NULL,
0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0);
0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0, NULL);
TASSERT(sc);
int ticks = 0;
@ -81,7 +81,7 @@ static int test2_wrong_key(void) {
struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", BASE_PORT + 2, &c_keys,
rogue.public_key, NULL,
0, 0, NULL,
0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0);
0, 200, 0, on_cli_ready, &cli_ready, NULL, NULL, NULL, NULL, NULL, 0, NULL);
TASSERT(sc);
int ticks = 0;

Loading…
Cancel
Save