# Протокол Aura Протокол Aura обеспечивает взаимно аутентифицированный, пост-квантово стойкий туннель между клиентом и сервером. Он реализован в крейте `aura-proto` поверх `aura-crypto` (гибридный KEM, HKDF, AEAD) и `aura-pki` (взаимная проверка X.509). Этот документ предназначен для инженера, который проводит аудит протокола или реализует его заново. Всё, что описано ниже, отражает **фактическую реализацию**, а не идеализированную спецификацию. Там, где исходная спецификация была неоднозначной (особенно порядок сообщений рукопожатия), реализация фиксирует конкретный выбор — и именно этот зафиксированный выбор здесь задокументирован. --- ## Транспорт v2: свой UDP-канал, TCP/443 и QUIC как fallback Это ключевое изменение по сравнению с ранней версией: основной канал данных теперь — это **собственный транспорт Aura поверх обычного UDP**, без QUIC и без внешнего TLS на основном пути. Единственная граница безопасности — внутреннее рукопожатие Aura (гибридное X25519 + ML-KEM-768 со взаимной X.509), которое само по себе уже пост-квантовое. QUIC сохраняется как fallback и средство камуфляжа. Доступны три транспорта, все они выдают единый `Arc`, поэтому маршрутизатору туннеля безразлично, какой транспорт перенёс соединение (`crates/aura-transport/src/dial.rs`): | Режим (`TransportMode`) | Реализация | Роль | |-------------------------|------------|------| | `Udp` | `crates/aura-transport/src/udp.rs` | Свой протокол Aura поверх обычного UDP — **основной путь** | | `Tcp` | `crates/aura-transport/src/tcp.rs` | Aura поверх TCP/443 (fallback для сетей, блокирующих UDP; опц. HTTP-маскировка) | | `Quic` | `crates/aura-transport/src/lib.rs`, `conn.rs`, `quic.rs` | Aura внутри QUIC/HTTP3-мимикрии (fallback / сильный камуфляж) | ### Порядок переключения (handover) Клиентский `dial` (`dial.rs`) пробует транспорты по списку `order` слева направо; первый, который дозвонился, побеждает. По умолчанию `order = [Udp, Tcp, Quic]`. Транспорт без сконфигурированного адреса пропускается; транспорт, который ошибся или вышел по таймауту (`attempt_timeout`, по умолчанию 8 с), уступает место следующему. ``` dial(order = [Udp, Tcp, Quic]) └─ Udp → UdpClient::connect ── ок? → соединение по UDP │ ошибка/таймаут └─ Tcp → TcpClient::connect ── ок? → соединение по TCP │ ошибка/таймаут └─ Quic → AuraClient::connect ── ок? → соединение по QUIC │ все упали Err(последняя ошибка) ``` Серверная сторона (`MultiServer`, `dial.rs`) поступает иначе: она привязывается и слушает **все** включённые в `order` транспорты одновременно и выдаёт принятые соединения из любого из них через один `MultiServer::accept`. > **Важно про порты.** Свой UDP-транспорт и QUIC оба используют UDP, поэтому им нужны **разные** > порты (`udp_port` ≠ `quic_port`). TCP может занимать тот же номер порта, что и UDP-транспорт (это > другой протокол). См. `docs/deployment.md`. ### Свой транспорт поверх UDP (основной путь) Один сокет `tokio::net::UdpSocket` несёт обе фазы, различаемые по первому байту-типу (`crates/aura-transport/src/udp.rs`): ```text 0x01 HS : 0x01 || hs_seq(u16 BE) || ack_upto(u16 BE) || msg_bytes 0x02 DATA : 0x02 || rec_len(u16 BE) || sealed_record [|| random_padding] ``` * **Фаза рукопожатия (`0x01` HS).** Рукопожатие Aura написано поверх `AsyncRead` + `AsyncWrite` и обменивается целыми кадрами. UDP теряет и переупорядочивает датаграммы, поэтому рукопожатие выполняется через `ReliableHsAdapter` — небольшой слой надёжности в стиле «полётов» DTLS: - **границы сообщений.** Записанные байты буферизуются; адаптер парсит 5-байтный заголовок кадра Aura, чтобы узнать полный размер сообщения (`5 + len`), и эмитит ровно одну HS-датаграмму на каждое целое сообщение (границы берутся из заголовка, а не из `flush`). - **надёжность отправки.** Каждая отправленная HS-датаграмма хранится в упорядоченной карте по `hs_seq`. Каждые `hs_rto` (по умолчанию 250 мс) все ещё не подтверждённые датаграммы перепосылаются, пока их не подтвердят или пока не истечёт общий дедлайн `hs_timeout` (по умолчанию 10 с → ошибка). - **подтверждения (ack).** Каждая HS-датаграмма несёт `ack_upto` = наибольший **непрерывный** полученный `hs_seq` (кумулятивный ack); значение-сентинел `ACK_NONE = 0xFFFF` означает «ничего не получено». При приёме адаптер отбрасывает свои неподтверждённые записи с `hs_seq <= ack_upto`. Если подтверждать есть что, а посылать нечего, эмитится «голая» HS-датаграмма с пустым телом. - **упорядочивание приёма.** Полученные HS-полезные нагрузки буферизуются в карте по `hs_seq` и выдаются читателю строго в непрерывном порядке; дубликаты отбрасываются. - **linger.** После завершения рукопожатия короткая фаза дослки (`hs_linger`, по умолчанию 2 с) повторно отправляет последний «полёт», чтобы потеря финального сообщения не сорвала установление. * **Фаза данных (`0x02` DATA).** После рукопожатия берутся `Session::into_datagram_parts`, и каждый прикладной пакет уходит как одна explicit-nonce AEAD-запись в одной UDP-датаграмме — ненадёжно, ровно как сама сеть. Потери/переупорядочивание — забота вызывающего (Aura туннелирует IP-пакеты, которые это терпят), а датаграммный кодек уже проверяет replay. Для DATA `sealed_record` — это ровно `rec_len` байт (один вывод `DatagramSender::seal`); любые хвостовые байты — это обфускационный паддинг, и приёмник их игнорирует (читает ровно `rec_len`). **Обфускация (`UdpOpts::obfuscate`).** Когда включена, каждая исходящая DATA-датаграмма дополняется случайными байтами до следующей «корзины» размера из `padding::HTTPS_SIZE_BUCKETS` (`[64, 128, 256, 512, 1024, 1280, 1460]`), чтобы размыть распределение размеров на проводе под HTTPS-подобное. Приёмник читает ровно `rec_len` запечатанной записи и игнорирует паддинг. **Один пир на принятое соединение (v1).** `UdpServer::accept` обслуживает **одного** клиента за вызов: ждёт первую HS-датаграмму клиента, фиксирует адрес источника, проводит рукопожатие, привязанное к этому адресу, и возвращает выделенное `UdpConnection`. Для обслуживания многих клиентов на одном порту понадобился бы слой демультиплексирования по адресу источника — это вне рамок v1; пока для нескольких клиентов предпочтительны TCP/QUIC. ### TCP/443 (fallback) `crates/aura-transport/src/tcp.rs` гоняет **то же** рукопожатие Aura и `aura_proto::Session` напрямую поверх `TcpStream` (он уже реализует `AsyncRead` + `AsyncWrite`). Никакой дополнительной криптографии и никакого QUIC — граница безопасности по-прежнему внутреннее рукопожатие Aura. **Опциональная HTTP-маскировка (`TcpOpts::masquerade`).** Перед рукопожатием стороны обмениваются минимальной преамбулой HTTP/1.1 (клиент шлёт `GET / HTTP/1.1` с заголовком `Host`, сервер отвечает `HTTP/1.1 200 OK`), так что начало соединения для поверхностного наблюдателя похоже на обычный HTTP. Это **лёгкая маскировка, а не TLS** — полноценная HTTPS/TLS-443-мимикрия (переиспользование внешнего слоя rustls из QUIC-бэкенда) запланирована; сейчас задача TCP — пропихнуть байты там, где UDP заблокирован. Преамбула читается побайтно до терминатора `\r\n\r\n`, чтобы не залезть в поток рукопожатия. ### QUIC (fallback / камуфляж) QUIC-путь (`crates/aura-transport/`) переносит протокол Aura поверх настоящего QUIC и выдаёт установленное соединение как `aura_proto::PacketConnection`. У него два слоя, и какой из них является границей безопасности — ключевой момент дизайна (подробности — в разделе [Слой мимикрии](#слой-мимикрии)): - **Внешний QUIC/TLS** — это камуфляж под трафик браузера HTTP/3. Он **не** выполняет осмысленной аутентификации. - **Внутреннее рукопожатие/сессия Aura** — настоящая граница безопасности. ``` +-------------------------------------------------------------+ | Прикладные IP-пакеты (TUN) | +-------------------------------------------------------------+ | Внутренняя сессия Aura: Frame -> AEAD-запись Data | <- настоящая граница безопасности | Внутреннее рукопожатие Aura: гибридный KEM + взаимный X.509 | +-------------------------------------------------------------+ | Несущий транспорт: | | • свой UDP (без TLS) — основной | | • TCP/443 (опц. HTTP-маскировка) — fallback | | • QUIC/TLS (мимикрия под HTTP/3) — fallback / камуфляж | +-------------------------------------------------------------+ ``` Внутренний протокол транспортно-независим: `client_handshake` / `server_handshake` обобщены по отдельным `tokio::io::AsyncRead`-читателю и `AsyncWrite`-писателю, поэтому один и тот же код одинаково управляет надёжным UDP-адаптером, TCP-потоком, разрезанными `RecvStream` / `SendStream` от quinn (QUIC) и in-memory дуплекс-каналом (тесты). --- ## Формат кадра Каждое сообщение протокола Aura — это **5-байтный заголовок**, за которым следует полезная нагрузка (`crates/aura-proto/src/frame.rs`): ``` байт 0 : msg_type (u8) байты 1..4 : length (u24, big-endian) = длина полезной нагрузки в байтах байт 4 : version = 0x01 байты 5.. : payload (length байт) ``` - `length` — это 24-битное целое big-endian, так что максимальная полезная нагрузка — `0x00FF_FFFF` (16 МиБ − 1). Слишком большая нагрузка отвергается с `FrameTooLarge`. - `version` равна `0x01`. Заголовок, у которого байт 4 не `0x01`, отвергается с `BadVersion`. ### Типы сообщений | Байт | `MsgType` | Направление | Шифрование | Роль | |--------|---------------|-------------|------------|--------------------------------------------| | `0x01` | `ClientHello` | C→S | нет | Рукопожатие 1: гибридный публичный ключ + nonce | | `0x02` | `ServerHello` | S→C | нет | Рукопожатие 2: гибридный шифртекст + nonce | | `0x03` | `ClientAuth` | C→S | да | Рукопожатие 4: сертификат клиента + подпись | | `0x04` | `ServerAuth` | S→C | да | Рукопожатие 3: сертификат сервера + подпись | | `0x05` | `Finished` | оба | да | Рукопожатие 5/6: HMAC по transcript | | `0x06` | `Data` | оба | да | Прикладная запись (AEAD-запечатанный `Frame`) | | `0xFF` | `Alert` | оба | нет | Фатальный alert; байт 0 нагрузки — код | > Замечание: числовые значения байтов **не** соответствуют порядку отправки. `ServerAuth` (`0x04`) > отправляется *перед* `ClientAuth` (`0x03`). Порядок отправки задаётся конечным автоматом (ниже), а > не байтом-типом. ### Прикладные кадры Когда сессия установлена, прикладная нагрузка внутри каждой зашифрованной записи `Data` — это `Frame` (`crates/aura-proto/src/frame.rs`). Все многобайтные целые — big-endian: | Кадр | Тег | Кодирование | |---------|--------|------------------------------------------------------| | `Data` | `0x01` | `0x01 \|\| stream_id(u32) \|\| payload` | | `Ping` | `0x02` | `0x02 \|\| seq(u32)` | | `Pong` | `0x03` | `0x03 \|\| seq(u32)` | | `Close` | `0x04` | `0x04 \|\| code(u8) \|\| reason_len(u32) \|\| reason_utf8` | На всех трёх транспортах прикладные IP-пакеты упаковываются как `Frame::Data` на `stream_id 0`; входящий `Ping` отвечается `Pong`, лишний `Pong` игнорируется, а `Close` всплывает как ошибка. --- ## Рукопожатие ### Зафиксированный порядок сообщений Исходная диаграмма спецификации была неоднозначна насчёт порядка зашифрованных сообщений auth/Finished. Реализация фиксирует ровно такой порядок, и обе стороны следуют ему шаг в шаг (`crates/aura-proto/src/handshake.rs`): ``` 1. C -> S ClientHello (открытый текст): x25519_pub[32] || mlkem_ek[1184] || client_nonce[32] 2. S -> C ServerHello (открытый текст): x25519_ephemeral[32] || mlkem_ct[1088] || server_nonce[32] -- обе стороны выводят гибридный общий секрет и два направленных SessionKeys -- 3. S -> C ServerAuth (зашифровано под s2c): u16(cert_der_len) || server_leaf_cert_der || sig(transcript) 4. C -> S ClientAuth (зашифровано под c2s): u16(cert_der_len) || client_leaf_cert_der || sig(transcript) 5. C -> S Finished (зашифровано под c2s): HMAC-SHA256(key_c2s, transcript) 6. S -> C Finished (зашифровано под s2c): HMAC-SHA256(key_s2c, transcript) -- зашифрованный канал Data теперь открыт в обе стороны -- ``` ```mermaid sequenceDiagram participant C as Клиент participant S as Сервер Note over C,S: открытый текст C->>S: 1. ClientHello (x25519_pub, mlkem_ek, client_nonce) S->>C: 2. ServerHello (x25519_eph, mlkem_ct, server_nonce) Note over C,S: обе стороны выводят общий секрет + SessionKeys
transcript = SHA-256(CH_frame || SH_frame) Note over C,S: зашифровано (AEAD под направленными ключами) S->>C: 3. ServerAuth (сертификат сервера + подпись по transcript) C->>S: 4. ClientAuth (сертификат клиента + подпись по transcript) C->>S: 5. Finished (HMAC_c2s по transcript) S->>C: 6. Finished (HMAC_s2c по transcript) Note over C,S: сессия установлена; записи Data идут в обе стороны ``` ### Полезные нагрузки Hello (точные размеры) | Поле | ClientHello | ServerHello | Байт | |-------------------|:-----------:|:-----------:|-----:| | X25519 pub / eph | ✔ | ✔ | 32 | | ML-KEM-768 ek | ✔ | | 1184 | | ML-KEM-768 ct | | ✔ | 1088 | | nonce | ✔ | ✔ | 32 | | **Итого нагрузка**| **1248** | **1152** | | Hello отправляются в открытом виде и при приёме проверяются на точную длину; неверная длина отвергается с `MalformedHandshake`. ### Transcript-хеш ``` transcript = SHA-256( ClientHello_frame_bytes || ServerHello_frame_bytes ) ``` Хеш покрывает **полные сериализованные кадры** (5-байтный заголовок + полезная нагрузка) ClientHello и ServerHello, ровно как они передаются на проводе. Это привязывает согласованный ключевой материал и версию протокола одновременно к подписям и к MAC-ам Finished. ### Аутентификация (ServerAuth / ClientAuth) Каждая нагрузка Auth: ``` u16_be(cert_der_len) || leaf_cert_der || signature ``` - `leaf_cert_der` — это **листовой сертификат** отправителя в DER (передаётся встроенным, без цепочки — CA является якорем доверия на принимающей стороне). - `signature` — это подпись **ECDSA P-256 / SHA-256**, в кодировке ASN.1 DER (`ECDSA_P256_SHA256_ASN1`), вычисленная по 32-байтному `transcript` (через `ring`). Проверка (`crates/aura-proto/src/handshake.rs`): 1. Принимающая сторона строит `AuraCertVerifier` из настроенного PEM своего CA и проверяет листовой сертификат пира против CA (цепочка + назначение ключа + срок действия; см. `pki.md`). - **Клиент** дополнительно требует, чтобы листовой сертификат сервера был валиден для ожидаемого `server_name` (совпадение DNS SAN). - **Сервер** захватывает проверенный **id клиента** (Common Name листа) и сохраняет его как `peer_id` сессии. 2. Принимающая сторона извлекает точку EC-публичного ключа из листа и проверяет `signature` по `transcript`. Неуспех — это `Signature(...)`. Таким образом, владение приватным ключом сертификата доказывается подписью по transcript, а личность сертификата — проверкой цепочки против CA. ### Finished Каждая сторона отправляет, затем проверяет, MAC Finished, привязанный к transcript и ключу направления: ``` Finished_c2s = HMAC-SHA256(key_c2s, transcript) // отправляет клиент (сообщение 5), проверяет сервер Finished_s2c = HMAC-SHA256(key_s2c, transcript) // отправляет сервер (сообщение 6), проверяет клиент ``` Проверка выполняется за постоянное время (`Hmac::verify_slice`); несовпадение — это `FinishedMismatch`. Обмен Finished подтверждает, что обе стороны вывели одинаковые ключи и согласны по всему transcript. ### Зашифрованные сообщения рукопожатия и непрерывность счётчиков Сообщения 3–6 запечатываются AEAD под **теми же** двумя направленными `AeadSession`, что защищают прикладные Data; их счётчики nonce непрерывны через границу рукопожатие/данные. - AAD каждого зашифрованного сообщения рукопожатия — это его 5-байтный заголовок кадра (привязка типа + длины), как и в записях Data. - Каждое направление запечатывает **ровно два** зашифрованных сообщения рукопожатия до начала Data: - c2s запечатывает `ClientAuth` (счётчик 0) и `Finished` (счётчик 1) - s2c запечатывает `ServerAuth` (счётчик 0) и `Finished` (счётчик 1) - Поэтому оба направления достигают AEAD-счётчика **2** в конце рукопожатия, и первая прикладная запись Data ставит `seq == 2` (`POST_HANDSHAKE_COUNTER`). Это задаёт начальное состояние replay-окна (ниже). На датаграммном (UDP) пути это же значение счётчика переносится в explicit-nonce кодеки через `into_datagram_parts`, так что nonce, уже использованные в рукопожатии, не переиспользуются. --- ## Гибридный KEM Обмен ключами — гибрид классического X25519 ECDH и пост-квантового ML-KEM-768 (`crates/aura-crypto/src/kem/`). Атакующему нужно сломать **оба** примитива, чтобы восстановить ключ сессии. > **ML-KEM-768 (FIPS 203)** через крейт RustCrypto `ml-kem` (v0.3) — это стандартизованная схема > FIPS 203, а **не** Kyber раунда 3. ### Роли - **Клиент** владеет долговременным `HybridPrivateKey` и публикует свой `HybridPublicKey` в ClientHello. - **Сервер** вызывает `encapsulate()` против этого публичного ключа: он генерирует **эфемерную** пару X25519 и ML-KEM-инкапсуляцию, возвращает `HybridCiphertext` в ServerHello и выводит общий секрет. - **Клиент** восстанавливает тот же секрет через `decapsulate()`. То есть X25519 здесь **эфемерно–статический** (эфемерный сервера против статического публичного клиента), а ML-KEM — это стандартный KEM против инкапсуляционного ключа клиента. ### Размеры | Величина | Байт | Константа | |-----------------------------------|------:|---------------------| | X25519 публичный / эфемерный / секрет | 32 | `X25519_LEN` | | ML-KEM-768 инкапсуляционный ключ (ek) | 1184 | `EK_LEN` | | ML-KEM-768 шифртекст (ct) | 1088 | `CT_LEN` | | ML-KEM-768 общий секрет | 32 | `SS_LEN` | | ML-KEM-768 деинкапсуляционный ключ (dk) | 2400 | `DK_LEN` | > **Деталь реализации — кодирование dk.** Деинкапсуляционный (секретный) ключ хранится в > **развёрнутой 2400-байтной** форме FIPS 203 (`ExpandedKeyEncoding`), а не в 64-байтном seed, > который предпочитает `ml-kem` 0.3. Именно с этим кодированием работают KAT-векторы ACVP / FIPS 203 > проекта, поэтому оно используется для совместимости/interop. dk никогда не путешествует по проводу — > по нему идут только `ek` (1184 Б) и `ct` (1088 Б). ### Объединённый общий секрет ``` shared = x25519_ss (32 Б) || mlkem_ss (32 Б) // всего 64 байта ``` Деинкапсуляция ML-KEM не может завершиться ошибкой на корректно размерном шифртексте: подделанный шифртекст даёт псевдослучайный секрет (неявное отклонение), а не ошибку, что позже всплывает как сбой AEAD/Finished. --- ## Вывод ключей (HKDF) Направленные ключи сессии выводятся с помощью **HKDF-SHA256** (RFC 5869) (`crates/aura-crypto/src/kdf.rs`): ``` salt = client_nonce || server_nonce (64 байта) IKM = x25519_ss || mlkem_ss (64 байта) info = "aura-v1-session" OKM = HKDF-Expand(HKDF-Extract(salt, IKM), info, 64) (64 байта) key_client_to_server = OKM[0..32] key_server_to_client = OKM[32..64] ``` Вывод полностью детерминирован по своим входам. Строка `info` обеспечивает доменное разделение. Промежуточный секретный материал (`salt`, `IKM`, `OKM`) обнуляется после использования, а `SessionKeys` обнуляет свои ключи при drop. --- ## AEAD Шифр записей — **ChaCha20-Poly1305** (`crates/aura-crypto/src/aead.rs`). Есть два режима: - `AeadSession` — для **потоковых** транспортов (TCP, QUIC, поток рукопожатия): держит 256-битный ключ и 64-битный счётчик сообщений; nonce выводится из счётчика, который продвигается шаг в шаг на каждом `seal` и `open`, поэтому стороны остаются синхронными без передачи nonce. - `AeadKey` — для **датаграммного** (UDP) пути: nonce-счётчик передаётся аргументом на каждый вызов, потому что датаграммы могут теряться или переупорядочиваться, так что счётчик каждой записи несётся на проводе. Схема nonce идентична `AeadSession`, поэтому оба совместимы на одном ключе, пока их диапазоны счётчиков не пересекаются. ### Схема nonce 96-битный (12-байтный) nonce выводится из счётчика: ``` nonce[0..8] = counter как little-endian u64 nonce[8..12] = 0x00 00 00 00 ``` В потоковом режиме nonce никогда не переиспользуется в рамках сессии (переполнение счётчика 2^64 недостижимо; при переполнении происходит паника, а не повторное использование nonce). В датаграммном режиме за уникальность счётчика отвечает отправитель (`DatagramSender` монотонно увеличивает `seq`). Ключ обнуляется при drop. --- ## Записи данных и защита от повтора (replay) После рукопожатия прикладные `Frame` передаются как записи `Data` (`crates/aura-proto/src/session.rs`). Кодирование зависит от того, потоковый транспорт или датаграммный — но логика replay-окна общая. ### Потоковый путь (TCP / QUIC) **Полезная нагрузка** каждой записи `Data`: ``` seq (u64, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = header || seq ) ``` - `seq` — 8-байтный big-endian счётчик записи. На «счастливом пути» он равен счётчику запечатывающего AEAD (и ожидаемому счётчику AEAD приёмника). - **AAD** AEAD — это 5-байтный заголовок кадра `header`, сцепленный с 8-байтным `seq`, так что запись криптографически привязана и к своей объявленной длине/типу, и к заявленной позиции. - Шифртекст включает 16-байтный тег Poly1305. Полная запись на проводе: ``` [ header(5) ][ seq(8) ][ ciphertext + tag ] \_____________________________________________/ header.length = 8 + len(ciphertext+tag) ``` ### Датаграммный путь (свой UDP) Здесь нет 5-байтного потокового заголовка внутри записи. Датаграммная запись (`DatagramSender::seal`): ``` seq(8, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = seq ) ``` То есть AAD — это **только** `seq` (а не `header || seq`). На проводе эта запись несётся внутри DATA-датаграммы UDP-транспорта как `0x02 || rec_len(u16 BE) || запись [|| паддинг]` (см. раздел про транспорт v2 выше). ### Скользящее replay-окно Приёмник запускает проверку повтора **скользящим окном шириной 64** (`REPLAY_WINDOW = 64`) *до* обращения к AEAD, так что дубликат или слишком старая запись отвергаются с `Replay(seq)`, не трогая счётчик AEAD (сессия остаётся работоспособной). Окно: - отслеживает наибольший принятый `seq` плюс 64-битную битовую карту принятых позиций ниже него; - принимает `seq`, только если он строго новее всего виденного, либо попадает в окно и ранее не был виден; - отвергает `seq`, который равен текущему наибольшему, уже отмечен в битовой карте или находится более чем на `REPLAY_WINDOW` ниже наибольшего. Окно инициализируется на пост-рукопожатном счётчике (`start = 2`): всё строго ниже `start` считается уже потреблённым, поэтому первая легитимная запись Data (`seq == 2`) принимается как «новейшая». Этот механизм работает одинаково на потоковом и датаграммном путях. ### Полнодуплексное разделение `Session` можно `split()` на независимые половины `SessionSender` (писатель + исходящий AEAD + счётчик отправки) и `SessionReceiver` (читатель + входящий AEAD + replay-окно), которыми можно управлять из разных задач для конкурентного чтения/записи (например, в VPN-туннеле). `recv_frame` **не** безопасен к отмене (cancellation-safe) и должен выполняться из одной владеющей задачи. Для датаграммного пути аналогично есть `into_datagram_parts`, выдающий `DatagramSender` / `DatagramReceiver`. --- ## Слой мимикрии Внешний слой QUIC/TLS (`crates/aura-transport/`) существует исключительно для маскировки соединения под трафик браузера HTTP/3. Он явно **не** является границей аутентификации. - **ALPN** объявляет `h3` и `h3-29` (`ALPN_H3`) — ровно то, что предлагает Chrome для HTTP/3 — поэтому расширение ALPN неотличимо от реального браузерного. - **Параметры транспорта** зеркалят соединение Chromium HTTP/3: ~30 с idle-таймаут, ~15 с keep-alive, 100 конкурентных bidi/uni-потоков, ~10 МБ окна управления потоком на приём (`chrome_quic_transport_config`). - **SNI** по умолчанию — обобщённое CDN-подобное имя (`cdn.example.com`), если вызывающий его не задал; развёртывания передают своё камуфляжное имя. - QUIC-**клиент принимает любой серверный сертификат** (`AcceptAnyServerCert` — все методы верификатора возвращают успех). Это безопасно *только* потому, что внешний TLS не является аутентификацией: настоящая взаимная аутентификация — это внутреннее рукопожатие Aura. Внешний TLS сервера также отключает клиентскую аутентификацию (`with_no_client_auth`). > Не переиспользуйте `AcceptAnyServerCert` нигде, где слой TLS *является* границей аутентификации. Лёгкая HTTP-маскировка TCP-транспорта (`TcpOpts::masquerade`) преследует ту же цель, но гораздо скромнее (см. раздел про транспорт v2): это преамбула HTTP/1.1, а не TLS. --- ## Модель ошибок Слой протокола выдаёт `ProtoError` (`crates/aura-proto/src/lib.rs`), в том числе: `Io`, `Crypto`, `Pki`, `UnknownMsgType`, `BadVersion`, `FrameTooLarge`, `UnexpectedMsg`, `MalformedHandshake`, `MalformedFrame`, `Signature`, `FinishedMismatch`, `Replay` и `Alert`. Пир может отправить фатальный кадр `Alert` (тип `0xFF`); первый байт полезной нагрузки — код alert, который всплывает на локальной стороне как `ProtoError::Alert(code)`.