99a9eee908
Маскировка: - ChangeCipherSpec middlebox-compat запись после ClientHello/ServerHello — без неё последовательность типов TLS-записей отличала нас от настоящего браузера даже при идеальном JA3/JA4-отпечатке. - SNI-aware stealth fallback: невалидный клиент проксируется на запрошенный им хост, а не всегда на один и тот же decoy — иначе активное зондирование с разными SNI на одном IP получало одинаковый ответ. Резолв только в публичные IP (защита от SSRF на внутреннюю сеть ноды через подставной SNI). - Профиль браузера привязан к session_id, а не к номеру попытки реконнекта: раньше при нескольких быстрых ретраях с одного IP летели ClientHello Chrome→Edge→Firefox подряд — сама по себе аномалия для DPI. - Бакетный паддинг Data/UdpData кадров вместо полного отсутствия паддинга; джиттер перед ServerHello вместо мгновенного детерминированного ответа. Версионирование: - PROTOCOL_VERSION в session_id ClientHello — сервер узнаёт версию клиента до отправки чего-либо и может включать версионно-зависимое поведение (сейчас — CCS) только для тех, кто его понимает. Позволяет катить будущие несовместимые изменения протокола без синхронного flag-day релиза. Тесты (core, было 0): - Round-trip тесты TLS-записей/hello-сообщений/кадров/кодека. - Полный цикл хендшейка с реальным выводом ключей на обеих сторонах. - Вся SSRF-защита (hostname/IP валидация, v4/v6). - Интеграционные тесты по настоящему TCP: легитимный хендшейк и stealth-fallback на мусорный ClientHello. tools/loadtest — нагрузочный тест поверх настоящего скомпилированного netrunner-server (реальный процесс, метрики RSS/CPU из /proc) и настоящего публичного клиентского API. Попутно найден и обойдён гоночный баг: Connect-кадр в ClientHandler::connect() уходит через try_send ещё до того, как поднимется хоть одна нога тунеля, и молча теряется при синтетической нагрузке. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
60 lines
4.3 KiB
Rust
60 lines
4.3 KiB
Rust
//! # Протокол NRXP (`nrxp`) — Netrunner eXchange Protocol
|
||
//!
|
||
//! Прикладной протокол, который ездит **внутри** замаскированного TLS-канала.
|
||
//! Снаружи трафик выглядит как обычные TLS-записи `ApplicationData` (`0x17`), а
|
||
//! внутри каждой записи лежит один зашифрованный кадр NRXP с мультиплексированием
|
||
//! логических потоков.
|
||
//!
|
||
//! Этот блок отвечает за «упаковку/распаковку» и ничего не знает о сети как
|
||
//! таковой — он лишь превращает `(stream_id, тип, payload)` в байты и обратно.
|
||
//!
|
||
//! ## Состав блока
|
||
//!
|
||
//! | Файл | Ответственность |
|
||
//! |-------------|-----------------------------------------------------------------------|
|
||
//! | [`frame`] | Структура кадра, его (де)сериализация, паддинг. |
|
||
//! | [`codec`] | Шифрующий слой: [`TxCodec`]/[`RxCodec`] (кадр ⇄ зашифрованный TLS). |
|
||
//! | [`bridge`] | TLS-обёртка: хендшейк (`ClientHello`/`ServerHello`) и `ApplicationData`.|
|
||
//! | [`errors`] | [`TlsError`] и стратегия реакции ([`ErrorAction`]: Wait/Redirect/Drop).|
|
||
//!
|
||
//! ## Формат кадра (25-байтовый заголовок + payload + padding)
|
||
//!
|
||
//! ```text
|
||
//! ┌──────────────┬───────────┬──────┬────────────┬────────────┬─────────┬─────────┐
|
||
//! │ Auth Tag │ Stream ID │ Type │ Payload Len│ Padding Len│ Payload │ Padding │
|
||
//! │ 16 байт │ 4 байта │ 1 б. │ 2 байта │ 2 байта │ N байт │ 0..255 │
|
||
//! └──────────────┴───────────┴──────┴────────────┴────────────┴─────────┴─────────┘
|
||
//! └──────────────────── FRAME_HEADER_SIZE = 25 ────────────────────┘
|
||
//! ```
|
||
//!
|
||
//! - **Auth Tag** — TOTP-подобный HMAC-тег (см. [`SessionAuth`](crate::crypto)),
|
||
//! привязан ко времени → защита от replay со стороны DPI.
|
||
//! - **Stream ID** — id логического потока внутри туннеля (нечётные у клиента,
|
||
//! чётные у сервера — так стороны не конфликтуют за номера).
|
||
//! - **Type** — [`FrameType`]: `Connect`/`Data`/`Close`/`Heartbeat`/`UdpConnect`/`UdpData`.
|
||
//! - **Padding** — ломает анализ длин пакетов. Управляющие кадры получают
|
||
//! 0..255 случайных байт; `Data`/`UdpData` выравниваются до ближайшего
|
||
//! "круглого" бакета (256/512/1024/2048/4096/8192, см. `Frame::into_bytes`)
|
||
//! — кроме кадров, уже близких к максимальному размеру (крупные закачки),
|
||
//! где паддинг только бил бы по throughput без выигрыша в приватности.
|
||
//!
|
||
//! ## Конвейер кодирования
|
||
//!
|
||
//! ```text
|
||
//! TX: Frame::new → into_bytes(tag) → AEAD encrypt in-place → TlsBridge::pack_app_data
|
||
//! RX: TlsBridge::unpack_app_data → AEAD decrypt in-place (staging) → Frame::parse
|
||
//! ```
|
||
//!
|
||
//! **Инвариант кодирования:** одна TLS-запись `ApplicationData` = ровно один
|
||
//! зашифрованный кадр NRXP. На нём держится потоковая расшифровка в [`codec`].
|
||
|
||
mod bridge;
|
||
mod codec;
|
||
mod errors;
|
||
mod frame;
|
||
|
||
pub(crate) use bridge::TlsBridge;
|
||
pub(crate) use codec::{Codec, RxCodec, TxCodec};
|
||
pub(crate) use errors::{ErrorAction, ErrorStage, TlsError};
|
||
pub(crate) use frame::{Frame, FrameType, MAX_FRAME_PAYLOAD};
|