diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9056c0 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Aura VPN + +Aura — гибридный пост-квантовый VPN на Rust. Внутреннее рукопожатие гибридное и взаимно +аутентифицированное (X25519 + ML-KEM-768 по FIPS 203 со взаимной X.509-проверкой), данные +шифруются ChaCha20-Poly1305 с explicit-nonce, обфускация — паддинг датаграмм под «корзины» +HTTPS-размеров. + +На проводе по умолчанию идёт **собственный UDP-транспорт Aura** (без QUIC и без внешнего TLS на +основном пути). Если сеть режет UDP, клиент автоматически переключается на **TCP/443** или **QUIC** +(мимикрия HTTP/3), последовательно пробуя транспорты из настраиваемого `[transport] order`. На +стороне клиента есть TUN-интерфейс и split-tunnel (longest-prefix matching по CIDR + правила по +доменам), которым можно управлять на лету через admin-сокет. + +## Крейты + +| Крейт | Что внутри | +|------------------|----------------------------------------------------------------------------| +| `aura-crypto` | Гибридный KEM (X25519 + ML-KEM-768), HKDF, AEAD ChaCha20-Poly1305, helpers | +| `aura-pki` | Самоподписанный CA, выпуск server/client-сертификатов, проверка, плоский CRL | +| `aura-proto` | Рукопожатие Aura, фрейминг, датаграмный/потоковый кодек данных | +| `aura-transport` | Транспорты: собственный UDP, TCP/443, QUIC; единый dialer с handover | +| `aura-tunnel` | TUN, маршрутизатор, split-tunnel (CIDR + домены), DNS-резолв в host-маршруты | +| `aura-cli` | Бинарь `aura`: `pki`, `server`, `client`, `route`, `status`, `bench-crypto` | + +## Быстрый старт + +Подъём сервера на удалённой машине и подключение клиента описаны в +[`docs/deployment.md`](docs/deployment.md). Это основная точка входа для развёртывания. + +## Документация + +- [`docs/deployment.md`](docs/deployment.md) — руководство по развёртыванию (сервер + клиент) +- [`docs/protocol.md`](docs/protocol.md) — wire-протокол: рукопожатие, кадры, выбор транспорта +- [`docs/pki.md`](docs/pki.md) — модель PKI, команды `aura pki`, верификация и CRL +- [`docs/split-tunnel.md`](docs/split-tunnel.md) — split-tunnel, статика и admin-сокет на лету +- [`docs/sing-box.md`](docs/sing-box.md) — план интеграции с sing-box (для мобильных клиентов) diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..652fbe3 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,369 @@ +# Развёртывание Aura VPN + +Этот документ — пошаговое руководство, по которому вы поднимаете сервер Aura на удалённой машине, +провижините на нём сертификат для клиента и подключаете клиент (десктоп) к этому серверу. Все +команды и поля конфигов взяты из фактического кода и поставляемых примеров в `config/`. + +> Полезные сопутствующие документы: [`protocol.md`](protocol.md) (wire-протокол), +> [`pki.md`](pki.md) (CA и сертификаты), [`split-tunnel.md`](split-tunnel.md) (правила +> маршрутизации), [`sing-box.md`](sing-box.md) (интеграция с sing-box, план). + +--- + +## 1. Обзор схемы + +Сервер Aura на удалённой машине провижинит сертификат для клиента (десктопа или, в будущем, +телефона через sing-box), отдаёт клиенту бандл сертификатов и трастового якоря, и клиент +подключается к серверу по протоколу AuraVPN. + +На проводе по умолчанию используется **собственный UDP-транспорт Aura с пост-квантовой +криптографией** (без QUIC и без внешнего TLS на основном пути); fallback'и — это TCP/443 и QUIC. +Всё рукопожатие пост-квантовое: **гибридное X25519 + ML-KEM-768** с взаимной X.509-аутентификацией. +Для данных используется AEAD **ChaCha20-Poly1305** с explicit-nonce. Обфускация — это паддинг +датаграмм до «корзин» размера, характерных для HTTPS. + +``` + [клиент-десктоп] [удалённый сервер aura] + client.toml + PEM-бандл server.toml + PKI (CA + server leaf) + | | + | UDP (основной) / TCP/443 / QUIC | + | гибридное PQ-рукопожатие | + | ChaCha20-Poly1305 | + +---------------------------------------> + AuraVPN +``` + +--- + +## 2. Сервер (удалённый хост) + +### 2.1. Установка бинаря + +В корне репозитория: + +```bash +cargo build --release +# -> target/release/aura +``` + +Скопируйте получившийся бинарь `target/release/aura` на сервер (например в `/usr/local/bin/aura`) +либо соберите его прямо на сервере (требуется Rust toolchain). + +### 2.2. Поднять PKI + +Эти три команды создают CA и выпускают листовые сертификаты для сервера и клиента. Все они +проверены против реализации в `crates/aura-pki/src/{ca,cert,store}.rs` и +`crates/aura-cli/src/pki.rs`. + +```bash +# 1) Создать CA Aura. +aura pki init --ca-name "Aura Root CA" --out /etc/aura/pki +# -> /etc/aura/pki/ca.crt +# -> /etc/aura/pki/ca.key # секрет, защищайте правами файловой системы + +# 2) Выпустить сертификат сервера. --domain должен совпадать с тем именем, +# которое клиент будет ожидать в [client] sni (это же имя проверяется по SAN). +aura pki issue-server \ + --domain vpn.example.com \ + --out /etc/aura/pki/server \ + --ca /etc/aura/pki +# -> /etc/aura/pki/server/server.crt +# -> /etc/aura/pki/server/server.key # секрет + +# 3) Выпустить сертификат клиента (по одному на устройство). +# --id становится Common Name'ом и проверенным peer_id, который видит сервер. +aura pki issue-client \ + --id phone-1 \ + --out /etc/aura/clients/phone-1 \ + --ca /etc/aura/pki +# -> /etc/aura/clients/phone-1/client.crt +# -> /etc/aura/clients/phone-1/client.key # секрет +``` + +Подробности (включая `aura pki revoke` / `list`) — см. [`pki.md`](pki.md). + +### 2.3. `server.toml` + +Раскладка ниже взята из `config/server.toml.example` и поставляемых serde-структур +(`crates/aura-cli/src/config.rs`). Скопируйте пример и поправьте под себя. + +```toml +[server] +# Человекочитаемое имя (также внутренняя identity сервера в рукопожатии). +name = "aura-edge-1" +# UDP/TCP listen-сокет. ":443" мимикрирует под HTTPS; для его биндинга нужны привилегии. +# IP отсюда переиспользуется как listen-IP для каждого включённого транспорта. +listen = "0.0.0.0:443" +# Число accept-воркеров (в v1 носит совещательный характер). +workers = 4 + +[pki] +# Trust anchor (Aura CA) и листовая пара сервера, все PEM. +ca_cert = "/etc/aura/pki/ca.crt" +cert = "/etc/aura/pki/server/server.crt" +key = "/etc/aura/pki/server/server.key" + +[tunnel] +# Адресный пул для клиентов; в v1 на сервере один общий TUN в этой сети. +pool_cidr = "10.7.0.0/24" +# MTU TUN-устройства (запас под QUIC + framing Aura). +mtu = 1420 +# DNS, анонсируемый клиентам (в v1 информационно). +dns = "10.7.0.1" + +[mimicry] +# Hostname, под который мимикрирует внешний TLS-слой (для QUIC). +sni = "cdn.example.com" +# Паддинг для размытия размеров пакетов под «корзины» HTTPS. +padding = true + +[transport] +# Набор и порядок транспортов, биндящихся одновременно. UDP — основной; TCP/443 и +# QUIC (мимикрия H3) — fallback'и. При отсутствии всей секции включаются udp/tcp/quic +# на 443/443/444. +order = ["udp", "tcp", "quic"] +# UDP-транспорт и QUIC оба используют UDP, поэтому udp_port и quic_port ДОЛЖНЫ +# различаться. TCP может занимать тот же номер порта (другой протокол). +udp_port = 443 +tcp_port = 443 +quic_port = 444 +# UDP: дополнять датаграммы до «корзин» размера HTTPS, чтобы размыть распределение размеров. +obfuscate = true +# TCP: добавлять минимальный HTTP/1.1-преамбулу (Host = [mimicry] sni), чтобы открытие +# выглядело как обычный HTTP. +masquerade = true +``` + +Пути могут начинаться с `~` (раскрывается в домашнюю директорию). + +### 2.4. Сеть на сервере + +#### Файрвол + +Откройте те порты, которые перечислены у вас в `[transport]`. С приведённой выше конфигурацией: + +- UDP **443** — основной транспорт Aura. +- TCP **443** — fallback Aura поверх TCP. +- UDP **444** — fallback Aura поверх QUIC. + +Важно: UDP-транспорт и QUIC — это **оба UDP**, поэтому их порты обязательно должны различаться +(в примере: udp_port=443, quic_port=444). Конфиг-валидатор `transport.modes()` отвергает совпадение. + +#### IP-форвардинг и NAT (для выхода клиентов в интернет) + +В v1 настройка egress на стороне сервера — **обязательный ручной шаг**. На Linux: + +```bash +# 1) Включить IP-форвардинг. +sudo sysctl -w net.ipv4.ip_forward=1 +# (для постоянства добавьте в /etc/sysctl.conf или /etc/sysctl.d/*) + +# 2) MASQUERADE для исходящего трафика клиентов на интернет-интерфейсе (например eth0). +sudo iptables -t nat -A POSTROUTING \ + -s 10.7.0.0/24 \ + -o eth0 \ + -j MASQUERADE +``` + +Подставьте свой `pool_cidr` и имя интернет-интерфейса. + +### 2.5. Запуск сервера + +```bash +sudo aura server --config /etc/aura/server.toml +``` + +`sudo` нужен для создания TUN-устройства и для биндинга привилегированных портов (`:443`). + +Можно опционально указать путь admin-сокета: + +```bash +sudo aura server \ + --config /etc/aura/server.toml \ + --admin-socket /var/run/aura-admin.sock +``` + +По умолчанию admin-сокет — `/tmp/aura-admin.sock`. + +--- + +## 3. Что вы получаете для клиента (бандл) + +Отдайте клиенту **три PEM-файла**: + +- `ca.crt` (из `/etc/aura/pki/ca.crt`) — trust anchor; +- `client.crt` (из `/etc/aura/clients//client.crt`) — листовой сертификат клиента; +- `client.key` (из `/etc/aura/clients//client.key`) — **секрет**, приватный ключ клиента. + +И сообщите ему два параметра: + +- **Адрес сервера** (например `203.0.113.10`). +- **`sni`** — то DNS-имя, которое вы указали в `aura pki issue-server --domain`. Оно же + ожидается в SAN серверного сертификата и проверяется в `verify_server_cert`. + +Эти три файла плюс два параметра — это всё, что нужно клиенту для подключения. + +--- + +## 4. Клиент (десктоп) + +Путь для телефона — через sing-box; пока нативного клиента нет, см. раздел 6 ниже. + +### 4.1. `client.toml` + +Раскладка взята из `config/client.toml.example` и `crates/aura-cli/src/config.rs`. + +```toml +[client] +# Человекочитаемое имя/id клиента. +name = "laptop" +# UDP-сокет сервера. IP отсюда переиспользуется как server-IP для каждого транспорта. +server_addr = "203.0.113.10:443" +# Внешний TLS-SNI (hostname-камуфляж), предъявляемый серверу. Он же проверяется +# внутри рукопожатия Aura против SAN серверного сертификата. +sni = "cdn.example.com" + +[pki] +# Trust anchor (Aura CA) и листовая пара клиента, все PEM. +ca_cert = "~/.aura/ca.crt" +cert = "~/.aura/client.crt" +key = "~/.aura/client.key" + +[tunnel] +# Запрошенное имя TUN-интерфейса (на macOS совещательно — ядро назначает utunN). +tun_name = "aura0" +# Локальный адрес для TUN и длина префикса. +local_ip = "10.7.0.2" +prefix = 24 +# MTU TUN. +mtu = 1420 +# DNS, используемый туннельным резолвером (в v1 информационно; реально используется +# системный резолвер). +dns = "10.7.0.1" + +# Split-tunnel: действие по умолчанию плюс точечные правила. +[tunnel.split] +default = "VPN" + +[[tunnel.split.direct]] +cidr = "192.168.0.0/16" + +[[tunnel.split.direct]] +cidr = "10.0.0.0/8" + +[[tunnel.split.direct]] +domain = "intranet.example.com" + +# Более узкий префикс возвращает поддиапазон обратно в VPN (longest-prefix бьёт /8). +[[tunnel.split.vpn]] +cidr = "10.7.0.0/24" + +[mimicry] +padding = false + +[transport] +# Порядок fallback'а (handover), пробуется слева направо: первый удавшийся побеждает. +# При отсутствии всей секции — ["udp","tcp","quic"] на 443/443/444. +order = ["udp", "tcp", "quic"] +udp_port = 443 +tcp_port = 443 +quic_port = 444 +obfuscate = true +masquerade = true +``` + +Подробности про `[tunnel.split]` — в [`split-tunnel.md`](split-tunnel.md). + +### 4.2. Запуск клиента + +```bash +sudo aura client --config client.toml +``` + +`sudo` нужен для поднятия TUN-устройства. Клиент: + +1. Загружает PEM-файлы из `[pki]` и строит `aura_proto::ClientConfig`. +2. Строит таблицу маршрутизации из `[tunnel.split]`. +3. Дозванивается до сервера, перебирая транспорты в `[transport] order` + (handover UDP → TCP → QUIC); первый, который удался, побеждает. +4. Разрезолвит доменные правила split-tunnel'а в host-маршруты (best-effort). +5. Создаёт TUN, передаёт его маршрутизатору и начинает гонять трафик. + +В логе при успехе вы увидите строку с выбранным транспортом: + +``` +INFO connected and authenticated to server peer=Some("cdn.example.com") mode=udp +``` + +`mode` принимает значения `udp`, `tcp` или `quic`. + +### 4.3. Управление на лету + +После запуска клиента (или сервера) admin-сокет позволяет менять правила и смотреть статус без +перезапуска: + +```bash +# Добавить CIDR на лету. +aura route add --cidr 8.8.8.0/24 --action direct + +# Завернуть домен через VPN. +aura route add --domain example.com --action vpn + +# Перечислить правила. +aura route list + +# Удалить CIDR-правило. +aura route remove --cidr 8.8.8.0/24 + +# Статус и счётчики. +aura status +# Aura tunnel status +# peer: cdn.example.com +# default: vpn +# rules: 2 +# rx packets: 0 +# tx packets: 0 +``` + +Если сокет лежит не там, добавьте `--admin-socket ` к каждой команде. Полная спецификация +команд и wire-протокола admin'а — в [`split-tunnel.md`](split-tunnel.md). + +--- + +## 5. Что идёт по проводу (резюме) + +- **Основной**: собственный UDP-транспорт Aura (в примере — `443/udp`). Один UDP-сокет несёт + обе фазы, различимые по первому байту: + - `0x01` HS — рукопожатие с надёжным DTLS-подобным слоем поверх (повторы, ack, упорядочивание); + - `0x02` DATA — датаграммы данных с explicit-nonce AEAD; обфускация = паддинг до «корзин» + HTTPS (`[64, 128, 256, 512, 1024, 1280, 1460]`). +- **Fallback TCP/443**: то же рукопожатие поверх TCP. Опциональная HTTP/1.1-преамбула как лёгкая + маскировка (`masquerade = true`). +- **Fallback QUIC**: внешний TLS-камуфляж под HTTP/3 + внутреннее Aura-рукопожатие. +- Клиент пробует транспорты по `order`, переключается при отказе или таймауте подключения + (по умолчанию 8 с). Сервер слушает все включённые транспорты одновременно (`MultiServer`). + +Подробный wire-протокол — в [`protocol.md`](protocol.md). + +--- + +## 6. Честные ограничения v1 + +- **TUN требует root.** И клиент, и сервер создают TUN; на macOS имя интерфейса (`utunN`) + назначается системой. +- **NAT/IP-форвардинг на сервере настраивается вручную.** Бинарь Aura сам ничего не правит в + netfilter/sysctl. +- **Сервер v1 — это один общий TUN.** UDP-транспорт обслуживает **одного пира на `accept`**: первый + HS-датаграмма фиксирует адрес источника, и дальнейшая сессия привязывается к нему. Для нескольких + клиентов одновременно лучше использовать TCP или QUIC (`MultiServer` принимает соединения из + любого включённого транспорта). +- **`send_direct` — заглушка.** Пакеты с действием `DIRECT` пишутся в trace-лог и отбрасываются; + настоящего raw-socket egress в v1 нет (см. [`split-tunnel.md`](split-tunnel.md)). +- **TCP-маскировка лёгкая.** Это минимальная HTTP/1.1-преамбула, а не полноценный TLS-443 + (полноценный план — в дорожной карте). +- **Нативного Go-модуля sing-box ещё нет.** В качестве моста — Option A из + [`sing-box.md`](sing-box.md) (process bridge через `aura client`). Нативный Go-outbound + (Option B) — план. +- **CRL не подписан** (плоское множество идентификаторов на диске) и распространяется вне + протокола; автоматической ротации листов нет (срок действия 365 дней, см. [`pki.md`](pki.md)). +- **Admin-сокет — только под Unix.** На Windows — `cfg`-заглушка. diff --git a/docs/pki.md b/docs/pki.md index e99252c..2f78e25 100644 --- a/docs/pki.md +++ b/docs/pki.md @@ -1,20 +1,20 @@ -# Aura PKI +# PKI Aura -Aura uses a small, self-contained X.509 PKI for **mutual authentication** of the inner -handshake. A single self-signed Aura **CA** issues one **server** certificate and one -**client** certificate per client. During the handshake the client verifies the server's -certificate and the server verifies the client's certificate, both against the CA. +Aura использует небольшую самодостаточную X.509-PKI для **взаимной аутентификации** во внутреннем +рукопожатии. Один самоподписанный **CA** Aura выдаёт один сертификат для **сервера** и по одному +сертификату для каждого **клиента**. Во время рукопожатия клиент проверяет сертификат сервера, а +сервер — сертификат клиента, и в обе стороны проверка идёт против этого CA. -The PKI is implemented in the `aura-pki` crate (`ca.rs`, `cert.rs`, `store.rs`) and exposed on -the command line as `aura pki ...` (`crates/aura-cli/src/pki.rs`, -`crates/aura-cli/src/main.rs`). +PKI реализована в крейте `aura-pki` (`ca.rs`, `cert.rs`, `store.rs`) и доступна в командной строке +как `aura pki ...` (`crates/aura-cli/src/pki.rs`, `crates/aura-cli/src/main.rs`). -> The outer QUIC/TLS layer does **not** use this PKI — it accepts any certificate (see -> `protocol.md`, "Mimicry layer"). All certificate trust lives in the inner Aura handshake. +> Внешний QUIC/TLS-слой эту PKI **не** использует — он принимает любой сертификат (см. +> `protocol.md`, раздел про слой мимикрии). Всё доверие сертификатам сосредоточено во внутреннем +> рукопожатии Aura. --- -## Trust model +## Модель доверия ``` Aura CA (self-signed) @@ -24,55 +24,54 @@ the command line as `aura pki ...` (`crates/aura-cli/src/pki.rs`, | | server leaf client leaf(s) CN = CN = - SAN: DNS: (no SAN) + SAN: DNS: (нет SAN) EKU: serverAuth EKU: clientAuth ``` -- The **CA** is self-signed with `BasicConstraints: CA`, and key usages - `keyCertSign` + `crlSign` + `digitalSignature`. Default lifetime **3650 days**. -- A **server leaf** carries `CN = `, a **`DNS:` SAN**, and - `extendedKeyUsage = serverAuth`. The DNS SAN is what the client matches against its expected - `server_name`. -- A **client leaf** carries `CN = ` and `extendedKeyUsage = clientAuth`. The CN is - the identity the server learns and records as the session `peer_id`. -- Leaf key usages are `digitalSignature` + `keyEncipherment`. Default lifetime **365 days**. -- All issued certs (CA and leaves) backdate `not_before` by **5 minutes** to tolerate clock - skew. +- **CA** самоподписан, имеет `BasicConstraints: CA` (unconstrained) и `keyUsage`: + `keyCertSign` + `crlSign` + `digitalSignature`. Срок действия по умолчанию — **3650 дней**. +- **Server leaf** несёт `CN = `, **`DNS:` SAN** и + `extendedKeyUsage = serverAuth`. Именно DNS-SAN сравнивается клиентом с ожидаемым `server_name`. +- **Client leaf** несёт `CN = ` и `extendedKeyUsage = clientAuth`. Этот CN — та + идентичность, которую увидит сервер и запишет как `peer_id` сессии. +- `keyUsage` для листовых сертификатов: `digitalSignature` + `keyEncipherment`. Срок действия по + умолчанию — **365 дней**. +- У всех выпускаемых сертификатов (и у CA, и у листовых) `not_before` смещён назад на **5 минут**, + чтобы выдерживать небольшой расхождение часов. -### Algorithms +### Алгоритмы -All keys are **ECDSA P-256 / SHA-256** (rcgen's default `KeyPair::generate`). Private keys are -written in **PKCS#8 PEM**. Chain verification (in `cert.rs`) accepts ECDSA P-256/SHA-256 -(required), and also ECDSA P-384/SHA-384 and Ed25519, so a deployment can switch key types -later without code changes. +Все ключи — **ECDSA P-256 / SHA-256** (`KeyPair::generate` rcgen по умолчанию). Приватные ключи +сохраняются в **PKCS#8 PEM**. Проверка цепочки (`cert.rs`) принимает ECDSA P-256/SHA-256 +(обязательно), а также ECDSA P-384/SHA-384 и Ed25519, — так что развёртывание сможет позже сменить +тип ключа без изменения кода. --- -## File layout +## Раскладка файлов -The CLI keeps files in plain directories. Conventional names -(`crates/aura-cli/src/pki.rs`): +CLI хранит файлы в обычных директориях. Стандартные имена (`crates/aura-cli/src/pki.rs`): -| File | Constant | Contents | -|---------------|------------|-------------------------------------------| -| `ca.crt` | `CA_CERT` | CA certificate (PEM) | -| `ca.key` | `CA_KEY` | CA private key (PKCS#8 PEM) — **secret** | -| `server.crt` | | Server leaf certificate (PEM) | -| `server.key` | | Server leaf private key (PEM) — **secret**| -| `client.crt` | | Client leaf certificate (PEM) | -| `client.key` | | Client leaf private key (PEM) — **secret**| -| `revoked.crl` | `CRL_FILE` | Revocation list (one identifier per line) | +| Файл | Константа | Содержимое | +|---------------|------------|--------------------------------------------------| +| `ca.crt` | `CA_CERT` | Сертификат CA (PEM) | +| `ca.key` | `CA_KEY` | Приватный ключ CA (PKCS#8 PEM) — **секрет** | +| `server.crt` | | Листовой сертификат сервера (PEM) | +| `server.key` | | Приватный ключ сервера (PEM) — **секрет** | +| `client.crt` | | Листовой сертификат клиента (PEM) | +| `client.key` | | Приватный ключ клиента (PEM) — **секрет** | +| `revoked.crl` | `CRL_FILE` | Список отозванных идентификаторов (по одному в строке) | -`issue-server` and `issue-client` load the CA from `ca.crt` + `ca.key` in the CA directory and -write `server.{crt,key}` / `client.{crt,key}` into the output directory. Paths beginning with -`~` are expanded to the home directory (from `$HOME`, or `$USERPROFILE` on Windows). +Команды `issue-server` и `issue-client` загружают CA из `ca.crt` + `ca.key` в директории CA и +записывают `server.{crt,key}` / `client.{crt,key}` в выходную директорию. Пути, начинающиеся с +`~`, раскрываются в домашнюю директорию (из `$HOME`, либо `$USERPROFILE` на Windows). -These names map directly onto the `[pki]` section of `server.toml` / `client.toml` +Эти имена напрямую соответствуют секции `[pki]` в `server.toml` / `client.toml` (`ca_cert`, `cert`, `key`). --- -## `aura pki` commands +## Команды `aura pki` ``` aura pki init --ca-name --out @@ -82,14 +81,14 @@ aura pki revoke --id [--crl ] aura pki list [--crl ] ``` -For `issue-server` / `issue-client`, `--ca` defaults to the value of `--out` (so the CA and -the issued leaf can live in the same directory). For `revoke` / `list`, `--crl` defaults to -`./revoked.crl`. +Для `issue-server` / `issue-client` параметр `--ca` по умолчанию равен значению `--out` (так что CA +и выпущенный лист могут лежать в одной директории). Для `revoke` / `list` параметр `--crl` по +умолчанию равен `./revoked.crl`. -### `init` — create a CA +### `init` — создать CA -Generates a fresh self-signed CA and writes `ca.crt` + `ca.key` into `--out` (creating the -directory if needed). +Генерирует свежий самоподписанный CA и записывает `ca.crt` + `ca.key` в `--out` (директория +создаётся при необходимости). ```bash aura pki init --ca-name "Aura Root CA" --out ~/.aura @@ -98,10 +97,10 @@ aura pki init --ca-name "Aura Root CA" --out ~/.aura # key: ~/.aura/ca.key ``` -### `issue-server` — issue a server certificate +### `issue-server` — выпустить сертификат сервера -Issues a server leaf for a DNS name, signed by the CA, with a `DNS:` SAN and -`serverAuth` EKU. +Выпускает листовой сертификат для DNS-имени, подписанный CA, с SAN `DNS:` и EKU +`serverAuth`. ```bash aura pki issue-server --domain vpn.example.com --out ~/.aura --ca ~/.aura @@ -110,14 +109,14 @@ aura pki issue-server --domain vpn.example.com --out ~/.aura --ca ~/.aura # key: ~/.aura/server.key ``` -> The `--domain` must equal the name the client expects in the handshake. In the shipped -> client config that name is taken from `[client] sni`, so the camouflage SNI and the -> verified server SAN are the same value. +> Значение `--domain` должно совпадать с тем именем, которое клиент ожидает в рукопожатии. В +> поставляемой конфигурации клиента это имя берётся из `[client] sni`, поэтому SNI камуфляжа и +> проверяемый SAN сервера — это одно и то же значение. -### `issue-client` — issue a client certificate +### `issue-client` — выпустить сертификат клиента -Issues a client leaf with `CN = ` and `clientAuth` EKU. The `` becomes the verified -`peer_id` the server sees. +Выпускает листовой сертификат с `CN = ` и EKU `clientAuth`. Это `` станет проверенным +`peer_id`, который увидит сервер. ```bash aura pki issue-client --id laptop --out ~/.aura --ca ~/.aura @@ -126,19 +125,20 @@ aura pki issue-client --id laptop --out ~/.aura --ca ~/.aura # key: ~/.aura/client.key ``` -### `revoke` — add to the revocation list +### `revoke` — добавить в список отзыва -Adds an identifier — a **client id / Common Name** or a **certificate serial** (lowercase -hex, no separators) — to the CRL file, creating it (and parent directories) if absent. +Добавляет идентификатор — это либо **client id / Common Name**, либо **серийный номер +сертификата** (строчные шестнадцатеричные цифры без разделителей) — в CRL-файл, создавая его (и +родительские директории) при отсутствии. ```bash aura pki revoke --id laptop --crl ~/.aura/revoked.crl # revoked 'laptop' (CRL: ~/.aura/revoked.crl) ``` -### `list` — show revoked identifiers +### `list` — показать отозванные идентификаторы -Prints the identifiers in the CRL file (empty if the file does not exist). +Печатает идентификаторы из CRL-файла (пусто, если файла нет). ```bash aura pki list --crl ~/.aura/revoked.crl @@ -146,87 +146,88 @@ aura pki list --crl ~/.aura/revoked.crl # laptop ``` -### End-to-end example +### Полный пример ```bash -# 1. Create the CA. +# 1. Создать CA. aura pki init --ca-name "Aura Root CA" --out ~/.aura -# 2. Issue the server cert for its public DNS name. +# 2. Выпустить серверный сертификат для публичного DNS-имени. aura pki issue-server --domain vpn.example.com --out ~/.aura -# 3. Issue a client cert per device. +# 3. Выпустить клиентский сертификат — по одному на устройство. aura pki issue-client --id laptop --out ~/.aura -# 4. (later) Revoke a compromised client. +# 4. (позже) Отозвать скомпрометированный клиент. aura pki revoke --id laptop ``` --- -## Verification +## Проверка -Verification is performed by `AuraCertVerifier` (`crates/aura-pki/src/cert.rs`), built from -the CA certificate PEM. It uses **`rustls-webpki`** to validate the peer's leaf against the CA -trust anchor. The Aura handshake invokes it on each side (see `protocol.md`). +Проверку выполняет `AuraCertVerifier` (`crates/aura-pki/src/cert.rs`), собранный из PEM-сертификата +CA. Внутри он использует **`rustls-webpki`** для валидации листового сертификата пира против CA как +trust anchor. Рукопожатие Aura вызывает его с обеих сторон (см. `protocol.md`). -**Server certificate** (`verify_server_cert`), run by the client: +**Сертификат сервера** (`verify_server_cert`), запускает клиент: -1. webpki chain verification against the CA with key usage **`serverAuth`**, plus validity - (time) check. -2. The leaf must be valid for the requested `server_name` (DNS SAN match); a mismatch is +1. webpki-валидация цепочки против CA с key usage **`serverAuth`** плюс проверка срока действия + (времени). +2. Лист должен быть валиден для запрошенного `server_name` (совпадение DNS-SAN); расхождение — `NameMismatch`. -3. CRL check (see below). +3. Проверка по CRL (см. ниже). -**Client certificate** (`verify_client_cert`), run by the server: +**Сертификат клиента** (`verify_client_cert`), запускает сервер: -1. webpki chain verification against the CA with key usage **`clientAuth`**, plus validity. -2. The **client id** is extracted as the first Common Name from the leaf subject (missing CN - is `MissingIdentity`). -3. CRL check. -4. Returns the client id, which the handshake records as the session `peer_id`. +1. webpki-валидация цепочки против CA с key usage **`clientAuth`** плюс проверка срока действия. +2. **client id** извлекается как первый Common Name из subject листа (отсутствие CN — + `MissingIdentity`). +3. Проверка по CRL. +4. Возвращает client id, который рукопожатие фиксирует как `peer_id` сессии. -The leaf certificate is sent **inline** in the handshake (DER, no intermediate chain); the CA -is the single trust anchor. Possession of the leaf's private key is proven separately by the -handshake signature over the transcript (see `protocol.md`). +Листовой сертификат передаётся **inline** в рукопожатии (DER, без промежуточной цепочки); CA — это +единственный trust anchor. Владение приватным ключом листового сертификата доказывается отдельно +подписью рукопожатия по транскрипту (см. `protocol.md`). -Errors surface as `PkiError`: `CertParse`, `EmptyChain`, `TrustAnchor`, `Verification`, +Ошибки сообщаются как `PkiError`: `CertParse`, `EmptyChain`, `TrustAnchor`, `Verification`, `NameMismatch`, `MissingIdentity`, `Revoked`. --- -## Revocation (CRL) +## Отзыв (CRL) -Aura v1 revocation is deliberately minimal (`crates/aura-pki/src/store.rs`). `CrlStore` is a -**set of revoked identifier strings**, where an identifier is either: +Механизм отзыва в Aura v1 намеренно минимальный (`crates/aura-pki/src/store.rs`). `CrlStore` — это +**множество строк-идентификаторов отозванных сертификатов**, где идентификатор — это либо: -- a certificate **serial number** (lowercase hex, no separators), or -- a **client id / Common Name**. +- **серийный номер** сертификата (строчные hex-цифры без разделителей), либо +- **client id / Common Name**. -During verification, if the CRL is non-empty the leaf is rejected (`Revoked`) when **either** -its serial **or** its Common Name is present in the set. An empty CRL skips the check -entirely. +При проверке, если CRL непуст, листовой сертификат отвергается (`Revoked`), когда **либо** его +серийный номер, **либо** его Common Name присутствует в множестве. Пустой CRL пропускает проверку +полностью. -The on-disk format is one identifier per line; blank lines and `#` comments are ignored on -load. `aura pki revoke` / `aura pki list` manage this file. +Формат на диске — один идентификатор в строке; пустые строки и комментарии `#` игнорируются при +загрузке. Файл управляется командами `aura pki revoke` / `aura pki list`. -> v1 limitation: this is a flat allow/deny set, not a signed X.509 CRL. There is no CRL -> signature, no `nextUpdate`, and no automatic distribution — the file must be provisioned to -> the verifying side out of band. The verifier passes `None` for webpki's own revocation -> hooks and relies solely on this set. +> Ограничение v1: это плоское множество разрешения/запрета, а не подписанный X.509 CRL. Нет +> подписи CRL, нет `nextUpdate` и нет автоматического распространения — файл нужно доставить на +> проверяющую сторону вне протокола. Верификатор передаёт `None` в собственные крючки отзыва +> webpki и полагается исключительно на это множество. --- -## Security notes +## Замечания по безопасности -- **Protect the private keys.** `ca.key` is the root of all trust; anyone with it can mint - valid server/client certs. `server.key` / `client.key` must stay on their respective hosts. - The CLI writes them with default file permissions — restrict them at the OS level. -- **The CA is self-signed and unconstrained** (`BasicConstraints: CA` unconstrained). It is - the sole trust anchor; there is no intermediate CA tier in v1. -- **Server identity is name-bound.** The client only accepts a server leaf whose DNS SAN - matches the expected name, so a different valid leaf from the same CA will not be accepted - for the wrong host. -- **Revocation is best-effort** (see above): plan to distribute the CRL file and keep it in - sync on every server that verifies clients. -- **Leaf lifetime is 365 days**; plan re-issuance. There is no automated rotation in v1. +- **Защищайте приватные ключи.** `ca.key` — корень всего доверия; владея им, можно выпускать + любые валидные серверные/клиентские сертификаты. `server.key` / `client.key` должны оставаться + на своих хостах. CLI пишет их с дефолтными правами файловой системы — ограничивайте доступ + средствами ОС. +- **CA самоподписан и не ограничен** (`BasicConstraints: CA` unconstrained). Это единственный + trust anchor; в v1 нет уровня промежуточных CA. +- **Идентичность сервера связана с именем.** Клиент принимает только тот серверный лист, чей + DNS-SAN совпадает с ожидаемым именем, поэтому другой валидный лист от того же CA не будет + принят для чужого хоста. +- **Отзыв — best-effort** (см. выше): планируйте раздачу CRL-файла и поддерживайте его в актуальном + состоянии на каждом сервере, который проверяет клиентов. +- **Срок жизни листов — 365 дней**; планируйте перевыпуск. Автоматической ротации в v1 нет. diff --git a/docs/protocol.md b/docs/protocol.md index 99345b5..7e53c58 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -1,314 +1,436 @@ -# Aura Protocol +# Протокол Aura -The Aura protocol provides a mutually-authenticated, post-quantum-secure tunnel between a -client and a server. It is implemented in the `aura-proto` crate on top of `aura-crypto` -(hybrid KEM, HKDF, AEAD) and `aura-pki` (mutual X.509 verification). +Протокол Aura обеспечивает взаимно аутентифицированный, пост-квантово стойкий туннель между +клиентом и сервером. Он реализован в крейте `aura-proto` поверх `aura-crypto` (гибридный KEM, HKDF, +AEAD) и `aura-pki` (взаимная проверка X.509). -This document is for an engineer auditing or reimplementing the protocol. Everything below -reflects the **actual implementation**, not an idealized spec. Where the original spec was -ambiguous (notably the handshake message order), the implementation pins an exact choice and -that pinned choice is what is documented here. - -## Layering - -``` -+-------------------------------------------------------------+ -| Application IP packets (TUN) | -+-------------------------------------------------------------+ -| Aura inner session: Frame -> AEAD-sealed Data record | <- real security boundary -| Aura inner handshake: hybrid KEM + mutual X.509 | -+-------------------------------------------------------------+ -| Outer QUIC/TLS (quinn + rustls) — MIMICRY ONLY | <- NOT a security boundary -| ALPN h3 / h3-29, Chrome-like transport params, | -| client accepts ANY server cert | -+-------------------------------------------------------------+ -| UDP | -+-------------------------------------------------------------+ -``` - -The two layers have very different jobs: - -- **Outer QUIC/TLS** is camouflage. It is configured to look like ordinary browser HTTP/3 - traffic. It performs **no** meaningful authentication — see [Mimicry layer](#mimicry-layer). -- **Inner Aura handshake/session** is the real security boundary: hybrid post-quantum key - agreement plus mutual certificate verification against the Aura CA, then an AEAD-protected - record stream with replay protection. - -The inner protocol is transport-agnostic: `client_handshake` / `server_handshake` are generic -over a separate `tokio::io::AsyncRead` reader and `AsyncWrite` writer, so the same code drives -an in-memory duplex pipe (tests) and quinn's split `RecvStream` / `SendStream` (the QUIC -transport) identically. +Этот документ предназначен для инженера, который проводит аудит протокола или реализует его заново. +Всё, что описано ниже, отражает **фактическую реализацию**, а не идеализированную спецификацию. Там, +где исходная спецификация была неоднозначной (особенно порядок сообщений рукопожатия), реализация +фиксирует конкретный выбор — и именно этот зафиксированный выбор здесь задокументирован. --- -## Wire format +## Транспорт v2: свой UDP-канал, TCP/443 и QUIC как fallback -Every Aura protocol message is a **5-byte header** followed by a payload +Это ключевое изменение по сравнению с ранней версией: основной канал данных теперь — это **собственный +транспорт 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`): ``` -byte 0 : msg_type (u8) -bytes 1..4 : length (u24, big-endian) = payload length in bytes -byte 4 : version = 0x01 -bytes 5.. : payload (length bytes) +байт 0 : msg_type (u8) +байты 1..4 : length (u24, big-endian) = длина полезной нагрузки в байтах +байт 4 : version = 0x01 +байты 5.. : payload (length байт) ``` -- `length` is a 24-bit big-endian integer, so the maximum payload is `0x00FF_FFFF` - (16 MiB − 1). An oversize payload is rejected with `FrameTooLarge`. -- `version` is `0x01`. A header whose byte 4 is not `0x01` is rejected with `BadVersion`. +- `length` — это 24-битное целое big-endian, так что максимальная полезная нагрузка — + `0x00FF_FFFF` (16 МиБ − 1). Слишком большая нагрузка отвергается с `FrameTooLarge`. +- `version` равна `0x01`. Заголовок, у которого байт 4 не `0x01`, отвергается с `BadVersion`. -### Message types +### Типы сообщений -| Byte | `MsgType` | Direction | Encrypted | Role | -|--------|---------------|-----------|-----------|--------------------------------------------| -| `0x01` | `ClientHello` | C→S | no | Handshake 1: hybrid public key + nonce | -| `0x02` | `ServerHello` | S→C | no | Handshake 2: hybrid ciphertext + nonce | -| `0x03` | `ClientAuth` | C→S | yes | Handshake 4: client cert + signature | -| `0x04` | `ServerAuth` | S→C | yes | Handshake 3: server cert + signature | -| `0x05` | `Finished` | both | yes | Handshake 5/6: HMAC over the transcript | -| `0x06` | `Data` | both | yes | Application record (AEAD-sealed `Frame`) | -| `0xFF` | `Alert` | both | no | Fatal alert; payload byte 0 is the code | +| Байт | `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 нагрузки — код | -> Note: the numeric byte values do **not** follow the send order. `ServerAuth` (`0x04`) is -> sent *before* `ClientAuth` (`0x03`). The send order is fixed by the state machine -> (below), not by the type byte. +> Замечание: числовые значения байтов **не** соответствуют порядку отправки. `ServerAuth` (`0x04`) +> отправляется *перед* `ClientAuth` (`0x03`). Порядок отправки задаётся конечным автоматом (ниже), а +> не байтом-типом. -### Application frames +### Прикладные кадры -Once the session is established, the application payload carried inside each encrypted `Data` -record is a `Frame` (`crates/aura-proto/src/frame.rs`). All multi-byte integers are -big-endian: +Когда сессия установлена, прикладная нагрузка внутри каждой зашифрованной записи `Data` — это `Frame` +(`crates/aura-proto/src/frame.rs`). Все многобайтные целые — big-endian: -| Frame | Tag | Encoding | -|---------|--------|-----------------------------------------------------| -| `Data` | `0x01` | `0x01 \|\| stream_id(u32) \|\| payload` | -| `Ping` | `0x02` | `0x02 \|\| seq(u32)` | -| `Pong` | `0x03` | `0x03 \|\| seq(u32)` | +| Кадр | Тег | Кодирование | +|---------|--------|------------------------------------------------------| +| `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` всплывает как ошибка. + --- -## Handshake +## Рукопожатие -### Pinned message order +### Зафиксированный порядок сообщений -The original spec diagram was ambiguous about the order of the encrypted auth/Finished -messages. The implementation pins this exact order, and both peers follow it lock-step +Исходная диаграмма спецификации была неоднозначна насчёт порядка зашифрованных сообщений +auth/Finished. Реализация фиксирует ровно такой порядок, и обе стороны следуют ему шаг в шаг (`crates/aura-proto/src/handshake.rs`): ``` -1. C -> S ClientHello (plaintext): x25519_pub[32] || mlkem_ek[1184] || client_nonce[32] -2. S -> C ServerHello (plaintext): x25519_ephemeral[32] || mlkem_ct[1088] || server_nonce[32] - -- both sides derive the hybrid shared secret and the two directional SessionKeys -- -3. S -> C ServerAuth (encrypted under s2c): u16(cert_der_len) || server_leaf_cert_der || sig(transcript) -4. C -> S ClientAuth (encrypted under c2s): u16(cert_der_len) || client_leaf_cert_der || sig(transcript) -5. C -> S Finished (encrypted under c2s): HMAC-SHA256(key_c2s, transcript) -6. S -> C Finished (encrypted under s2c): HMAC-SHA256(key_s2c, transcript) - -- encrypted Data channel is now open in both directions -- +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 Client - participant S as Server - Note over C,S: plaintext + 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: both derive shared secret + SessionKeys
transcript = SHA-256(CH_frame || SH_frame) - Note over C,S: encrypted (AEAD under directional keys) - S->>C: 3. ServerAuth (server cert + sig over transcript) - C->>S: 4. ClientAuth (client cert + sig over transcript) - C->>S: 5. Finished (HMAC_c2s over transcript) - S->>C: 6. Finished (HMAC_s2c over transcript) - Note over C,S: session established; Data records flow both ways + 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 payloads (exact sizes) +### Полезные нагрузки Hello (точные размеры) -| Field | ClientHello | ServerHello | Bytes | -|-------------------|:-----------:|:-----------:|------:| -| X25519 pub / eph | ✔ | ✔ | 32 | -| ML-KEM-768 ek | ✔ | | 1184 | -| ML-KEM-768 ct | | ✔ | 1088 | -| nonce | ✔ | ✔ | 32 | -| **Total payload** | **1248** | **1152** | | +| Поле | ClientHello | ServerHello | Байт | +|-------------------|:-----------:|:-----------:|-----:| +| X25519 pub / eph | ✔ | ✔ | 32 | +| ML-KEM-768 ek | ✔ | | 1184 | +| ML-KEM-768 ct | | ✔ | 1088 | +| nonce | ✔ | ✔ | 32 | +| **Итого нагрузка**| **1248** | **1152** | | -Hellos are sent in plaintext and validated for exact length on receipt; a wrong length is -rejected with `MalformedHandshake`. +Hello отправляются в открытом виде и при приёме проверяются на точную длину; неверная длина +отвергается с `MalformedHandshake`. -### Transcript hash +### Transcript-хеш ``` transcript = SHA-256( ClientHello_frame_bytes || ServerHello_frame_bytes ) ``` -The hash covers the **full serialized frames** (5-byte header + payload) of ClientHello and -ServerHello, exactly as transmitted on the wire. This binds the negotiated key material and -the protocol version into both the signatures and the Finished MACs. +Хеш покрывает **полные сериализованные кадры** (5-байтный заголовок + полезная нагрузка) ClientHello +и ServerHello, ровно как они передаются на проводе. Это привязывает согласованный ключевой материал и +версию протокола одновременно к подписям и к MAC-ам Finished. -### Authentication (ServerAuth / ClientAuth) +### Аутентификация (ServerAuth / ClientAuth) -Each Auth payload is: +Каждая нагрузка Auth: ``` u16_be(cert_der_len) || leaf_cert_der || signature ``` -- `leaf_cert_der` is the sender's **leaf certificate** in DER (sent inline; no chain — the - CA is the trust anchor on the receiving side). -- `signature` is an **ECDSA P-256 / SHA-256** signature, ASN.1 DER encoded - (`ECDSA_P256_SHA256_ASN1`), computed over the 32-byte `transcript` (via `ring`). +- `leaf_cert_der` — это **листовой сертификат** отправителя в DER (передаётся встроенным, без цепочки — + CA является якорем доверия на принимающей стороне). +- `signature` — это подпись **ECDSA P-256 / SHA-256**, в кодировке ASN.1 DER + (`ECDSA_P256_SHA256_ASN1`), вычисленная по 32-байтному `transcript` (через `ring`). -Verification (`crates/aura-proto/src/handshake.rs`): +Проверка (`crates/aura-proto/src/handshake.rs`): -1. The receiver builds an `AuraCertVerifier` from its configured CA PEM and verifies the - peer's leaf against the CA (chain + key-usage + validity; see `pki.md`). - - The **client** additionally requires the server leaf to be valid for the expected - `server_name` (DNS SAN match). - - The **server** captures the verified **client id** (leaf Common Name) and stores it as - the session's `peer_id`. -2. The receiver extracts the leaf's EC public-key point and verifies `signature` over - `transcript`. A failure is `Signature(...)`. +1. Принимающая сторона строит `AuraCertVerifier` из настроенного PEM своего CA и проверяет листовой + сертификат пира против CA (цепочка + назначение ключа + срок действия; см. `pki.md`). + - **Клиент** дополнительно требует, чтобы листовой сертификат сервера был валиден для ожидаемого + `server_name` (совпадение DNS SAN). + - **Сервер** захватывает проверенный **id клиента** (Common Name листа) и сохраняет его как + `peer_id` сессии. +2. Принимающая сторона извлекает точку EC-публичного ключа из листа и проверяет `signature` по + `transcript`. Неуспех — это `Signature(...)`. -Possession of the certificate's private key is therefore proven by the signature over the -transcript; the certificate identity is proven by the CA chain check. +Таким образом, владение приватным ключом сертификата доказывается подписью по transcript, а личность +сертификата — проверкой цепочки против CA. ### Finished -Each side sends, then verifies, a Finished MAC bound to the transcript and the direction key: +Каждая сторона отправляет, затем проверяет, MAC Finished, привязанный к transcript и ключу +направления: ``` -Finished_c2s = HMAC-SHA256(key_c2s, transcript) // client sends (msg 5), server verifies -Finished_s2c = HMAC-SHA256(key_s2c, transcript) // server sends (msg 6), client verifies +Finished_c2s = HMAC-SHA256(key_c2s, transcript) // отправляет клиент (сообщение 5), проверяет сервер +Finished_s2c = HMAC-SHA256(key_s2c, transcript) // отправляет сервер (сообщение 6), проверяет клиент ``` -Verification is constant-time (`Hmac::verify_slice`); a mismatch is `FinishedMismatch`. The -Finished exchange confirms both sides derived identical keys and agree on the full transcript. +Проверка выполняется за постоянное время (`Hmac::verify_slice`); несовпадение — это +`FinishedMismatch`. Обмен Finished подтверждает, что обе стороны вывели одинаковые ключи и согласны по +всему transcript. -### Encrypted handshake messages and counter continuity +### Зашифрованные сообщения рукопожатия и непрерывность счётчиков -Messages 3–6 are AEAD-sealed under the **same** two directional `AeadSession`s that protect -application Data; their nonce counters are continuous across the handshake/data boundary. +Сообщения 3–6 запечатываются AEAD под **теми же** двумя направленными `AeadSession`, что защищают +прикладные Data; их счётчики nonce непрерывны через границу рукопожатие/данные. -- The AAD for each encrypted handshake message is its 5-byte frame header (binding type + - length), matching the Data-record convention. -- Each direction seals **exactly two** encrypted handshake messages before Data begins: - - c2s seals `ClientAuth` (counter 0) and `Finished` (counter 1) - - s2c seals `ServerAuth` (counter 0) and `Finished` (counter 1) -- Therefore both directions reach AEAD counter **2** at the end of the handshake, and the - first application Data record stamps `seq == 2` (`POST_HANDSHAKE_COUNTER`). This seeds the - replay window (below). +- 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, уже использованные в рукопожатии, + не переиспользуются. --- -## Hybrid KEM +## Гибридный KEM -The key exchange is a hybrid of classical X25519 ECDH and post-quantum ML-KEM-768 -(`crates/aura-crypto/src/kem/`). An attacker must break **both** primitives to recover the -session key. +Обмен ключами — гибрид классического X25519 ECDH и пост-квантового ML-KEM-768 +(`crates/aura-crypto/src/kem/`). Атакующему нужно сломать **оба** примитива, чтобы восстановить ключ +сессии. -> **ML-KEM-768 (FIPS 203)**, via the RustCrypto `ml-kem` crate (v0.3) — this is the -> standardized FIPS 203 scheme, **not** round-3 Kyber. +> **ML-KEM-768 (FIPS 203)** через крейт RustCrypto `ml-kem` (v0.3) — это стандартизованная схема +> FIPS 203, а **не** Kyber раунда 3. -### Roles +### Роли -- The **client** owns the long-term `HybridPrivateKey` and publishes its `HybridPublicKey` - in ClientHello. -- The **server** calls `encapsulate()` against that public key: it generates an **ephemeral** - X25519 keypair and an ML-KEM encapsulation, returns the `HybridCiphertext` in ServerHello, - and derives the shared secret. -- The **client** recovers the same secret via `decapsulate()`. +- **Клиент** владеет долговременным `HybridPrivateKey` и публикует свой `HybridPublicKey` в + ClientHello. +- **Сервер** вызывает `encapsulate()` против этого публичного ключа: он генерирует **эфемерную** пару + X25519 и ML-KEM-инкапсуляцию, возвращает `HybridCiphertext` в ServerHello и выводит общий секрет. +- **Клиент** восстанавливает тот же секрет через `decapsulate()`. -So X25519 is **ephemeral–static** (server ephemeral against client static public), while -ML-KEM is a standard KEM against the client's encapsulation key. +То есть X25519 здесь **эфемерно–статический** (эфемерный сервера против статического публичного +клиента), а ML-KEM — это стандартный KEM против инкапсуляционного ключа клиента. -### Sizes +### Размеры -| Quantity | Bytes | Constant | +| Величина | Байт | Константа | |-----------------------------------|------:|---------------------| -| X25519 public / ephemeral / secret| 32 | `X25519_LEN` | -| ML-KEM-768 encapsulation key (ek) | 1184 | `EK_LEN` | -| ML-KEM-768 ciphertext (ct) | 1088 | `CT_LEN` | -| ML-KEM-768 shared secret | 32 | `SS_LEN` | -| ML-KEM-768 decapsulation key (dk) | 2400 | `DK_LEN` | +| 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` | -> **Implementation detail — dk encoding.** The decapsulation (secret) key is stored in the -> FIPS 203 **expanded 2400-byte** form (`ExpandedKeyEncoding`), not the 64-byte seed that -> `ml-kem` 0.3 prefers. This is the encoding the project's ACVP / FIPS-203 known-answer test -> vectors operate on, so it is used for interop/KAT compatibility. The dk never travels on the -> wire — only `ek` (1184 B) and `ct` (1088 B) do. +> **Деталь реализации — кодирование dk.** Деинкапсуляционный (секретный) ключ хранится в +> **развёрнутой 2400-байтной** форме FIPS 203 (`ExpandedKeyEncoding`), а не в 64-байтном seed, +> который предпочитает `ml-kem` 0.3. Именно с этим кодированием работают KAT-векторы ACVP / FIPS 203 +> проекта, поэтому оно используется для совместимости/interop. dk никогда не путешествует по проводу — +> по нему идут только `ek` (1184 Б) и `ct` (1088 Б). -### Combined shared secret +### Объединённый общий секрет ``` -shared = x25519_ss (32 B) || mlkem_ss (32 B) // 64 bytes total +shared = x25519_ss (32 Б) || mlkem_ss (32 Б) // всего 64 байта ``` -ML-KEM decapsulation is infallible on a correctly sized ciphertext: a tampered ciphertext -yields a pseudo-random secret (implicit rejection) rather than an error, which surfaces later -as an AEAD/Finished failure. +Деинкапсуляция ML-KEM не может завершиться ошибкой на корректно размерном шифртексте: подделанный +шифртекст даёт псевдослучайный секрет (неявное отклонение), а не ошибку, что позже всплывает как сбой +AEAD/Finished. --- -## Key derivation (HKDF) +## Вывод ключей (HKDF) -Directional session keys are derived with **HKDF-SHA256** (RFC 5869) +Направленные ключи сессии выводятся с помощью **HKDF-SHA256** (RFC 5869) (`crates/aura-crypto/src/kdf.rs`): ``` -salt = client_nonce || server_nonce (64 bytes) -IKM = x25519_ss || mlkem_ss (64 bytes) +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 bytes) +OKM = HKDF-Expand(HKDF-Extract(salt, IKM), info, 64) (64 байта) key_client_to_server = OKM[0..32] key_server_to_client = OKM[32..64] ``` -The derivation is fully deterministic in its inputs. The `info` string provides domain -separation. Intermediate secret material (`salt`, `IKM`, `OKM`) is zeroized after use, and -`SessionKeys` zeroizes its keys on drop. +Вывод полностью детерминирован по своим входам. Строка `info` обеспечивает доменное разделение. +Промежуточный секретный материал (`salt`, `IKM`, `OKM`) обнуляется после использования, а +`SessionKeys` обнуляет свои ключи при drop. --- ## AEAD -The record cipher is **ChaCha20-Poly1305** (`crates/aura-crypto/src/aead.rs`). An -`AeadSession` holds a 256-bit key and a 64-bit message counter; each direction has its own -session. +Шифр записей — **ChaCha20-Poly1305** (`crates/aura-crypto/src/aead.rs`). Есть два режима: -### Nonce scheme +- `AeadSession` — для **потоковых** транспортов (TCP, QUIC, поток рукопожатия): держит 256-битный ключ + и 64-битный счётчик сообщений; nonce выводится из счётчика, который продвигается шаг в шаг на каждом + `seal` и `open`, поэтому стороны остаются синхронными без передачи nonce. +- `AeadKey` — для **датаграммного** (UDP) пути: nonce-счётчик передаётся аргументом на каждый вызов, + потому что датаграммы могут теряться или переупорядочиваться, так что счётчик каждой записи несётся + на проводе. Схема nonce идентична `AeadSession`, поэтому оба совместимы на одном ключе, пока их + диапазоны счётчиков не пересекаются. -The 96-bit (12-byte) nonce is derived from the counter: +### Схема nonce + +96-битный (12-байтный) nonce выводится из счётчика: ``` -nonce[0..8] = counter as little-endian u64 +nonce[0..8] = counter как little-endian u64 nonce[8..12] = 0x00 00 00 00 ``` -The counter advances by one on every `seal` **and** every `open` (even on a failed `open`), -so a paired seal/open stay aligned without transmitting the nonce. The nonce is never reused -within a session (the 2^64 counter wrap is unreachable; an overflow panics rather than -reusing a nonce). The key is zeroized on drop. +В потоковом режиме nonce никогда не переиспользуется в рамках сессии (переполнение счётчика 2^64 +недостижимо; при переполнении происходит паника, а не повторное использование nonce). В датаграммном +режиме за уникальность счётчика отвечает отправитель (`DatagramSender` монотонно увеличивает `seq`). +Ключ обнуляется при drop. --- -## Data records and replay protection +## Записи данных и защита от повтора (replay) -After the handshake, application `Frame`s are exchanged as `Data` records -(`crates/aura-proto/src/session.rs`). Each `Data` record's **payload** is: +После рукопожатия прикладные `Frame` передаются как записи `Data` +(`crates/aura-proto/src/session.rs`). Кодирование зависит от того, потоковый транспорт или +датаграммный — но логика replay-окна общая. + +### Потоковый путь (TCP / QUIC) + +**Полезная нагрузка** каждой записи `Data`: ``` seq (u64, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = header || seq ) ``` -- `seq` is the 8-byte big-endian record counter. On the happy path it equals the sealing - AEAD's counter (and the receiver's expected AEAD counter). -- The AEAD **AAD** is the 5-byte frame `header` concatenated with the 8-byte `seq`, so the - record is cryptographically bound to both its declared length/type and its claimed position. -- The ciphertext includes the 16-byte Poly1305 tag. +- `seq` — 8-байтный big-endian счётчик записи. На «счастливом пути» он равен счётчику запечатывающего + AEAD (и ожидаемому счётчику AEAD приёмника). +- **AAD** AEAD — это 5-байтный заголовок кадра `header`, сцепленный с 8-байтным `seq`, так что запись + криптографически привязана и к своей объявленной длине/типу, и к заявленной позиции. +- Шифртекст включает 16-байтный тег Poly1305. -So the full record on the wire is: +Полная запись на проводе: ``` [ header(5) ][ seq(8) ][ ciphertext + tag ] @@ -316,56 +438,74 @@ So the full record on the wire is: header.length = 8 + len(ciphertext+tag) ``` -### Sliding replay window +### Датаграммный путь (свой UDP) -The receiver runs a **64-wide sliding-window** replay check (`REPLAY_WINDOW = 64`) *before* -touching the AEAD, so a duplicate or too-old record is rejected with `Replay(seq)` without -disturbing the AEAD counter (the session stays usable). The window: +Здесь нет 5-байтного потокового заголовка внутри записи. Датаграммная запись +(`DatagramSender::seal`): -- tracks the highest accepted `seq` plus a 64-bit bitmap of accepted positions below it; -- accepts a `seq` iff it is strictly newer than everything seen, or falls within the window - and has not been seen before; -- rejects a `seq` that equals the current highest, is already marked in the bitmap, or is - more than `REPLAY_WINDOW` below the highest. +``` +seq(8, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = seq ) +``` -The window is seeded at the post-handshake counter (`start = 2`): everything strictly below -`start` is treated as already-consumed, so the first legitimate Data record (`seq == 2`) is -accepted as "newer". +То есть AAD — это **только** `seq` (а не `header || seq`). На проводе эта запись несётся внутри +DATA-датаграммы UDP-транспорта как `0x02 || rec_len(u16 BE) || запись [|| паддинг]` (см. раздел про +транспорт v2 выше). -### Full-duplex split +### Скользящее replay-окно -A `Session` can be `split()` into independent `SessionSender` (writer + outbound AEAD + -send counter) and `SessionReceiver` (reader + inbound AEAD + replay window) halves, which can -be driven from separate tasks for a concurrent read/write data path (e.g. the VPN tunnel). -`recv_frame` is **not** cancellation-safe and must be driven from a single owning task. +Приёмник запускает проверку повтора **скользящим окном шириной 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`. --- -## Mimicry layer +## Слой мимикрии -The outer QUIC/TLS layer (`crates/aura-transport/`) exists purely to disguise the connection -as browser HTTP/3 traffic. It is explicitly **not** the authentication boundary. +Внешний слой QUIC/TLS (`crates/aura-transport/`) существует исключительно для маскировки соединения +под трафик браузера HTTP/3. Он явно **не** является границей аутентификации. -- **ALPN** advertises `h3` and `h3-29` (`ALPN_H3`) — exactly what Chrome offers for HTTP/3 — - so the ALPN extension is indistinguishable from a real browser's. -- **Transport params** mirror a Chromium HTTP/3 connection: ~30 s idle timeout, ~15 s - keep-alive, 100 concurrent bidi/uni streams, ~10 MB flow-control receive windows +- **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** defaults to a generic CDN-looking hostname (`cdn.example.com`) when the caller does - not supply one; deployments pass their own camouflage hostname. -- The QUIC **client accepts any server certificate** (`AcceptAnyServerCert` — all verifier - methods return success). This is safe *only* because the outer TLS is not authentication: - the real mutual auth is the inner Aura handshake. The server's outer TLS likewise disables - client auth (`with_no_client_auth`). +- **SNI** по умолчанию — обобщённое CDN-подобное имя (`cdn.example.com`), если вызывающий его не задал; + развёртывания передают своё камуфляжное имя. +- QUIC-**клиент принимает любой серверный сертификат** (`AcceptAnyServerCert` — все методы + верификатора возвращают успех). Это безопасно *только* потому, что внешний TLS не является + аутентификацией: настоящая взаимная аутентификация — это внутреннее рукопожатие Aura. Внешний TLS + сервера также отключает клиентскую аутентификацию (`with_no_client_auth`). -> Do not reuse `AcceptAnyServerCert` anywhere the TLS layer *is* the authentication boundary. +> Не переиспользуйте `AcceptAnyServerCert` нигде, где слой TLS *является* границей аутентификации. + +Лёгкая HTTP-маскировка TCP-транспорта (`TcpOpts::masquerade`) преследует ту же цель, но гораздо +скромнее (см. раздел про транспорт v2): это преамбула HTTP/1.1, а не TLS. --- -## Error model +## Модель ошибок -The protocol layer surfaces `ProtoError` (`crates/aura-proto/src/lib.rs`), including: +Слой протокола выдаёт `ProtoError` (`crates/aura-proto/src/lib.rs`), в том числе: `Io`, `Crypto`, `Pki`, `UnknownMsgType`, `BadVersion`, `FrameTooLarge`, `UnexpectedMsg`, -`MalformedHandshake`, `MalformedFrame`, `Signature`, `FinishedMismatch`, `Replay`, and -`Alert`. A peer may send a fatal `Alert` frame (type `0xFF`); the first payload byte is the -alert code, surfaced to the local side as `ProtoError::Alert(code)`. +`MalformedHandshake`, `MalformedFrame`, `Signature`, `FinishedMismatch`, `Replay` и `Alert`. Пир может +отправить фатальный кадр `Alert` (тип `0xFF`); первый байт полезной нагрузки — код alert, который +всплывает на локальной стороне как `ProtoError::Alert(code)`. diff --git a/docs/sing-box.md b/docs/sing-box.md index 3e009df..cfb934e 100644 --- a/docs/sing-box.md +++ b/docs/sing-box.md @@ -1,70 +1,80 @@ -# Integrating AuraVPN with sing-box (approach note) +# Интеграция AuraVPN с sing-box (план) -Goal: let a phone client running **sing-box** connect to an Aura server speaking the **AuraVPN** -protocol (Aura's own tunneling — not a third party's). This is a short note on *how*; the wire -protocol it must match is fully specified in [`protocol.md`](protocol.md). +Цель: дать клиенту на телефоне, работающему под **sing-box**, возможность подключиться к серверу +Aura по протоколу **AuraVPN** (это собственное туннелирование Aura, не сторонний протокол). Это +короткая заметка о том, *как* это сделать; сам wire-протокол, который должна повторять реализация, +полностью описан в [`protocol.md`](protocol.md). -sing-box is written in Go and has no generic "load an arbitrary external wire protocol" plugin, so -integration means giving sing-box a Go implementation (or a bridge) of the Aura protocol. Three -realistic paths, cheapest first: +sing-box написан на Go, и в нём нет универсального плагина «загрузить произвольный внешний wire- +протокол», поэтому интеграция означает дать sing-box Go-реализацию (или мост) протокола Aura. Три +реалистичных пути, от самого дешёвого: -## Option A — Process bridge (fastest, desktop/server) +## Option A — Process bridge (быстрее всего, для десктопа/сервера) -Run the existing Rust `aura` client as a local process and expose a local proxy/TUN, then point -sing-box at it: +Запустить существующий Rust-клиент `aura` как локальный процесс и выставить локальный +прокси/TUN, а затем направить sing-box на него: -- `aura client` already creates a TUN and routes through the Aura tunnel; sing-box can route selected - traffic into that interface, **or** -- add a local **SOCKS5 inbound** to `aura-cli` (small addition) and configure a sing-box `socks` - outbound pointing at `127.0.0.1:`. +- `aura client` уже создаёт TUN и направляет туда трафик через туннель Aura; sing-box может + заворачивать выбранный трафик в этот интерфейс, **или** +- добавить в `aura-cli` локальный **SOCKS5 inbound** (небольшое дополнение) и настроить в + sing-box `socks` outbound на `127.0.0.1:`. -Pros: reuses the audited Rust core verbatim; no crypto re-implementation. Cons: two processes; weak -fit for mobile (sing-box mobile apps embed the core and don't spawn helpers easily). +Плюсы: переиспользует прошедшую аудит Rust-сердцевину дословно; нет переписывания криптографии. +Минусы: два процесса; плохо ложится на мобильные платформы (мобильные приложения sing-box +встраивают ядро и не умеют легко запускать вспомогательные процессы). -## Option B — Native Go outbound/inbound (the real target for phones) +## Option B — Нативный Go-outbound/inbound (целевой путь для телефонов) -Implement the AuraVPN protocol natively in Go and register it as a sing-box **outbound** (client) and -**inbound** (server), so the phone's embedded sing-box core speaks AuraVPN directly. This is the -clean, performant, mobile-friendly path. Crypto maps cleanly to existing Go libraries: +Реализовать протокол AuraVPN на Go нативно и зарегистрировать его как sing-box **outbound** +(клиент) и **inbound** (сервер), чтобы встроенное в телефон ядро sing-box само говорило на +AuraVPN. Это чистый, производительный и mobile-friendly путь. Криптография чисто ложится на +существующие Go-библиотеки: -| Aura piece | Rust crate | Go equivalent | -|---|---|---| -| X25519 ECDH | `x25519-dalek` | `crypto/ecdh` (stdlib) | -| ML-KEM-768 (FIPS 203) | `ml-kem` | `crypto/mlkem` (Go 1.24+) or `cloudflare/circl` | -| ChaCha20-Poly1305 | `chacha20poly1305` | `golang.org/x/crypto/chacha20poly1305` | -| HKDF-SHA256 | `hkdf` | `golang.org/x/crypto/hkdf` | -| HMAC-SHA256 (Finished) | `hmac` | `crypto/hmac` + `crypto/sha256` | -| ECDSA P-256 sigs (cert auth) | `ring` | `crypto/ecdsa` + `crypto/x509` | -| X.509 verify + CRL | `rustls-webpki` | `crypto/x509` | +| Компонент Aura | Rust-крейт | Эквивалент в Go | +|--------------------------------|---------------------|---------------------------------------------------------| +| X25519 ECDH | `x25519-dalek` | `crypto/ecdh` (stdlib) | +| ML-KEM-768 (FIPS 203) | `ml-kem` | `crypto/mlkem` (Go 1.24+) или `cloudflare/circl` | +| ChaCha20-Poly1305 | `chacha20poly1305` | `golang.org/x/crypto/chacha20poly1305` | +| HKDF-SHA256 | `hkdf` | `golang.org/x/crypto/hkdf` | +| HMAC-SHA256 (Finished) | `hmac` | `crypto/hmac` + `crypto/sha256` | +| ECDSA P-256 signatures (cert) | `ring` | `crypto/ecdsa` + `crypto/x509` | +| X.509 verify + CRL | `rustls-webpki` | `crypto/x509` | -What the Go code must reproduce **exactly** (see `protocol.md`): -- 5-byte frame header `msg_type(1) || len(u24 BE) || version=0x01`. -- Handshake order CH → SH → ServerAuth → ClientAuth → Finished(c→s) → Finished(s→c); transcript = - `SHA-256(ClientHello_frame || ServerHello_frame)`; ECDSA-P256/SHA-256 signature over the transcript; - HMAC-SHA256 Finished. -- Hybrid shared secret = `x25519_ss || mlkem_ss`; HKDF salt = `client_nonce || server_nonce`, +Что Go-код должен повторить **в точности** (см. `protocol.md`): + +- 5-байтный заголовок кадра: `msg_type(1) || len(u24 BE) || version=0x01`. +- Порядок рукопожатия `CH → SH → ServerAuth → ClientAuth → Finished(c→s) → Finished(s→c)`; + транскрипт = `SHA-256(ClientHello_frame || ServerHello_frame)`; подпись ECDSA-P256/SHA-256 по + транскрипту; Finished — HMAC-SHA256. +- Гибридный общий секрет = `x25519_ss || mlkem_ss`; salt HKDF = `client_nonce || server_nonce`, info = `b"aura-v1-session"`. -- Data record (datagram/UDP) = `seq(8 BE) || ChaCha20Poly1305(frame, aad = seq)`, nonce = - `LE(seq) || 0x00000000`; replay window 64. (Stream/TCP record adds the 5-byte header to the AAD.) -- Transport selection: UDP (type `0x01` HS / `0x02` DATA) primary; TCP/443 and QUIC fallbacks. +- Запись данных (datagram/UDP) = `seq(8 BE) || ChaCha20Poly1305(frame, aad = seq)`, nonce = + `LE(seq) || 0x00000000`; окно anti-replay — 64. (Stream/TCP-запись дополнительно включает + 5-байтный заголовок в AAD.) +- Выбор транспорта: UDP (тип-байт `0x01` HS / `0x02` DATA) как основной; TCP/443 и QUIC как + fallback. -To de-risk the Go port, export **known-answer test vectors** from the Rust side (a captured -handshake transcript + derived keys + a sealed data record) and assert the Go implementation -reproduces them byte-for-byte. The ML-KEM KAT already lives in `aura-crypto/tests/kat_kyber.rs`. +Чтобы снизить риск порта в Go, экспортируйте со стороны Rust **known-answer test vectors** +(захваченный транскрипт рукопожатия + производные ключи + запечатанная запись данных) и +утверждайте, что Go-реализация воспроизводит их побайтово. KAT для ML-KEM уже лежит в +`aura-crypto/tests/kat_kyber.rs`. -## Option C — Rust core via cgo (`cdylib`) +## Option C — Rust core через cgo (`cdylib`) -Compile the Aura Rust core to a C-ABI shared library and call it from a thin sing-box Go shim via -cgo. Reuses the audited crypto/handshake with no Go re-implementation, but cgo + per-platform -(Android/iOS) packaging is fiddly and complicates sing-box's pure-Go build. +Скомпилировать Rust-сердцевину Aura в C-ABI shared library и вызывать её из тонкого Go-shim'а +sing-box через cgo. Переиспользует прошедшую аудит крипто/рукопожатие без Go-переписывания, но +cgo плюс упаковка под каждую платформу (Android/iOS) — занудно и усложняет чисто-Go-сборку +sing-box. -## Recommendation +## Рекомендация -- **Now:** Option A (process bridge) for desktop/server validation — minimal work, real protocol. -- **For the phone:** Option B (native Go outbound), built against `protocol.md` + exported Rust test - vectors. It is the only option that fits sing-box's embedded mobile core well. -- Keep `protocol.md` the single source of truth and version the wire protocol (the header already - carries `version = 0x01`) so the Rust and Go implementations stay in lockstep. +- **Сейчас:** Option A (process bridge) для валидации на десктопе/сервере — минимум работы, + настоящий протокол. +- **Для телефона:** Option B (нативный Go-outbound), написанный по `protocol.md` + по + экспортированным из Rust тест-векторам. Это единственный вариант, который хорошо ложится на + встроенное мобильное ядро sing-box. +- Держите `protocol.md` единственным источником истины и версионируйте wire-протокол (заголовок + уже несёт `version = 0x01`), чтобы Rust- и Go-реализации шли в ногу. -> Status: this is a design note. No Go code or sing-box module is implemented yet — that is a -> separate deliverable tracked for after the Rust transport stabilizes. +> Статус: это проектная заметка. Go-кода и sing-box-модуля пока **нет** — это отдельный +> deliverable, поставленный в план после стабилизации Rust-транспорта. diff --git a/docs/split-tunnel.md b/docs/split-tunnel.md index fa444fc..7e4fbdf 100644 --- a/docs/split-tunnel.md +++ b/docs/split-tunnel.md @@ -1,121 +1,124 @@ -# Aura Split Tunnel +# Split tunnel Aura -Split tunneling decides, per destination IP, whether a packet travels **through the encrypted -VPN** or **egresses directly** (bypassing the tunnel). It lets you keep, say, RFC1918 LAN -traffic local while sending the rest through Aura — or the reverse. +Split-tunneling решает для каждого назначения IP, идёт ли пакет **через шифрованный VPN** или +**уходит напрямую** (минуя туннель). Это позволяет, например, оставить трафик к RFC1918-сетям +локальным, а остальное пустить через Aura — или наоборот. -It is implemented in the `aura-tunnel` crate (`routes.rs`, `router.rs`, `dns.rs`), configured -statically via the `[tunnel.split]` section of `client.toml` -(`crates/aura-cli/src/config.rs`), and managed live via the `aura route` / `aura status` -admin commands (`crates/aura-cli/src/admin.rs`). +Реализация лежит в крейте `aura-tunnel` (`routes.rs`, `router.rs`, `dns.rs`), статически +настраивается секцией `[tunnel.split]` в `client.toml` (`crates/aura-cli/src/config.rs`), а +управляется на лету командами `aura route` / `aura status` через admin-сокет +(`crates/aura-cli/src/admin.rs`). --- -## Concept: VPN vs DIRECT +## Концепция: VPN или DIRECT -Every outbound IP packet read from the TUN device is classified into one of two actions +Каждый исходящий IP-пакет, прочитанный с TUN-устройства, классифицируется в одно из двух действий (`RouteAction`): -- **`Vpn`** — encrypt and send the packet over the Aura connection to the server. -- **`Direct`** — let the packet egress directly, bypassing the tunnel. +- **`Vpn`** — зашифровать и отправить пакет по соединению Aura на сервер. +- **`Direct`** — выпустить пакет напрямую, минуя туннель. -The router (`AuraRouter::run`, `router.rs`) parses each packet's destination IP, classifies -it, and dispatches: +Маршрутизатор (`AuraRouter::run`, `router.rs`) парсит у каждого пакета IP назначения, +классифицирует его и диспетчеризует: ``` TUN read --> parse dst IP --> RouteTable.classify(dst) --> Vpn? -> conn.send_packet() - \ Direct? -> send_direct() (v1 stub) + \ Direct? -> send_direct() (заглушка в v1) ``` -> **v1 limitation — `Direct` is a stub.** `send_direct` currently **logs and drops** the -> packet; real raw-socket / OS-stack re-injection is out of scope for v1. The method is -> already `async` and fallible so a real egress path can slot in without changing call sites. -> The VPN path is fully functional end-to-end. Packets whose destination cannot be parsed -> (not IPv4/IPv6, or too short) are dropped with a trace. +> **Ограничение v1 — `Direct` это заглушка.** Текущая реализация `send_direct` **логирует и +> отбрасывает** пакет; реальный raw-socket / реинъекция в стек ОС в объём v1 не входят. Метод уже +> объявлен `async` и возвращает `Result`, чтобы реальный путь egress подключился без изменения +> вызывающего кода. Путь через VPN полностью работоспособен сквозным образом. Пакеты, у которых +> не получилось разобрать назначение (не IPv4/IPv6 или слишком короткие), отбрасываются с trace- +> сообщением. -The inbound direction is straightforward: decrypted IP packets received from the peer are -written back to the TUN device. +Входящее направление прямолинейно: расшифрованные IP-пакеты, полученные от пира, пишутся обратно +на TUN-устройство. --- -## Rules +## Правила -The routing table (`RouteTable`, `routes.rs`) holds three things: a set of **CIDR rules**, a -set of **domain rules**, and a **default action**. +Таблица маршрутизации (`RouteTable`, `routes.rs`) хранит три вещи: набор **CIDR-правил**, набор +**доменных правил** и **действие по умолчанию**. -### CIDR rules +### CIDR-правила -A CIDR rule is an `IpNetwork` (e.g. `10.0.0.0/8`) plus an action. CIDR rules are keyed by -network, so re-adding the same network **overwrites** its action. +CIDR-правило — это `IpNetwork` (например `10.0.0.0/8`) плюс действие. CIDR-правила +ключуются сетью, поэтому повторное добавление той же сети **перезаписывает** её действие. -### Domain rules +### Доменные правила -A domain rule is a domain name plus an action. Domains do **not** match IPs directly. Instead -`AuraDns` (`dns.rs`) resolves the domain via the system resolver (hickory) and inserts each -resulting address as a **host route** — `/32` for IPv4, `/128` for IPv6 — so it participates -in the normal longest-prefix match. Resolution results are cached. +Доменное правило — это доменное имя плюс действие. Домены **не** сопоставляются с IP напрямую. +Вместо этого `AuraDns` (`dns.rs`) резолвит домен через системный резолвер (hickory) и вставляет +каждый получившийся адрес как **host-маршрут** — `/32` для IPv4, `/128` для IPv6, — так что они +участвуют в обычном longest-prefix matching. Результаты резолва кэшируются. -> Because domain rules become host routes at resolution time, they only take effect once the -> domain has been resolved (at startup, or on demand). They reflect the addresses seen at -> resolution time and are not continuously re-resolved in v1. +> Поскольку доменные правила становятся host-маршрутами в момент резолва, они действуют только +> после того, как домен был разрешён (при старте или по требованию). Они отражают адреса, +> увиденные в момент резолва, и не перерезолвятся непрерывно в v1. -### Default action +### Действие по умолчанию -If no CIDR rule (including resolved domain host routes) matches a destination, the table's -**default action** applies. +Если ни одно CIDR-правило (включая host-маршруты от резолва доменов) не совпало с назначением, +применяется **действие по умолчанию** таблицы. --- -## Longest-prefix precedence +## Приоритет longest-prefix -`classify(dst_ip)` performs a **longest-prefix match** (`routes.rs`): +`classify(dst_ip)` выполняет **longest-prefix match** (`routes.rs`): -> Among all CIDR rules whose network contains the destination, the rule with the **largest -> prefix length** (most specific) wins. If no rule matches, the default action is returned. +> Среди всех CIDR-правил, чьи сети содержат назначение, побеждает правило с **наибольшей длиной +> префикса** (наиболее специфичное). Если ни одно правило не совпало, возвращается действие по +> умолчанию. -This lets a specific range override a broader one regardless of insertion order. IPv4 rules -only match IPv4 destinations and IPv6 rules only match IPv6 destinations. +Так специфичный диапазон может перекрыть более широкий независимо от порядка вставки. IPv4-правила +совпадают только с IPv4-назначениями, а IPv6 — только с IPv6. -Example (from the shipped config): with `default = VPN`, `10.0.0.0/8 = Direct`, and +Пример (из поставляемой конфигурации): при `default = VPN`, `10.0.0.0/8 = Direct` и `10.7.0.0/24 = Vpn`: -| Destination | Matched rule | Action | -|--------------|----------------------|--------| -| `10.1.2.3` | `10.0.0.0/8` | Direct | -| `10.7.0.9` | `10.7.0.0/24` (more specific, wins over `/8`) | Vpn | -| `192.168.1.1`| `192.168.0.0/16` | Direct | -| `8.8.8.8` | (none) → default | Vpn | +| Назначение | Сработавшее правило | Действие | +|--------------|----------------------------------------------|----------| +| `10.1.2.3` | `10.0.0.0/8` | Direct | +| `10.7.0.9` | `10.7.0.0/24` (более специфичное, бьёт `/8`) | Vpn | +| `192.168.1.1`| `192.168.0.0/16` | Direct | +| `8.8.8.8` | (нет) → действие по умолчанию | Vpn | -> Edge case: if two rules share the **same** prefix length, the **last-inserted** one wins -> (it overwrites the earlier entry, since rules are keyed by network). +> Крайний случай: если два правила имеют **одинаковую** длину префикса, побеждает +> **вставленное последним** (оно перезатирает предыдущее, потому что правила ключуются сетью). --- -## Static config: `[tunnel.split]` +## Статическая конфигурация: `[tunnel.split]` -The split tunnel is configured in `client.toml` under `[tunnel.split]` -(`crates/aura-cli/src/config.rs`). `build_route_table` turns it into a `RouteTable`: CIDR -rules are applied directly; domain rules are recorded and returned for the client to resolve -at startup. +Split-tunnel настраивается в `client.toml` в секции `[tunnel.split]` +(`crates/aura-cli/src/config.rs`). `build_route_table` превращает её в `RouteTable`: CIDR-правила +применяются напрямую; доменные правила сохраняются и возвращаются, чтобы клиент мог разрезолвить +их на старте. -### Schema +### Схема -| Key | Type | Default | Meaning | -|------------------------------|-----------------|---------|----------------------------------------------------| -| `default` | string | `"VPN"` | Action when no rule matches: `VPN` / `DIRECT` (case-insensitive) | -| `[[tunnel.split.direct]]` | array of rules | `[]` | Rules forcing matching destinations to **Direct** | -| `[[tunnel.split.vpn]]` | array of rules | `[]` | Rules forcing matching destinations through the **VPN** | +| Ключ | Тип | По умолчанию | Смысл | +|------------------------------|---------------------|--------------|------------------------------------------------| +| `default` | строка | `"VPN"` | Действие, когда ни одно правило не совпало: `VPN` / `DIRECT` (регистронезависимо) | +| `[[tunnel.split.direct]]` | массив правил | `[]` | Правила, отправляющие совпавшие назначения в **Direct** | +| `[[tunnel.split.vpn]]` | массив правил | `[]` | Правила, отправляющие совпавшие назначения **через VPN** | -Each rule in `direct` / `vpn` is a table with **exactly one** of: +Каждое правило в `direct` / `vpn` — это таблица с **ровно одним** из ключей: -| Key | Type | Example | -|----------|--------|---------------------| -| `cidr` | string | `"192.168.0.0/16"` | -| `domain` | string | `"intranet.example.com"` | +| Ключ | Тип | Пример | +|----------|--------|--------------------------| +| `cidr` | строка | `"192.168.0.0/16"` | +| `domain` | строка | `"intranet.example.com"` | -A rule with both `cidr` and `domain`, or neither, is rejected when the route table is built. +Правило, у которого указаны и `cidr`, и `domain` (или ни того ни другого), отвергается на этапе +построения таблицы маршрутизации. -### Example +### Пример ```toml # Split-tunnel routing: the default action plus per-destination overrides. @@ -139,23 +142,23 @@ domain = "intranet.example.com" cidr = "10.7.0.0/24" ``` -This is the configuration shipped in `config/client.toml.example`. +Это и есть та конфигурация, что поставляется в `config/client.toml.example`. --- -## Live management: `aura route` / `aura status` +## Управление на лету: `aura route` / `aura status` -A running `aura client` (or `aura server`) hosts an **admin socket** — a tiny JSON -line-protocol over a **Unix domain socket** (`crates/aura-cli/src/admin.rs`). The `aura -route` and `aura status` subcommands connect to it to inspect and mutate the live routing -table without restarting the tunnel. The default socket path is `/tmp/aura-admin.sock` -(override with `--admin-socket`). +Работающий `aura client` (или `aura server`) поднимает **admin-сокет** — крошечный построчный +JSON-протокол поверх **Unix domain socket** (`crates/aura-cli/src/admin.rs`). Подкоманды `aura +route` и `aura status` подключаются к нему, чтобы инспектировать и менять живую таблицу +маршрутизации, не перезапуская туннель. Путь сокета по умолчанию — `/tmp/aura-admin.sock` +(перекрывается через `--admin-socket`). -> Platform note: the admin socket uses Unix domain sockets (Linux/macOS). On Windows it is a -> `cfg`-gated stub that returns an explanatory error (a named-pipe transport is future work), -> so the rest of the CLI still compiles there. +> Замечание про платформу: admin-сокет использует Unix domain sockets (Linux/macOS). На Windows +> это `cfg`-заглушка, возвращающая поясняющую ошибку (named-pipe-транспорт — будущая работа), — +> остальная часть CLI там по-прежнему собирается. -### Commands +### Команды ``` aura route add (--cidr | --domain ) --action [--admin-socket ] @@ -164,29 +167,29 @@ aura route remove --cidr [--a aura status [--admin-socket ] ``` -`route add` takes **exactly one** of `--cidr` / `--domain` (they are mutually exclusive, and -one is required), plus `--action vpn` or `--action direct`. +`route add` принимает **ровно один** из ключей `--cidr` / `--domain` (они взаимоисключающие, и +один из них обязателен), плюс `--action vpn` или `--action direct`. ```bash -# Send a CIDR directly, live. +# Отправить CIDR напрямую, на лету. aura route add --cidr 8.8.8.0/24 --action direct # ok -# Route a domain through the VPN (resolved into host routes). +# Завернуть домен через VPN (разрезолвится в host-маршруты). aura route add --domain example.com --action vpn # ok -# Inspect the current rules and default. +# Посмотреть текущие правила и действие по умолчанию. aura route list # default: vpn # cidr 8.8.8.0/24 direct # domain example.com vpn -# Remove a CIDR rule. +# Удалить CIDR-правило. aura route remove --cidr 8.8.8.0/24 -# ok (removed) # or: "ok (nothing to remove)" if it wasn't present +# ok (removed) # или: "ok (nothing to remove)", если его не было -# Tunnel status / counters. +# Статус туннеля и счётчики. aura status # Aura tunnel status # peer: client-1 @@ -196,22 +199,23 @@ aura status # tx packets: 0 ``` -### Behavior notes +### Особенности поведения -- **`route remove` only removes CIDR rules** — it takes `--cidr` and has no domain form. The - library `RouteTable` has no per-rule remove API, so a removal **rebuilds** the table from - the surviving rules (preserving the default). Domain rules are re-added on rebuild, but - their previously resolved host routes are dropped and re-resolved on demand. -- **`route list` enumerates a rule mirror.** The live `RouteTable` is the source of truth for - classification but does not expose iteration, so the admin layer keeps a parallel mirror in - lockstep with every mutation; `list` echoes that mirror while `classify` still uses the real - table. -- **`status`** reports the verified peer id, the default action, the total rule count - (CIDR + domain), and inbound/outbound packet counters. +- **`route remove` удаляет только CIDR-правила** — он принимает `--cidr` и не имеет доменной + формы. У библиотечного `RouteTable` нет API для покнопочного удаления, поэтому удаление + **перестраивает** таблицу из оставшихся правил (с сохранением действия по умолчанию). Доменные + правила добавляются заново при перестройке, но их ранее разрезолвенные host-маршруты + сбрасываются и перерезолвятся по требованию. +- **`route list` перечисляет зеркало правил.** Истиной для классификации остаётся живой + `RouteTable`, но он не отдаёт итерацию по правилам, поэтому admin-слой ведёт параллельное + зеркало, синхронизированное с каждой мутацией; `list` отдаёт это зеркало, а `classify` + по-прежнему ходит в реальную таблицу. +- **`status`** сообщает проверенный peer id, действие по умолчанию, суммарное число правил + (CIDR + домены), а также счётчики пакетов на вход/выход. -### Wire protocol (for reference) +### Wire-протокол (для справки) -One JSON object per line, request then response (`crates/aura-cli/src/admin.rs`): +По одному JSON-объекту в строке: сначала запрос, затем ответ (`crates/aura-cli/src/admin.rs`): ```text -> {"cmd":"route_add","cidr":"8.8.8.0/24","action":"direct"} @@ -224,18 +228,18 @@ One JSON object per line, request then response (`crates/aura-cli/src/admin.rs`) <- {"ok":true,"peer_id":"client-1","rx_packets":0,"tx_packets":0,"default":"vpn","rules":1} ``` -On error the response is `{"ok":false,"error":"..."}`. +При ошибке ответ имеет вид `{"ok":false,"error":"..."}`. --- -## v1 limitations summary +## Сводка ограничений v1 -- **`Direct` egress is a stub** — `Direct` packets are logged and dropped, not re-injected to - the OS stack. The VPN path is fully functional. -- **Domain rules are resolved once** (at startup / on demand) into host routes; no continuous - re-resolution. -- **`route remove` is CIDR-only** and rebuilds the table (domain host routes are re-resolved - on demand afterward). -- **Admin socket is Unix-only**; Windows is a `cfg`-gated stub. -- The server is a **single shared TUN** in v1, and the tunnel resolver `dns` config field is - informational (the system resolver is used). +- **`Direct` — заглушка**: пакеты с действием `Direct` логируются и отбрасываются, а не + реинъецируются в стек ОС. Путь через VPN полностью функционален. +- **Доменные правила резолвятся один раз** (на старте или по требованию) в host-маршруты; + непрерывного перерезолва нет. +- **`route remove` работает только с CIDR** и перестраивает таблицу (доменные host-маршруты потом + разрезолвятся по требованию). +- **Admin-сокет только под Unix**; на Windows — `cfg`-заглушка. +- Сервер в v1 — это **один общий TUN**, а поле `dns` в конфигурации туннеля носит информационный + характер (используется системный резолвер).