Files
AuraVPN/docs/protocol.md
T
xah30 083c441e4c docs: rewrite all documentation in Russian + add deployment guide
- docs/protocol.md, pki.md, split-tunnel.md, sing-box.md переведены на русский
  и сверены с текущим кодом (транспорт v2: свой UDP + TCP/443 + QUIC fallback,
  handover; PKI; split-tunnel; sing-box-план).
- docs/deployment.md (новый, 369 строк): пошаговое руководство для удалённого
  сервера — сборка, PKI init/issue-server/issue-client (проверено бинарём),
  server.toml/client.toml на основе фактических config/*.example, firewall +
  NAT/IP-форвардинг, sudo-запуск, бандл клиента (ca.crt + client.crt + client.key
  + server addr/sni), на каком транспорте идёт трафик, ограничения v1.
- README.md (новый, корень): краткий обзор + таблица крейтов + быстрый старт.

Всё на русском (проза); команды/идентификаторы/конфиги — как есть.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:42:08 +03:00

37 KiB
Raw Blame History

Протокол 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<dyn aura_proto::PacketConnection>, поэтому маршрутизатору туннеля безразлично, какой транспорт перенёс соединение (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_portquic_port). TCP может занимать тот же номер порта, что и UDP-транспорт (это другой протокол). См. docs/deployment.md.

Свой транспорт поверх UDP (основной путь)

Один сокет tokio::net::UdpSocket несёт обе фазы, различаемые по первому байту-типу (crates/aura-transport/src/udp.rs):

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 теперь открыт в обе стороны --
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<br/>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).