diff --git a/lib/debug_config.c b/lib/debug_config.c index cb024dc7..a7196562 100644 --- a/lib/debug_config.c +++ b/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} }; diff --git a/lib/debug_config.h b/lib/debug_config.h index 2d77e864..cabbec83 100644 --- a/lib/debug_config.h +++ b/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 */ diff --git a/src/Makefile.am b/src/Makefile.am index 8c9bac7e..95be5b92 100644 --- a/src/Makefile.am +++ b/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 \ diff --git a/src/config_parser.c b/src/config_parser.c index 25567af8..c393289b 100644 --- a/src/config_parser.c +++ b/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); diff --git a/src/config_parser.h b/src/config_parser.h index c03edec8..7f54f3bf 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -6,6 +6,7 @@ #include #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 { diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 985abfce..e44c6f0f 100644 --- a/src/transport_layer/etcp_connections.c +++ b/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); diff --git a/src/transport_layer/reality.c b/src/transport_layer/reality.c new file mode 100644 index 00000000..4f62f1e2 --- /dev/null +++ b/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 +#endif + +#define OPENSSL_API_COMPAT 0x10100000L + +#include "reality.h" +#include "reality_fingerprint.h" +#include "../lib/debug_config.h" +#include "../lib/platform_compat.h" +#include +#include +#include +#include + +// 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; +} diff --git a/src/transport_layer/reality.h b/src/transport_layer/reality.h new file mode 100644 index 00000000..08df7c79 --- /dev/null +++ b/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 +#include + +// Размеры +#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 diff --git a/src/transport_layer/reality_fingerprint.c b/src/transport_layer/reality_fingerprint.c new file mode 100644 index 00000000..e164bead --- /dev/null +++ b/src/transport_layer/reality_fingerprint.c @@ -0,0 +1,47 @@ +// reality_fingerprint.c — статические отпечатки для REALITY-камуфляжа +#include "reality_fingerprint.h" +#include "reality.h" +#include + +// 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; +} diff --git a/src/transport_layer/reality_fingerprint.h b/src/transport_layer/reality_fingerprint.h new file mode 100644 index 00000000..5d616f75 --- /dev/null +++ b/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 + +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 diff --git a/src/transport_layer/reality_relay.c b/src/transport_layer/reality_relay.c new file mode 100644 index 00000000..ba2f080f --- /dev/null +++ b/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 +#include +#include +#ifndef _WIN32 +#include +#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; +} diff --git a/src/transport_layer/reality_relay.h b/src/transport_layer/reality_relay.h new file mode 100644 index 00000000..67c22eea --- /dev/null +++ b/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 +#include +#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 diff --git a/src/transport_layer/stcp.h b/src/transport_layer/stcp.h index e1409233..dc235db6 100644 --- a/src/transport_layer/stcp.h +++ b/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); diff --git a/src/transport_layer/stcp_client.c b/src/transport_layer/stcp_client.c index 1220cf87..87e5252e 100644 --- a/src/transport_layer/stcp_client.c +++ b/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; } diff --git a/src/transport_layer/stcp_client.h b/src/transport_layer/stcp_client.h index 5b16ed50..2f326c8a 100644 --- a/src/transport_layer/stcp_client.h +++ b/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). */ diff --git a/src/transport_layer/stcp_link.c b/src/transport_layer/stcp_link.c index 81643c21..413ad149 100644 --- a/src/transport_layer/stcp_link.c +++ b/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", diff --git a/src/transport_layer/stcp_link.h b/src/transport_layer/stcp_link.h index c456d164..9ba5067c 100644 --- a/src/transport_layer/stcp_link.h +++ b/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 ====== diff --git a/src/transport_layer/stcp_server.c b/src/transport_layer/stcp_server.c index 9bfa0b9b..19f937b1 100644 --- a/src/transport_layer/stcp_server.c +++ b/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); } diff --git a/src/transport_layer/stcp_server.h b/src/transport_layer/stcp_server.h index f2085b5d..d9371f90 100644 --- a/src/transport_layer/stcp_server.h +++ b/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 } diff --git a/tests/Makefile.am b/tests/Makefile.am index a1a4f70c..ea433063 100644 --- a/tests/Makefile.am +++ b/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) diff --git a/tests/test_reality_hello.c b/tests/test_reality_hello.c new file mode 100644 index 00000000..47527fb5 --- /dev/null +++ b/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 +#include + +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; +} diff --git a/tests/test_stcp.c b/tests/test_stcp.c index 6ad94505..83454e52 100644 --- a/tests/test_stcp.c +++ b/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++; } diff --git a/tests/test_stcp_link.c b/tests/test_stcp_link.c index b9b4e33e..79441f16 100644 --- a/tests/test_stcp_link.c +++ b/tests/test_stcp_link.c @@ -52,7 +52,7 @@ static int test1_basic(void) { struct stcp_client *sc = stcp_client_connect(ua, "127.0.0.1", BASE_PORT + 1, &c_keys, s_keys.public_key, NULL, 0, 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;