From 7e526521eb256da2d0900cac203da2db16935412 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 25 Oct 2026 23:48:38 +0100 Subject: [PATCH] Document REALITY session ownership and test relay failure cleanup --- src/transport_layer/reality.h | 57 ++++++++------- src/transport_layer/reality_io.h | 25 +++++-- src/transport_layer/reality_relay.h | 16 +++-- tests/Makefile.am | 4 ++ tests/test_reality_record.c | 5 ++ tests/test_reality_relay_faults.c | 106 ++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 34 deletions(-) create mode 100644 tests/test_reality_relay_faults.c diff --git a/src/transport_layer/reality.h b/src/transport_layer/reality.h index 2c8c0b61..c6035407 100644 --- a/src/transport_layer/reality.h +++ b/src/transport_layer/reality.h @@ -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]); diff --git a/src/transport_layer/reality_io.h b/src/transport_layer/reality_io.h index 9dbdff3f..90abbc97 100644 --- a/src/transport_layer/reality_io.h +++ b/src/transport_layer/reality_io.h @@ -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 diff --git a/src/transport_layer/reality_relay.h b/src/transport_layer/reality_relay.h index a3b2ecc4..80069786 100644 --- a/src/transport_layer/reality_relay.h +++ b/src/transport_layer/reality_relay.h @@ -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 diff --git a/tests/Makefile.am b/tests/Makefile.am index 0fa6dc88..be27ceb9 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -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) diff --git a/tests/test_reality_record.c b/tests/test_reality_record.c index 87e0960d..95292fd6 100644 --- a/tests/test_reality_record.c +++ b/tests/test_reality_record.c @@ -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; } diff --git a/tests/test_reality_relay_faults.c b/tests/test_reality_relay_faults.c new file mode 100644 index 00000000..2401ff03 --- /dev/null +++ b/tests/test_reality_relay_faults.c @@ -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 +#include +#include +#include + +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; +}