Document REALITY session ownership and test relay failure cleanup

This commit is contained in:
evgeny committed 2026-10-25 23:48:38 +01:00
1 parent 88b05c07d9
commit 7e526521eb
6 files changed
+179 -34

No files matched your search

+33 -24
View File
@@ -45,43 +45,53 @@ extern "C" {
struct reality_client_config;
struct reality_server_config;
/* Контекст одноразового Hello и последующих защищённых записей. */
/* Одноразовая сессия: перед start обнулить, после любой ошибки вызвать cleanup.
* Все функции — в потоке владельца; входы/выходы не должны перекрываться. */
struct reality_session {
uint8_t ephemeral_private[32], auth_key[32], session_id[32];
uint8_t finished_send[32], finished_recv[32];
struct reality_traffic send, recv;
uint8_t ephemeral_private[32], auth_key[32], session_id[32]; // секреты Hello и ожидаемый echo SessionId
uint8_t finished_send[32], finished_recv[32]; // ключи проверки transcript по направлениям
struct reality_traffic send, recv; // независимые ключи и счётчики защищённых записей
void *transcript; // EVP_MD_CTX, точные handshake-байты без заголовков записей
uint32_t auth_timestamp;
uint8_t is_server, keys_ready;
uint32_t auth_timestamp; // timestamp авторизованного ClientHello для replay-кеша I/O
uint8_t is_server, keys_ready; // роль и готовность ключей после обоих Hello
};
// После обоих Finished удаляет transcript и handshake-секреты, сохраняя send/recv; NULL допустим.
void reality_session_handshake_done(struct reality_session *s);
// Освобождает transcript и стирает весь контекст; NULL допустим, повторный вызов безопасен.
void reality_session_cleanup(struct reality_session *s);
// Старт клиента: собирает Hello и сохраняет контекст для client_accept; cap >= REALITY_MAX_CH_SIZE.
// now — Unix-секунды. 0/REALITY_ERR_*; written действителен только при успехе.
int reality_session_client_start(struct reality_session *s, const struct reality_client_config *cfg, uint32_t now,
uint8_t *out, size_t cap, size_t *written);
// Старт сервера: проверяет нормализованный ClientHello и выводит ключи; cap >= REALITY_MAX_SH_SIZE.
// Проверяет время/short_id, но уникальность Hello проверяет replay-кеш reality_io; 0/REALITY_ERR_*.
int reality_session_server_start(struct reality_session *s, const struct reality_server_config *cfg, uint32_t now,
const uint8_t *ch, size_t len, uint8_t *out, size_t cap, size_t *written);
// Клиент принимает один полный ServerHello после client_start и выводит traffic/Finished-ключи; 0/REALITY_ERR_*.
int reality_session_client_accept(struct reality_session *s, const uint8_t *sh, size_t len);
// Добавляет точные handshake-байты (с handshake-заголовком) в SHA256 transcript; 0/-1.
int reality_transcript_add(struct reality_session *s, const uint8_t *data, size_t len);
// Вычисляет 32 байта Finished для текущего transcript; sending выбирает свой/чужой ключ; 0/-1.
int reality_finished(struct reality_session *s, int sending, uint8_t out[32]);
// Конфигурация клиента
struct reality_client_config {
uint8_t server_static_pubkey[REALITY_AUTH_KEY_SIZE]; // X25519 static pubkey сервера
uint8_t short_id[REALITY_SHORT_ID_SIZE];
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")
char server_name[REALITY_SERVER_NAME_MAX]; // непустое NUL-terminated SNI, максимум 255 байт
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 short_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE]; // допустимые идентификаторы клиента
int short_id_count; // 1..REALITY_MAX_SHORT_IDS
uint8_t version[REALITY_VERSION_SIZE]; // принимаемая версия
int64_t time_window_sec; // допуск timestamp; replay-кеш находится в instance
int64_t time_window_sec; // допуск timestamp 0..3600 секунд; replay-кеш в instance
int relay_idle_timeout_sec; // idle-timeout релея неавторизованных (0=выкл)
int fingerprint;
int fingerprint; // REALITY_FP_*; сервер сейчас использует профиль OpenSSL
};
// Общая конфигурация reality (секция [reality] в конфиге utun)
@@ -92,23 +102,21 @@ struct reality_config {
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;
int short_id_count; // число допустимых серверных short_ids
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 has_public_key; // public_key задан в конфиге
uint8_t has_private_key; // private_key задан в конфиге
uint8_t version[REALITY_VERSION_SIZE]; // версия протокола
int time_window_sec;
int time_window_sec; // допустимое расхождение Unix-времени (0..3600)
int relay_idle_timeout_sec; // idle-timeout релея неавторизованных (0=выкл)
int fingerprint;
int fingerprint; // REALITY_FP_*
};
// Заполняет cfg->version/fingerprint/time_window значениями по умолчанию
// (version=2.0.0, fingerprint=openssl, time_window=30). Вызывается при инициализации.
// Сбрасывает cfg: выключен, ключи/строки пусты; version=2.0.0, OpenSSL, time_window=30, relay_idle=5; NULL допустим.
void reality_config_set_defaults(struct reality_config *cfg);
// Заполняет клиентскую конфигурацию (version=2.0.0, fingerprint=openssl).
// Вызывается при парсинге reality-ключей в секции [client].
// Сбрасывает клиентский cfg: ключ/short_id/SNI пусты, version=2.0.0, OpenSSL; NULL допустим.
void reality_client_config_set_defaults(struct reality_client_config *cfg);
// Клиент: собрать ClientHello (TLS record + handshake) с REALITY-авторизацией.
@@ -122,7 +130,8 @@ int reality_client_hello_build_at(const struct reality_client_config *cfg, uint3
uint8_t *out, size_t out_cap, size_t *out_len);
// Сервер: распарсить ClientHello, проверить авторизацию, собрать ServerHello.
// ch/ch_len — принятые байты (TLS record + handshake, допускается одна запись).
// ch/ch_len — ровно одна полная нормализованная запись TLS с ClientHello; out_cap >= REALITY_MAX_SH_SIZE.
// Обёртка без контекста сессии: не проверяет повторы Hello и не сохраняет ключи защищённых записей.
// REALITY_OK — авторизация валидна, out/out_len = ServerHello.
// REALITY_ERR_AUTH — авторизация не прошла (вызывающий код запускает релей).
// REALITY_ERR_* — формат/аргумент/крипто-ошибка.
@@ -138,11 +147,11 @@ int reality_server_hello_build_at(const struct reality_server_config *cfg, uint3
uint8_t *out, size_t out_cap, size_t *out_len);
// Сгенерировать X25519 пару ключей (для конфигурации: сервер хранит privkey,
// клиент — pubkey сервера). Возвращает REALITY_OK / REALITY_ERR_CRYPTO.
// клиент — pubkey сервера). Возвращает REALITY_OK / REALITY_ERR_ARG / 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.
// Вычислить X25519 pubkey из privkey. Возвращает REALITY_OK / REALITY_ERR_ARG / REALITY_ERR_CRYPTO.
int reality_pubkey_from_priv(const uint8_t priv[REALITY_AUTH_KEY_SIZE],
uint8_t pub[REALITY_AUTH_KEY_SIZE]);
+21 -4
View File
@@ -11,7 +11,8 @@
* "uTun-reality-record-v2"); в transcript входят handshake-заголовки, но не record-заголовки.
* От root раздельно выводятся client-traffic/server-traffic и их key/iv/finished-ключи.
* После собственного SH fallback запрещён. До него relay получает весь исходный буфер без нормализации.
* Таймеры/списки/кеш replay принадлежат instance; owner_cleanup принудительно закрывает I/O без ожидания callbacks. */
* Списки relay и кеш replay принадлежат instance; owner_cleanup закрывает только эти общие ресурсы.
* Состояния активных STCP-соединений освобождаются отдельно через reality_io_cleanup. */
#ifndef REALITY_IO_H
#define REALITY_IO_H
#include "reality.h"
@@ -24,37 +25,53 @@ struct reality_io;
struct reality_replay_cache;
struct reality_relay;
struct reality_owner {
struct reality_replay_cache *replay;
struct reality_relay *relays;
struct reality_replay_cache *replay; // ленивый кеш 1024 Hello до истечения timestamp + window
struct reality_relay *relays; // активные relay, не активные STCP-соединения
struct reality_relay *closed_relays; // закрытые контексты до call_soon или синхронного owner_cleanup
unsigned relay_count;
unsigned relay_count; // число активных relay; предупреждение выше 64, жёсткого лимита нет
};
// ── Запуск защищённого соединения ──
// Создаёт оболочку и ставит ClientHello в очередь STCP; now — Unix-секунды, padding копируется (можно NULL).
// ready вызывается после проверки Finished, не внутри start. 0/-1; ошибка полностью удаляет новое I/O.
// c и ready обязательны, c->reality_io должен быть NULL; соединение остаётся собственностью вызывающего.
int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now,
const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *));
// Создаёт серверную оболочку ожидания Hello с настройками c; 0/-1, ready обязателен.
int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *));
// ── Состояние и обмен данными ──
// Число ещё не обработанных wire-байтов; NULL/отсутствие оболочки даёт 0.
size_t reality_io_buffered(const struct stcp_conn *c);
// Статическая диагностическая строка состояния, "none" для NULL/отсутствия оболочки.
const char* reality_io_state_name(const struct stcp_conn *c);
// Неблокирующий recv и обработка записей: 1 — приняты байты, 0 — пока нет, -1 — EOF/ошибка/close_notify.
// Ошибки закрывают STCP; close_notify запускает штатный finish. Обработчики STCP могут закрыть c.
int reality_io_read(struct stcp_conn *c);
// Обрабатывает буфер после снятия rx_paused; учитывает finish/close_notify; NULL допустим.
void reality_io_resume(struct stcp_conn *c);
// Передаёт plaintext после Finished; 0 — отправлено, 1 — есть очередь, -1 — ошибка.
// При успехе забирает и u_free(data); при ошибке буфер остаётся вызывающему, соединение следует закрыть:
// часть записей уже могла попасть в очередь. Для пустой записи допустим NULL/0.
int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len);
// Один раз ставит close_notify: 0 — готово/не требуется, 1 — очередь, -1 — ошибка (требуется закрытие).
int reality_io_close_notify(struct stcp_conn *c);
// ── Освобождение соединения и общих ресурсов ──
// Стирает ключи и освобождает оболочку, не закрывая fd; повторный вызов и NULL безопасны.
void reality_io_cleanup(struct stcp_conn *c);
// Закрывает relay, отменяет их отложенное освобождение и освобождает replay-кеш синхронно.
// До уничтожения UASYNC, вне callbacks relay; owner изначально обнулён, NULL допустим.
void reality_owner_cleanup(struct reality_owner *owner);
#ifdef __cplusplus
+10 -6
View File
@@ -4,6 +4,8 @@
// не прошла (не utun-клиент — DPI-проба, случайный посетитель), соединение
// прозрачно проксируется на реальный сайт (dest), чтобы проба видела настоящий
// HTTPS-сеанс, а не ошибку/разрыв.
// Все операции — в uasync-потоке owner; owner изначально обнулён и живёт до shutdown.
// DNS выполняется асинхронно (только A); подключение использует первый IPv4-адрес.
#ifndef REALITY_RELAY_H
#define REALITY_RELAY_H
@@ -19,20 +21,22 @@ struct UASYNC;
struct reality_owner;
// Начать релей. Забирает владение client_sock (non-blocking, зарегистрирован
// в uasync) и проксирует его на dest ("host:port").
// в uasync либо ещё не зарегистрирован) и проксирует его на dest (IPv4/hostname[:port], по умолчанию 443).
// IPv6 dest не поддерживается. Явный порт — десятичный 1..65535, вся строка dest короче 256 байт.
// initial_data/initial_len — уже прочитанные байты от клиента (ClientHello),
// отправляются на dest первыми (буфер КОПИРУЕТСЯ — владение остаётся у вызывающего).
// idle_timeout_sec — время простоя (без трафика в обе стороны), после которого
// релей принудительно закрывается; 0 = таймер выключен.
// Возвращает 0 = ok, -1 = ошибка (client_sock уже закрыт внутри).
// релей принудительно закрывается; 0 = таймер выключен, максимум INT_MAX/10000 секунд.
// initial_len <= UINT32_MAX; NULL допустим только при initial_len=0.
// Возвращает 0 = запуск принят, -1 = синхронная ошибка (client_sock уже закрыт внутри).
// Ошибки DNS/connect после принятия закрывают relay и логируются; отдельного callback нет.
int reality_relay_start(struct reality_owner *owner, struct UASYNC *ua, socket_t client_sock,
const char *dest,
const uint8_t *initial_data, size_t initial_len,
int idle_timeout_sec);
// Принудительно завершить все активные релеи (вызывается при shutdown).
// Освобождение отложено через uasync_call_soon — после вызова нужно
// uasync_drain_immediate, чтобы реальный u_free прошёл.
// Синхронно завершает активные и освобождает закрытые relay, отменяя их call_soon.
// Вызывать до уничтожения UASYNC и вне callbacks relay; повторный вызов/NULL безопасны.
void reality_relay_shutdown(struct reality_owner *owner);
#ifdef __cplusplus
+4
View File
@@ -268,6 +268,10 @@ endif
if !OS_WINDOWS
check_PROGRAMS += test_socket_monitor
check_PROGRAMS += test_reality_relay_faults
test_reality_relay_faults_SOURCES = test_reality_relay_faults.c
test_reality_relay_faults_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_reality_relay_faults_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=uasync_call_soon -Wl,--wrap=socket_set_nonblocking
check_PROGRAMS += test_call_ring
test_call_ring_SOURCES = test_call_ring.c ../src/transport_layer/call_ring.c
test_call_ring_LDADD = $(COMMON_LIBS)
+5
View File
@@ -62,6 +62,11 @@ static int vectors(void) {
CHECK(!reality_traffic_update(&recv));
CHECK(!reality_record_seal(&send, 23, data, sizeof(data), 0, wire, sizeof(wire), &n));
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type));
CHECK(!reality_traffic_update(&send) && !reality_traffic_update(&recv));
CHECK(send.generation == 2 && recv.generation == 2 && !send.sequence && !recv.sequence);
CHECK(!reality_record_seal(&send, 23, data, sizeof(data), 0, wire, sizeof(wire), &n));
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type));
CHECK(!reality_traffic_init(&send, secret) && !send.generation && !send.sequence);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "HKDF, GCM nonce/AAD, replay, limits, empty DATA and KeyUpdate vectors passed");
return 0;
}
+106
View File
@@ -0,0 +1,106 @@
/* Прямой локальный relay: half-close, строгий dest и владение при отказах таймера/free. */
#include "../src/transport_layer/reality_relay.h"
#include "../src/transport_layer/reality_io.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/platform_compat.h"
#include "test_net.h"
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <string.h>
static int fail_idle, fail_free, fail_nonblock;
extern void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int delay, void* arg, timeout_callback_t cb, const char* name) {
if (fail_idle && !strcmp(name, "relay_idle") && !--fail_idle) return NULL;
return __real_uasync_set_timeout(ua, delay, arg, cb, name);
}
extern void* __real_uasync_call_soon(struct UASYNC*, void*, timeout_callback_t);
void* __wrap_uasync_call_soon(struct UASYNC* ua, void* arg, timeout_callback_t cb) {
if (fail_free) { fail_free = 0; return NULL; }
return __real_uasync_call_soon(ua, arg, cb);
}
extern int __real_socket_set_nonblocking(socket_t);
int __wrap_socket_set_nonblocking(socket_t fd) {
if (fail_nonblock) { fail_nonblock = 0; errno = EIO; return -1; }
return __real_socket_set_nonblocking(fd);
}
static void client_pair(socket_t pair[2]) {
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, pair));
assert(!socket_set_nonblocking(pair[0]) && !socket_set_nonblocking(pair[1]));
}
static void test_failures(void) {
size_t initial = u_get_allocated_count();
struct UASYNC* ua = uasync_create(); assert(ua);
struct reality_owner owner = {0};
const char* invalid[] = {"127.0.0.1:0", "host:-1", "host:65536", "host:1x", "::1"};
for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
socket_t pair[2]; client_pair(pair);
assert(reality_relay_start(&owner, ua, pair[0], invalid[i], NULL, 0, 1) == -1);
assert(!owner.relay_count && !owner.relays);
uint8_t byte; assert(recv(pair[1], &byte, 1, 0) == 0); socket_close_wrapper(pair[1]);
uasync_drain_immediate(ua);
assert(!owner.closed_relays);
}
socket_t pair[2]; client_pair(pair);
assert(reality_relay_start(&owner, ua, pair[0], "127.0.0.1:1", NULL, 0, INT_MAX) == -1);
socket_close_wrapper(pair[1]);
client_pair(pair); fail_idle = fail_free = 1;
assert(reality_relay_start(&owner, ua, pair[0], "127.0.0.1:1", NULL, 0, 1) == -1);
assert(!owner.relay_count && !owner.relays && owner.closed_relays);
reality_owner_cleanup(&owner); assert(!owner.closed_relays);
socket_close_wrapper(pair[1]);
client_pair(pair); fail_nonblock = 1;
assert(reality_relay_start(&owner, ua, pair[0], "127.0.0.1:1", NULL, 0, 1) == -1);
socket_close_wrapper(pair[1]); reality_owner_cleanup(&owner);
uasync_drain_immediate(ua); uasync_destroy(ua, 1);
assert(u_get_allocated_count() == initial);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "PASS relay invalid ports, timeout range, timer/nonblocking failures and retained free context cleanup");
}
static void test_stream(void) {
size_t initial = u_get_allocated_count();
struct UASYNC* ua = uasync_create(); assert(ua);
struct reality_owner owner = {0};
socket_t listener = socket(AF_INET, SOCK_STREAM, 0); assert(listener != SOCKET_INVALID);
struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
assert(!bind(listener, (struct sockaddr*)&address, sizeof(address)) && !listen(listener, 1));
assert(!socket_set_nonblocking(listener));
char destination[64]; snprintf(destination, sizeof(destination), "127.0.0.1:%u", test_bound_port(listener));
socket_t pair[2]; client_pair(pair);
assert(!reality_relay_start(&owner, ua, pair[0], destination, (const uint8_t*)"hello", 5, 1));
socket_t remote = SOCKET_INVALID;
for (unsigned i = 0; remote == SOCKET_INVALID && i < 1000; i++) { remote = accept(listener, NULL, NULL); uasync_poll(ua, 10); }
assert(remote != SOCKET_INVALID && !socket_set_nonblocking(remote));
assert(socket_send(pair[1], "tail", 4) == 4 && !shutdown(pair[1], SHUT_WR));
char input[16]; size_t have = 0; int eof = 0;
for (unsigned i = 0; !eof && i < 1000; i++) {
ssize_t n = recv(remote, input + have, sizeof(input) - have, 0);
if (n > 0) have += (size_t)n;
else if (!n) eof = 1;
uasync_poll(ua, 10);
}
assert(eof && have == 9 && !memcmp(input, "hellotail", 9));
assert(socket_send(remote, "response", 8) == 8 && !shutdown(remote, SHUT_WR));
have = 0; eof = 0;
for (unsigned i = 0; !eof && i < 1000; i++) {
ssize_t n = recv(pair[1], input + have, sizeof(input) - have, 0);
if (n > 0) have += (size_t)n;
else if (!n) eof = 1;
uasync_poll(ua, 10);
}
assert(eof && have == 8 && !memcmp(input, "response", 8) && !owner.relay_count);
reality_owner_cleanup(&owner);
socket_close_wrapper(pair[1]); socket_close_wrapper(remote); socket_close_wrapper(listener);
uasync_drain_immediate(ua); uasync_destroy(ua, 1); assert(u_get_allocated_count() == initial);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "PASS relay original prefix, backpressure state, bidirectional half-close and cleanup");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_WARN);
test_failures(); test_stream(); return 0;
}