docs [AI]
This commit is contained in:
@@ -1 +1,51 @@
|
||||
# Блок `nrxp` — протокол Netrunner eXchange Protocol
|
||||
|
||||
Прикладной протокол, который ездит **внутри** замаскированного TLS-канала. Снаружи
|
||||
трафик выглядит как обычные TLS-записи `ApplicationData` (`0x17`), а внутри каждой
|
||||
записи — один зашифрованный кадр NRXP с мультиплексированием потоков.
|
||||
|
||||
Блок отвечает только за «упаковку/распаковку» и ничего не знает о сети — он
|
||||
превращает `(stream_id, тип, payload)` в байты и обратно.
|
||||
|
||||
> Детали — в rustdoc, модуль `nrxp`.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Что внутри |
|
||||
|--------------|------------------------------------------------------------------|
|
||||
| `frame.rs` | Структура кадра, (де)сериализация, паддинг. |
|
||||
| `codec.rs` | Шифрующий слой: `TxCodec`/`RxCodec` (кадр ⇄ зашифрованный TLS). |
|
||||
| `bridge.rs` | TLS-обёртка `TlsBridge`: хендшейк и `ApplicationData`. |
|
||||
| `errors.rs` | `TlsError` + стратегия реакции `ErrorAction` (Wait/Redirect/Drop).|
|
||||
|
||||
## Формат кадра (25-байтовый заголовок)
|
||||
|
||||
```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-тег (см. [`crypto`](../crypto)), анти-replay.
|
||||
- **Stream ID** — id логического потока (нечётные у клиента, чётные у сервера).
|
||||
- **Type** — `Connect`/`Data`/`Close`/`Heartbeat`/`UdpConnect`/`UdpData`.
|
||||
- **Padding** — случайные байты против анализа длин; только у управляющих кадров
|
||||
(`Data`/`UdpData` не паддятся ради 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.rs` (буфер `staging`).
|
||||
|
||||
## Связи
|
||||
|
||||
- Шифрование — [`crypto`](../crypto) (`AeadPacker`, `SessionAuth`).
|
||||
- TLS-маскировка и хендшейк — [`tlseng`](../tlseng).
|
||||
- Кадры передаёт/принимает движок туннеля [`net`](../net).
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
//! TLS-обёртка: граница между NRXP и маскирующим слоем [`tlseng`](crate::tlseng).
|
||||
//!
|
||||
//! [`TlsBridge`] — единственная точка, где протокол соприкасается с TLS-кадрами.
|
||||
//! Он умеет две вещи:
|
||||
//!
|
||||
//! 1. **Хендшейк.** Собрать `ClientHello` (клиент) / `ServerHello` (сервер) с
|
||||
//! нужным профилем браузера и провести обмен ключами. На стороне сервера здесь
|
||||
//! же проверяется начальный auth-тег, спрятанный в `session_id` ClientHello, —
|
||||
//! первый барьер против чужих/сканирующих подключений.
|
||||
//! 2. **Data-фаза.** Упаковать готовый шифртекст в TLS-запись `ApplicationData`
|
||||
//! ([`pack_app_data`](TlsBridge::pack_app_data)) и распаковать обратно
|
||||
//! ([`unpack_app_data`](TlsBridge::unpack_app_data)).
|
||||
//!
|
||||
//! Внутренний трейт [`TlsInterceptor`] задаёт общий каркас «распарсить TLS-запись
|
||||
//! → проверить её тип → достать полезное содержимое» для хендшейка и AppData.
|
||||
|
||||
use crate::crypto::{SessionAuth, SessionKeys};
|
||||
use crate::nrxp::errors::{ErrorAction, ErrorStage, TlsError};
|
||||
use crate::parser::Parser;
|
||||
@@ -8,6 +24,9 @@ use crate::tlseng::{ClientHello, HelloHeader, ServerHello};
|
||||
use crate::tlseng::{ContentType, HelloType};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
/// Каркас разбора TLS-записи нужного типа: `start_process` парсит запись и
|
||||
/// делегирует в `handle_record`, который проверяет content-type и извлекает
|
||||
/// типизированный результат.
|
||||
trait TlsInterceptor {
|
||||
type Output;
|
||||
|
||||
@@ -22,11 +41,16 @@ trait TlsInterceptor {
|
||||
fn handle_record(record: TlsRecord) -> Result<Option<Self::Output>, TlsError>;
|
||||
}
|
||||
|
||||
/// Разобранное handshake-сообщение одной из сторон вместе с его TLS-расширениями
|
||||
/// (в расширениях лежат публичный ключ KeyShare и прочие поля, нужные для вывода
|
||||
/// ключей сессии).
|
||||
pub(crate) enum HandshakeMessage {
|
||||
/// `ClientHello` от клиента.
|
||||
Client {
|
||||
base: ClientHello,
|
||||
extensions: ExtensionStack,
|
||||
},
|
||||
/// `ServerHello` от сервера.
|
||||
Server {
|
||||
base: ServerHello,
|
||||
extensions: ExtensionStack,
|
||||
@@ -108,21 +132,31 @@ impl TlsInterceptor for ApplicationData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Фасад TLS-обёртки. Безсостоятельный набор статических операций над буферами;
|
||||
/// всё состояние сессии живёт в [`SessionKeys`], которые передаются явно.
|
||||
pub(crate) struct TlsBridge;
|
||||
|
||||
impl TlsBridge {
|
||||
/// Распаковать handshake-сообщение (`ClientHello`/`ServerHello`) из буфера.
|
||||
pub fn unpack_handshake(buffer: &mut BytesMut) -> Result<Option<HandshakeMessage>, TlsError> {
|
||||
HandshakeMessage::start_process(buffer)
|
||||
}
|
||||
|
||||
/// Распаковать TLS-запись `ApplicationData` (ещё зашифрованный кадр NRXP).
|
||||
pub fn unpack_app_data(buffer: &mut BytesMut) -> Result<Option<ApplicationData>, TlsError> {
|
||||
ApplicationData::start_process(buffer)
|
||||
}
|
||||
|
||||
/// Собрать `ClientHello` по профилю браузера для маскировки (клиент).
|
||||
pub fn wrap_client_hello(profile: &BrowserProfile, host: &str, keys: &SessionKeys) -> Bytes {
|
||||
ClientHello::make_client_hello(profile, host, keys)
|
||||
}
|
||||
|
||||
/// Обработать `ClientHello` и собрать ответный `ServerHello` (сервер).
|
||||
///
|
||||
/// Порядок критичен: сначала проверяется auth-тег из `session_id`
|
||||
/// (16 байт со смещения 16) — неверный тег ⇒ отказ ещё до любых
|
||||
/// криптоопераций; затем выводятся ключи сессии и формируется ответ.
|
||||
pub fn wrap_server_hello(
|
||||
client_msg: &HandshakeMessage,
|
||||
keys: &mut SessionKeys,
|
||||
@@ -156,6 +190,7 @@ impl TlsBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Обернуть готовый шифртекст кадра в TLS-запись `ApplicationData` (`0x17`).
|
||||
pub fn pack_app_data(buffer: Bytes) -> Bytes {
|
||||
TlsRecord::build_application_data(buffer)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
//! Шифрующий кодек: мост между кадрами [`Frame`] и зашифрованными TLS-записями.
|
||||
//!
|
||||
//! Это слой, где встречаются протокол ([`nrxp::frame`](super::frame)),
|
||||
//! криптография ([`crypto`](crate::crypto)) и TLS-обёртка ([`bridge`](super::bridge)).
|
||||
//! Кодек разнесён на два независимых направления, чтобы чтение и запись жили в
|
||||
//! разных задачах tokio без общего мьютекса:
|
||||
//!
|
||||
//! - [`TxCodec`] — `Frame` → AEAD-шифр in-place → TLS `ApplicationData`;
|
||||
//! - [`RxCodec`] — TLS `ApplicationData` → AEAD-дешифр in-place → `Frame`.
|
||||
//!
|
||||
//! [`Codec`] — лишь фабрика: создаёт оба направления из одного [`ChaChaCipher`] и
|
||||
//! `auth_key`, после чего [`split`](Codec::split) раздаёт их reader'у и writer'у.
|
||||
//!
|
||||
//! ## Буфер `staging` в [`RxCodec`]
|
||||
//!
|
||||
//! Опирается на инвариант «1 TLS-запись = 1 кадр NRXP». TLS-записи
|
||||
//! расшифровываются по одной в общий буфер `staging`, и сразу делается попытка
|
||||
//! распарсить кадр. `staging` переживает вызовы `decode_inbound`: если в одном
|
||||
//! TCP-чтении пришло несколько записей, лишние остаются в нём до следующего
|
||||
//! вызова. Любой провал AEAD или парсинга после успешной расшифровки трактуется
|
||||
//! как рассинхрон/tampering → [`ErrorAction::Drop`] (пересоздать ногу с нуля).
|
||||
|
||||
use crate::crypto::{AeadPacker, ChaChaCipher, ChaChaStream, SessionAuth};
|
||||
use crate::nrxp::bridge::TlsBridge;
|
||||
use crate::nrxp::errors::{ErrorAction, ErrorStage, TlsError};
|
||||
@@ -5,6 +27,7 @@ use crate::nrxp::frame::{Frame, FrameType};
|
||||
use crate::parser::Parser;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
/// Исходящее направление: шифрует кадры для отправки в туннель.
|
||||
pub struct TxCodec {
|
||||
crypto: ChaChaStream,
|
||||
auth: SessionAuth,
|
||||
@@ -15,6 +38,11 @@ impl TxCodec {
|
||||
Self { crypto, auth }
|
||||
}
|
||||
|
||||
/// Кодирует один кадр в готовую к отправке TLS-запись `ApplicationData`.
|
||||
///
|
||||
/// Шаги: сгенерировать time-based тег → собрать байты кадра → зашифровать
|
||||
/// in-place (буфер вырастает на 16 байт AEAD-тега) → обернуть в TLS-запись.
|
||||
/// Любая ошибка шифрования критична → [`ErrorAction::Drop`].
|
||||
pub(crate) fn encode_frame(
|
||||
&mut self,
|
||||
stream_id: u32,
|
||||
@@ -42,9 +70,12 @@ impl TxCodec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Входящее направление: расшифровывает TLS-записи и собирает из них кадры.
|
||||
pub struct RxCodec {
|
||||
crypto: ChaChaStream,
|
||||
auth: SessionAuth,
|
||||
/// Накопитель расшифрованного открытого текста между вызовами `decode_inbound`
|
||||
/// (хранит «хвост» кадров, не разобранных в текущем вызове).
|
||||
staging: BytesMut,
|
||||
}
|
||||
|
||||
@@ -56,6 +87,12 @@ impl RxCodec {
|
||||
staging,
|
||||
}
|
||||
}
|
||||
/// Пытается извлечь **один** следующий кадр из накопленных TCP-данных.
|
||||
///
|
||||
/// Возвращает `Ok(Some(frame))`, если кадр готов; `Ok(None)`, если данных
|
||||
/// пока недостаточно (ждём следующего чтения сокета); `Err(Drop)` при провале
|
||||
/// AEAD/парсинга. Сначала дочищает «хвост» из `staging`, затем по одной
|
||||
/// расшифровывает новые TLS-записи из `buffer`.
|
||||
pub(crate) fn decode_inbound(
|
||||
&mut self,
|
||||
buffer: &mut BytesMut,
|
||||
@@ -139,12 +176,17 @@ impl RxCodec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Фабрика кодеков: владеет обоими направлениями до момента, пока их не раздадут
|
||||
/// в задачи reader/writer через [`split`](Codec::split).
|
||||
pub struct Codec {
|
||||
tx: Option<TxCodec>,
|
||||
rx: Option<RxCodec>,
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Создаёт оба направления из шифра сессии и ключа аутентификации.
|
||||
/// `auth` (одна `SessionAuth`) общий для tx и rx — тег зависит только от
|
||||
/// времени и `auth_key`, а не от направления.
|
||||
pub fn new(cipher: ChaChaCipher, auth_key: [u8; 32]) -> Self {
|
||||
let (rx_stream, tx_stream) = cipher.split();
|
||||
let auth = SessionAuth::new(auth_key);
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
//! Ошибки разбора/обработки протокола и стратегия реакции на них.
|
||||
//!
|
||||
//! Ключевая идея — ошибка несёт в себе не только «что и где сломалось»
|
||||
//! ([`ErrorStage`]), но и **что с этим делать** ([`ErrorAction`]). Вызывающий код
|
||||
//! не принимает решение сам: он берёт [`TlsError::action`] и реагирует
|
||||
//! единообразно. Это разводит «частичные данные» (норма — ждём ещё) и
|
||||
//! «вмешательство/рассинхрон» (рвём ногу) по разным веткам без дублирования
|
||||
//! логики в каждом месте парсинга.
|
||||
|
||||
use bytes::Bytes;
|
||||
use netrunner_logger::{error, trace};
|
||||
|
||||
/// Что делать с соединением после ошибки.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ErrorAction {
|
||||
/// Данных пока недостаточно — это не ошибка, ждём следующего чтения сокета.
|
||||
Wait,
|
||||
/// Проксировать как обычный TLS (stealth-fallback) вместо разрыва.
|
||||
Redirect,
|
||||
/// Критично (tampering/рассинхрон) — закрыть ногу и переподключиться.
|
||||
Drop,
|
||||
}
|
||||
|
||||
/// На каком уровне протокола произошла ошибка (со static-описанием причины).
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorStage {
|
||||
/// Слой TLS-записи / шифрования.
|
||||
Tls(&'static str),
|
||||
/// Фаза хендшейка (`ClientHello`/`ServerHello`, обмен ключами).
|
||||
Handshake(&'static str),
|
||||
/// Фаза передачи данных (`ApplicationData`).
|
||||
ApplicationData(&'static str),
|
||||
}
|
||||
|
||||
/// Ошибка протокола: стадия + предписанное действие + (опционально) сырые данные
|
||||
/// для перенаправления/диагностики.
|
||||
#[derive(Debug)]
|
||||
pub struct TlsError {
|
||||
/// Где и почему сломалось.
|
||||
pub stage: ErrorStage,
|
||||
/// Как на это реагировать.
|
||||
pub action: ErrorAction,
|
||||
/// Сырые байты (например, для `Redirect` — переслать как есть; иначе пусто).
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
@@ -81,6 +103,8 @@ impl TlsError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Логирует ошибку с уровнем, соответствующим её серьёзности, и возвращает
|
||||
/// предписанное действие. Вызывающий код матчится по результату.
|
||||
pub fn execute_strategy(&self) -> ErrorAction {
|
||||
self.log_error();
|
||||
self.action
|
||||
|
||||
@@ -1,44 +1,79 @@
|
||||
//! Кадр NRXP: структура и (де)сериализация.
|
||||
//!
|
||||
//! Полный байтовый формат см. в [обзоре модуля](super). Здесь — типы кадра и две
|
||||
//! зеркальные операции:
|
||||
//! - [`Frame::into_bytes`] — собрать заголовок + payload + случайный padding в
|
||||
//! единый [`BytesMut`] (заготовка под последующее AEAD-шифрование на месте);
|
||||
//! - реализации [`Parser`] для [`FrameHeader`] и [`Frame`] — разобрать буфер
|
||||
//! обратно в кадр, не копируя payload лишний раз (zero-copy через `split_to`).
|
||||
//!
|
||||
//! Весь файл написан под zero-copy/zero-alloc на горячем пути — комментарии
|
||||
//! «🔥 ОПТИМИЗАЦИЯ» помечают места, где это сознательно важно.
|
||||
|
||||
use crate::parser::Parser;
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use rand::Rng;
|
||||
|
||||
/// Тип кадра — первый байт после `stream_id`. Числовые значения фиксированы и
|
||||
/// являются частью wire-формата (менять — это смена версии протокола).
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum FrameType {
|
||||
/// Открыть TCP-поток к цели (payload — адрес назначения).
|
||||
Connect = 0x00,
|
||||
/// Данные TCP-потока.
|
||||
Data = 0x01,
|
||||
/// Закрыть поток (FIN/abort).
|
||||
Close = 0x02,
|
||||
/// Keep-alive; держит туннель живым и измеряет RTT.
|
||||
Heartbeat = 0x03,
|
||||
/// Открыть UDP-«сессию» к цели.
|
||||
UdpConnect = 0x04,
|
||||
/// Датаграмма UDP-сессии.
|
||||
UdpData = 0x05,
|
||||
}
|
||||
|
||||
/// Разобранный заголовок кадра (25 байт). Поля идут в том же порядке, что и в wire.
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct FrameHeader {
|
||||
/// Time-based HMAC-тег (анти-replay). Проверяется приёмной стороной.
|
||||
pub(crate) auth_tag: [u8; 16],
|
||||
/// Идентификатор логического потока внутри туннеля.
|
||||
pub(crate) stream_id: u32,
|
||||
/// Длина полезной нагрузки в байтах.
|
||||
pub(crate) payload_len: u16,
|
||||
/// Длина случайного padding после payload (0 для Data/UdpData).
|
||||
pub(crate) padding_len: u16,
|
||||
/// Тип кадра.
|
||||
pub(crate) frame_type: FrameType,
|
||||
}
|
||||
|
||||
/// Полный разобранный кадр: заголовок + payload (без padding — он отбрасывается).
|
||||
pub(crate) struct Frame {
|
||||
// 🔥 ОПТИМИЗАЦИЯ: Поле _padding удалено, так как оно никогда не используется.
|
||||
/// Полезная нагрузка как [`Bytes`] (zero-copy ссылка на исходный буфер).
|
||||
pub(crate) payload: Bytes,
|
||||
/// Разобранный заголовок.
|
||||
pub(crate) header: FrameHeader,
|
||||
}
|
||||
|
||||
// Размеры полей заголовка в байтах (см. формат в обзоре модуля).
|
||||
const AUTH_TAG_SIZE: u16 = 16;
|
||||
const STREAM_ID_SIZE: u16 = 4;
|
||||
const FRAME_TYPE_SIZE: u16 = 1;
|
||||
const PAYLOAD_LEN_SIZE: u16 = 2;
|
||||
const PADDING_LEN_SIZE: u16 = 2;
|
||||
|
||||
/// Суммарный размер заголовка кадра — 25 байт.
|
||||
pub const FRAME_HEADER_SIZE: u16 =
|
||||
AUTH_TAG_SIZE + STREAM_ID_SIZE + FRAME_TYPE_SIZE + PAYLOAD_LEN_SIZE + PADDING_LEN_SIZE; // 25 bytes
|
||||
/// Потолок payload одного кадра (16 КБ). Совпадает с размером interleave-чанка
|
||||
/// writer'а: большие сообщения режутся на куски не больше этого значения.
|
||||
pub const MAX_FRAME_PAYLOAD: usize = 16 * 1024;
|
||||
|
||||
impl Frame {
|
||||
/// Конструирует кадр с нулевым `auth_tag` и `padding_len` — оба заполняются
|
||||
/// позже в [`into_bytes`](Frame::into_bytes) при сериализации.
|
||||
#[inline(always)]
|
||||
pub(crate) fn new(stream_id: u32, frame_type: FrameType, payload: Bytes) -> Self {
|
||||
Self {
|
||||
@@ -53,6 +88,13 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// Сериализует кадр в [`BytesMut`], готовый к шифрованию на месте.
|
||||
///
|
||||
/// `auth_key` здесь — это уже готовый 16-байтовый тег (имя историческое),
|
||||
/// который кладётся в начало заголовка. Для `Data`/`UdpData` padding не
|
||||
/// добавляется (throughput важнее), для остальных типов — 0..255 случайных
|
||||
/// байт. Буфер выделяется один раз точно под итоговый размер; заголовок
|
||||
/// собирается на стеке и пишется одним `copy_from_slice`.
|
||||
#[inline]
|
||||
pub(crate) fn into_bytes(mut self, auth_key: &[u8; 16]) -> BytesMut {
|
||||
// 🔥 ОПТИМИЗАЦИЯ: Быстрая побитовая маска (& 0xFF) вместо дорогого деления с остатком (%)
|
||||
@@ -91,6 +133,8 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбор только заголовка: `can_parse` проверяет, накопились ли 25 байт,
|
||||
/// `parse` читает их и сдвигает курсор буфера (payload остаётся в `bytes`).
|
||||
impl Parser for FrameHeader {
|
||||
type Error = String;
|
||||
|
||||
@@ -147,6 +191,9 @@ impl Parser for FrameHeader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбор полного кадра. `can_parse` подглядывает в поля длин прямо в буфере
|
||||
/// (без сдвига курсора), чтобы убедиться, что пришёл весь кадр целиком; только
|
||||
/// тогда `parse` извлекает заголовок и payload и пропускает padding.
|
||||
impl Parser for Frame {
|
||||
type Error = String;
|
||||
|
||||
@@ -156,6 +203,8 @@ impl Parser for Frame {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Подглядываем payload_len и padding_len по их смещениям в заголовке,
|
||||
// не трогая курсор: байты 21..23 и 23..25.
|
||||
let p_len = u16::from_be_bytes([bytes[21], bytes[22]]) as usize;
|
||||
let pad_len = u16::from_be_bytes([bytes[23], bytes[24]]) as usize;
|
||||
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
//! # Протокол 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** — случайные байты переменной длины: ломают анализ длин пакетов.
|
||||
//! Добавляется только к управляющим кадрам; кадры `Data`/`UdpData` не паддятся
|
||||
//! (их и так много, паддинг бил бы по 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;
|
||||
|
||||
Reference in New Issue
Block a user