docs: rewrite all documentation in Russian + add deployment guide

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-26 10:42:08 +03:00
parent d5b9a8611d
commit 083c441e4c
6 changed files with 1075 additions and 515 deletions
+369
View File
@@ -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/<id>/client.crt`) — листовой сертификат клиента;
- `client.key` (из `/etc/aura/clients/<id>/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 <PATH>` к каждой команде. Полная спецификация
команд и 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`-заглушка.
+115 -114
View File
@@ -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 = <domain> CN = <client_id>
SAN: DNS:<domain> (no SAN)
SAN: DNS:<domain> (нет 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 = <domain>`, a **`DNS:<domain>` SAN**, and
`extendedKeyUsage = serverAuth`. The DNS SAN is what the client matches against its expected
`server_name`.
- A **client leaf** carries `CN = <client_id>` 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 = <domain>`, **`DNS:<domain>` SAN** и
`extendedKeyUsage = serverAuth`. Именно DNS-SAN сравнивается клиентом с ожидаемым `server_name`.
- **Client leaf** несёт `CN = <client_id>` и `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 <CN> --out <DIR>
@@ -82,14 +81,14 @@ aura pki revoke --id <ID> [--crl <PATH>]
aura pki list [--crl <PATH>]
```
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:<domain>` SAN and
`serverAuth` EKU.
Выпускает листовой сертификат для DNS-имени, подписанный CA, с SAN `DNS:<domain>` и 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 = <id>` and `clientAuth` EKU. The `<id>` becomes the verified
`peer_id` the server sees.
Выпускает листовой сертификат с `CN = <id>` и EKU `clientAuth`. Это `<id>` станет проверенным
`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 нет.
+373 -233
View File
@@ -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<dyn aura_proto::PacketConnection>`, поэтому
маршрутизатору туннеля безразлично, какой транспорт перенёс соединение
(`crates/aura-transport/src/dial.rs`):
| Режим (`TransportMode`) | Реализация | Роль |
|-------------------------|------------|------|
| `Udp` | `crates/aura-transport/src/udp.rs` | Свой протокол Aura поверх обычного UDP — **основной путь** |
| `Tcp` | `crates/aura-transport/src/tcp.rs` | Aura поверх TCP/443 (fallback для сетей, блокирующих UDP; опц. HTTP-маскировка) |
| `Quic` | `crates/aura-transport/src/lib.rs`, `conn.rs`, `quic.rs` | Aura внутри QUIC/HTTP3-мимикрии (fallback / сильный камуфляж) |
### Порядок переключения (handover)
Клиентский `dial` (`dial.rs`) пробует транспорты по списку `order` слева направо; первый, который
дозвонился, побеждает. По умолчанию `order = [Udp, Tcp, Quic]`. Транспорт без сконфигурированного
адреса пропускается; транспорт, который ошибся или вышел по таймауту (`attempt_timeout`, по умолчанию
8 с), уступает место следующему.
```
dial(order = [Udp, Tcp, Quic])
└─ Udp → UdpClient::connect ── ок? → соединение по UDP
│ ошибка/таймаут
└─ Tcp → TcpClient::connect ── ок? → соединение по TCP
│ ошибка/таймаут
└─ Quic → AuraClient::connect ── ок? → соединение по QUIC
│ все упали
Err(последняя ошибка)
```
Серверная сторона (`MultiServer`, `dial.rs`) поступает иначе: она привязывается и слушает **все**
включённые в `order` транспорты одновременно и выдаёт принятые соединения из любого из них через один
`MultiServer::accept`.
> **Важно про порты.** Свой UDP-транспорт и QUIC оба используют UDP, поэтому им нужны **разные**
> порты (`udp_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<br/>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<br/>transcript = SHA-256(CH_frame || SH_frame)
Note over C,S: зашифровано (AEAD под направленными ключами)
S->>C: 3. ServerAuth (сертификат сервера + подпись по transcript)
C->>S: 4. ClientAuth (сертификат клиента + подпись по transcript)
C->>S: 5. Finished (HMAC_c2s по transcript)
S->>C: 6. Finished (HMAC_s2c по transcript)
Note over C,S: сессия установлена; записи Data идут в обе стороны
```
### Hello 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 36 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 **ephemeralstatic** (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)`.
+63 -53
View File
@@ -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:<port>`.
- `aura client` уже создаёт TUN и направляет туда трафик через туннель Aura; sing-box может
заворачивать выбранный трафик в этот интерфейс, **или**
- добавить в `aura-cli` локальный **SOCKS5 inbound** (небольшое дополнение) и настроить в
sing-box `socks` outbound на `127.0.0.1:<port>`.
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-транспорта.
+119 -115
View File
@@ -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 <CIDR> | --domain <DOMAIN>) --action <vpn|direct> [--admin-socket <PATH>]
@@ -164,29 +167,29 @@ aura route remove --cidr <CIDR> [--a
aura status [--admin-socket <PATH>]
```
`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` в конфигурации туннеля носит информационный
характер (используется системный резолвер).