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
+36
View File
@@ -0,0 +1,36 @@
# Aura VPN
Aura — гибридный пост-квантовый VPN на Rust. Внутреннее рукопожатие гибридное и взаимно
аутентифицированное (X25519 + ML-KEM-768 по FIPS 203 со взаимной X.509-проверкой), данные
шифруются ChaCha20-Poly1305 с explicit-nonce, обфускация — паддинг датаграмм под «корзины»
HTTPS-размеров.
На проводе по умолчанию идёт **собственный UDP-транспорт Aura** (без QUIC и без внешнего TLS на
основном пути). Если сеть режет UDP, клиент автоматически переключается на **TCP/443** или **QUIC**
(мимикрия HTTP/3), последовательно пробуя транспорты из настраиваемого `[transport] order`. На
стороне клиента есть TUN-интерфейс и split-tunnel (longest-prefix matching по CIDR + правила по
доменам), которым можно управлять на лету через admin-сокет.
## Крейты
| Крейт | Что внутри |
|------------------|----------------------------------------------------------------------------|
| `aura-crypto` | Гибридный KEM (X25519 + ML-KEM-768), HKDF, AEAD ChaCha20-Poly1305, helpers |
| `aura-pki` | Самоподписанный CA, выпуск server/client-сертификатов, проверка, плоский CRL |
| `aura-proto` | Рукопожатие Aura, фрейминг, датаграмный/потоковый кодек данных |
| `aura-transport` | Транспорты: собственный UDP, TCP/443, QUIC; единый dialer с handover |
| `aura-tunnel` | TUN, маршрутизатор, split-tunnel (CIDR + домены), DNS-резолв в host-маршруты |
| `aura-cli` | Бинарь `aura`: `pki`, `server`, `client`, `route`, `status`, `bench-crypto` |
## Быстрый старт
Подъём сервера на удалённой машине и подключение клиента описаны в
[`docs/deployment.md`](docs/deployment.md). Это основная точка входа для развёртывания.
## Документация
- [`docs/deployment.md`](docs/deployment.md) — руководство по развёртыванию (сервер + клиент)
- [`docs/protocol.md`](docs/protocol.md) — wire-протокол: рукопожатие, кадры, выбор транспорта
- [`docs/pki.md`](docs/pki.md) — модель PKI, команды `aura pki`, верификация и CRL
- [`docs/split-tunnel.md`](docs/split-tunnel.md) — split-tunnel, статика и admin-сокет на лету
- [`docs/sing-box.md`](docs/sing-box.md) — план интеграции с sing-box (для мобильных клиентов)
+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 Aura использует небольшую самодостаточную X.509-PKI для **взаимной аутентификации** во внутреннем
handshake. A single self-signed Aura **CA** issues one **server** certificate and one рукопожатии. Один самоподписанный **CA** Aura выдаёт один сертификат для **сервера** и по одному
**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. сервер — сертификат клиента, и в обе стороны проверка идёт против этого CA.
The PKI is implemented in the `aura-pki` crate (`ca.rs`, `cert.rs`, `store.rs`) and exposed on PKI реализована в крейте `aura-pki` (`ca.rs`, `cert.rs`, `store.rs`) и доступна в командной строке
the command line as `aura pki ...` (`crates/aura-cli/src/pki.rs`, как `aura pki ...` (`crates/aura-cli/src/pki.rs`, `crates/aura-cli/src/main.rs`).
`crates/aura-cli/src/main.rs`).
> The outer QUIC/TLS layer does **not** use this PKI — it accepts any certificate (see > Внешний QUIC/TLS-слой эту PKI **не** использует — он принимает любой сертификат (см.
> `protocol.md`, "Mimicry layer"). All certificate trust lives in the inner Aura handshake. > `protocol.md`, раздел про слой мимикрии). Всё доверие сертификатам сосредоточено во внутреннем
> рукопожатии Aura.
--- ---
## Trust model ## Модель доверия
``` ```
Aura CA (self-signed) 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) server leaf client leaf(s)
CN = <domain> CN = <client_id> CN = <domain> CN = <client_id>
SAN: DNS:<domain> (no SAN) SAN: DNS:<domain> (нет SAN)
EKU: serverAuth EKU: clientAuth EKU: serverAuth EKU: clientAuth
``` ```
- The **CA** is self-signed with `BasicConstraints: CA`, and key usages - **CA** самоподписан, имеет `BasicConstraints: CA` (unconstrained) и `keyUsage`:
`keyCertSign` + `crlSign` + `digitalSignature`. Default lifetime **3650 days**. `keyCertSign` + `crlSign` + `digitalSignature`. Срок действия по умолчанию — **3650 дней**.
- A **server leaf** carries `CN = <domain>`, a **`DNS:<domain>` SAN**, and - **Server leaf** несёт `CN = <domain>`, **`DNS:<domain>` SAN** и
`extendedKeyUsage = serverAuth`. The DNS SAN is what the client matches against its expected `extendedKeyUsage = serverAuth`. Именно DNS-SAN сравнивается клиентом с ожидаемым `server_name`.
`server_name`. - **Client leaf** несёт `CN = <client_id>` и `extendedKeyUsage = clientAuth`. Этот CN — та
- A **client leaf** carries `CN = <client_id>` and `extendedKeyUsage = clientAuth`. The CN is идентичность, которую увидит сервер и запишет как `peer_id` сессии.
the identity the server learns and records as the session `peer_id`. - `keyUsage` для листовых сертификатов: `digitalSignature` + `keyEncipherment`. Срок действия по
- Leaf key usages are `digitalSignature` + `keyEncipherment`. Default lifetime **365 days**. умолчанию — **365 дней**.
- All issued certs (CA and leaves) backdate `not_before` by **5 minutes** to tolerate clock - У всех выпускаемых сертификатов (и у CA, и у листовых) `not_before` смещён назад на **5 минут**,
skew. чтобы выдерживать небольшой расхождение часов.
### Algorithms ### Алгоритмы
All keys are **ECDSA P-256 / SHA-256** (rcgen's default `KeyPair::generate`). Private keys are Все ключи — **ECDSA P-256 / SHA-256** (`KeyPair::generate` rcgen по умолчанию). Приватные ключи
written in **PKCS#8 PEM**. Chain verification (in `cert.rs`) accepts ECDSA P-256/SHA-256 сохраняются в **PKCS#8 PEM**. Проверка цепочки (`cert.rs`) принимает ECDSA P-256/SHA-256
(required), and also ECDSA P-384/SHA-384 and Ed25519, so a deployment can switch key types (обязательно), а также ECDSA P-384/SHA-384 и Ed25519, — так что развёртывание сможет позже сменить
later without code changes. тип ключа без изменения кода.
--- ---
## File layout ## Раскладка файлов
The CLI keeps files in plain directories. Conventional names CLI хранит файлы в обычных директориях. Стандартные имена (`crates/aura-cli/src/pki.rs`):
(`crates/aura-cli/src/pki.rs`):
| File | Constant | Contents | | Файл | Константа | Содержимое |
|---------------|------------|-------------------------------------------| |---------------|------------|--------------------------------------------------|
| `ca.crt` | `CA_CERT` | CA certificate (PEM) | | `ca.crt` | `CA_CERT` | Сертификат CA (PEM) |
| `ca.key` | `CA_KEY` | CA private key (PKCS#8 PEM) — **secret** | | `ca.key` | `CA_KEY` | Приватный ключ CA (PKCS#8 PEM) — **секрет** |
| `server.crt` | | Server leaf certificate (PEM) | | `server.crt` | | Листовой сертификат сервера (PEM) |
| `server.key` | | Server leaf private key (PEM) — **secret**| | `server.key` | | Приватный ключ сервера (PEM) — **секрет** |
| `client.crt` | | Client leaf certificate (PEM) | | `client.crt` | | Листовой сертификат клиента (PEM) |
| `client.key` | | Client leaf private key (PEM) — **secret**| | `client.key` | | Приватный ключ клиента (PEM) — **секрет** |
| `revoked.crl` | `CRL_FILE` | Revocation list (one identifier per line) | | `revoked.crl` | `CRL_FILE` | Список отозванных идентификаторов (по одному в строке) |
`issue-server` and `issue-client` load the CA from `ca.crt` + `ca.key` in the CA directory and Команды `issue-server` и `issue-client` загружают CA из `ca.crt` + `ca.key` в директории CA и
write `server.{crt,key}` / `client.{crt,key}` into the output directory. Paths beginning with записывают `server.{crt,key}` / `client.{crt,key}` в выходную директорию. Пути, начинающиеся с
`~` are expanded to the home directory (from `$HOME`, or `$USERPROFILE` on Windows). `~`, раскрываются в домашнюю директорию (из `$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`). (`ca_cert`, `cert`, `key`).
--- ---
## `aura pki` commands ## Команды `aura pki`
``` ```
aura pki init --ca-name <CN> --out <DIR> aura pki init --ca-name <CN> --out <DIR>
@@ -82,14 +81,14 @@ aura pki revoke --id <ID> [--crl <PATH>]
aura pki list [--crl <PATH>] aura pki list [--crl <PATH>]
``` ```
For `issue-server` / `issue-client`, `--ca` defaults to the value of `--out` (so the CA and Для `issue-server` / `issue-client` параметр `--ca` по умолчанию равен значению `--out` (так что CA
the issued leaf can live in the same directory). For `revoke` / `list`, `--crl` defaults to и выпущенный лист могут лежать в одной директории). Для `revoke` / `list` параметр `--crl` по
`./revoked.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 Генерирует свежий самоподписанный CA и записывает `ca.crt` + `ca.key` в `--out` (директория
directory if needed). создаётся при необходимости).
```bash ```bash
aura pki init --ca-name "Aura Root CA" --out ~/.aura 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 # 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 Выпускает листовой сертификат для DNS-имени, подписанный CA, с SAN `DNS:<domain>` и EKU
`serverAuth` EKU. `serverAuth`.
```bash ```bash
aura pki issue-server --domain vpn.example.com --out ~/.aura --ca ~/.aura 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 # key: ~/.aura/server.key
``` ```
> The `--domain` must equal the name the client expects in the handshake. In the shipped > Значение `--domain` должно совпадать с тем именем, которое клиент ожидает в рукопожатии. В
> client config that name is taken from `[client] sni`, so the camouflage SNI and the > поставляемой конфигурации клиента это имя берётся из `[client] sni`, поэтому SNI камуфляжа и
> verified server SAN are the same value. > проверяемый SAN сервера — это одно и то же значение.
### `issue-client` — issue a client certificate ### `issue-client` — выпустить сертификат клиента
Issues a client leaf with `CN = <id>` and `clientAuth` EKU. The `<id>` becomes the verified Выпускает листовой сертификат с `CN = <id>` и EKU `clientAuth`. Это `<id>` станет проверенным
`peer_id` the server sees. `peer_id`, который увидит сервер.
```bash ```bash
aura pki issue-client --id laptop --out ~/.aura --ca ~/.aura 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 # key: ~/.aura/client.key
``` ```
### `revoke` — add to the revocation list ### `revoke` — добавить в список отзыва
Adds an identifier — a **client id / Common Name** or a **certificate serial** (lowercase Добавляет идентификатор — это либо **client id / Common Name**, либо **серийный номер
hex, no separators) — to the CRL file, creating it (and parent directories) if absent. сертификата** (строчные шестнадцатеричные цифры без разделителей) — в CRL-файл, создавая его (и
родительские директории) при отсутствии.
```bash ```bash
aura pki revoke --id laptop --crl ~/.aura/revoked.crl aura pki revoke --id laptop --crl ~/.aura/revoked.crl
# revoked '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 ```bash
aura pki list --crl ~/.aura/revoked.crl aura pki list --crl ~/.aura/revoked.crl
@@ -146,87 +146,88 @@ aura pki list --crl ~/.aura/revoked.crl
# laptop # laptop
``` ```
### End-to-end example ### Полный пример
```bash ```bash
# 1. Create the CA. # 1. Создать CA.
aura pki init --ca-name "Aura Root CA" --out ~/.aura 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 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 aura pki issue-client --id laptop --out ~/.aura
# 4. (later) Revoke a compromised client. # 4. (позже) Отозвать скомпрометированный клиент.
aura pki revoke --id laptop aura pki revoke --id laptop
``` ```
--- ---
## Verification ## Проверка
Verification is performed by `AuraCertVerifier` (`crates/aura-pki/src/cert.rs`), built from Проверку выполняет `AuraCertVerifier` (`crates/aura-pki/src/cert.rs`), собранный из PEM-сертификата
the CA certificate PEM. It uses **`rustls-webpki`** to validate the peer's leaf against the CA CA. Внутри он использует **`rustls-webpki`** для валидации листового сертификата пира против CA как
trust anchor. The Aura handshake invokes it on each side (see `protocol.md`). 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 1. webpki-валидация цепочки против CA с key usage **`serverAuth`** плюс проверка срока действия
(time) check. (времени).
2. The leaf must be valid for the requested `server_name` (DNS SAN match); a mismatch is 2. Лист должен быть валиден для запрошенного `server_name` (совпадение DNS-SAN); расхождение —
`NameMismatch`. `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. 1. webpki-валидация цепочки против CA с key usage **`clientAuth`** плюс проверка срока действия.
2. The **client id** is extracted as the first Common Name from the leaf subject (missing CN 2. **client id** извлекается как первый Common Name из subject листа (отсутствие CN
is `MissingIdentity`). `MissingIdentity`).
3. CRL check. 3. Проверка по CRL.
4. Returns the client id, which the handshake records as the session `peer_id`. 4. Возвращает client id, который рукопожатие фиксирует как `peer_id` сессии.
The leaf certificate is sent **inline** in the handshake (DER, no intermediate chain); the CA Листовой сертификат передаётся **inline** в рукопожатии (DER, без промежуточной цепочки); CA — это
is the single trust anchor. Possession of the leaf's private key is proven separately by the единственный trust anchor. Владение приватным ключом листового сертификата доказывается отдельно
handshake signature over the transcript (see `protocol.md`). подписью рукопожатия по транскрипту (см. `protocol.md`).
Errors surface as `PkiError`: `CertParse`, `EmptyChain`, `TrustAnchor`, `Verification`, Ошибки сообщаются как `PkiError`: `CertParse`, `EmptyChain`, `TrustAnchor`, `Verification`,
`NameMismatch`, `MissingIdentity`, `Revoked`. `NameMismatch`, `MissingIdentity`, `Revoked`.
--- ---
## Revocation (CRL) ## Отзыв (CRL)
Aura v1 revocation is deliberately minimal (`crates/aura-pki/src/store.rs`). `CrlStore` is a Механизм отзыва в Aura v1 намеренно минимальный (`crates/aura-pki/src/store.rs`). `CrlStore` — это
**set of revoked identifier strings**, where an identifier is either: **множество строк-идентификаторов отозванных сертификатов**, где идентификатор — это либо:
- a certificate **serial number** (lowercase hex, no separators), or - **серийный номер** сертификата (строчные hex-цифры без разделителей), либо
- a **client id / Common Name**. - **client id / Common Name**.
During verification, if the CRL is non-empty the leaf is rejected (`Revoked`) when **either** При проверке, если CRL непуст, листовой сертификат отвергается (`Revoked`), когда **либо** его
its serial **or** its Common Name is present in the set. An empty CRL skips the check серийный номер, **либо** его Common Name присутствует в множестве. Пустой CRL пропускает проверку
entirely. полностью.
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 > Ограничение v1: это плоское множество разрешения/запрета, а не подписанный X.509 CRL. Нет
> signature, no `nextUpdate`, and no automatic distribution — the file must be provisioned to > подписи CRL, нет `nextUpdate` и нет автоматического распространения — файл нужно доставить на
> the verifying side out of band. The verifier passes `None` for webpki's own revocation > проверяющую сторону вне протокола. Верификатор передаёт `None` в собственные крючки отзыва
> hooks and relies solely on this set. > webpki и полагается исключительно на это множество.
--- ---
## Security notes ## Замечания по безопасности
- **Protect the private keys.** `ca.key` is the root of all trust; anyone with it can mint - **Защищайте приватные ключи.** `ca.key` — корень всего доверия; владея им, можно выпускать
valid server/client certs. `server.key` / `client.key` must stay on their respective hosts. любые валидные серверные/клиентские сертификаты. `server.key` / `client.key` должны оставаться
The CLI writes them with default file permissions — restrict them at the OS level. на своих хостах. CLI пишет их с дефолтными правами файловой системы — ограничивайте доступ
- **The CA is self-signed and unconstrained** (`BasicConstraints: CA` unconstrained). It is средствами ОС.
the sole trust anchor; there is no intermediate CA tier in v1. - **CA самоподписан и не ограничен** (`BasicConstraints: CA` unconstrained). Это единственный
- **Server identity is name-bound.** The client only accepts a server leaf whose DNS SAN trust anchor; в v1 нет уровня промежуточных CA.
matches the expected name, so a different valid leaf from the same CA will not be accepted - **Идентичность сервера связана с именем.** Клиент принимает только тот серверный лист, чей
for the wrong host. DNS-SAN совпадает с ожидаемым именем, поэтому другой валидный лист от того же CA не будет
- **Revocation is best-effort** (see above): plan to distribute the CRL file and keep it in принят для чужого хоста.
sync on every server that verifies clients. - **Отзыв — best-effort** (см. выше): планируйте раздачу CRL-файла и поддерживайте его в актуальном
- **Leaf lifetime is 365 days**; plan re-issuance. There is no automated rotation in v1. состоянии на каждом сервере, который проверяет клиентов.
- **Срок жизни листов — 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 Протокол Aura обеспечивает взаимно аутентифицированный, пост-квантово стойкий туннель между
client and a server. It is implemented in the `aura-proto` crate on top of `aura-crypto` клиентом и сервером. Он реализован в крейте `aura-proto` поверх `aura-crypto` (гибридный KEM, HKDF,
(hybrid KEM, HKDF, AEAD) and `aura-pki` (mutual X.509 verification). 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`): (`crates/aura-proto/src/frame.rs`):
``` ```
byte 0 : msg_type (u8) байт 0 : msg_type (u8)
bytes 1..4 : length (u24, big-endian) = payload length in bytes байты 1..4 : length (u24, big-endian) = длина полезной нагрузки в байтах
byte 4 : version = 0x01 байт 4 : version = 0x01
bytes 5.. : payload (length bytes) байты 5.. : payload (length байт)
``` ```
- `length` is a 24-bit big-endian integer, so the maximum payload is `0x00FF_FFFF` - `length` — это 24-битное целое big-endian, так что максимальная полезная нагрузка —
(16 MiB 1). An oversize payload is rejected with `FrameTooLarge`. `0x00FF_FFFF` (16 МиБ 1). Слишком большая нагрузка отвергается с `FrameTooLarge`.
- `version` is `0x01`. A header whose byte 4 is not `0x01` is rejected with `BadVersion`. - `version` равна `0x01`. Заголовок, у которого байт 4 не `0x01`, отвергается с `BadVersion`.
### Message types ### Типы сообщений
| Byte | `MsgType` | Direction | Encrypted | Role | | Байт | `MsgType` | Направление | Шифрование | Роль |
|--------|---------------|-----------|-----------|--------------------------------------------| |--------|---------------|-------------|------------|--------------------------------------------|
| `0x01` | `ClientHello` | C→S | no | Handshake 1: hybrid public key + nonce | | `0x01` | `ClientHello` | C→S | нет | Рукопожатие 1: гибридный публичный ключ + nonce |
| `0x02` | `ServerHello` | S→C | no | Handshake 2: hybrid ciphertext + nonce | | `0x02` | `ServerHello` | S→C | нет | Рукопожатие 2: гибридный шифртекст + nonce |
| `0x03` | `ClientAuth` | C→S | yes | Handshake 4: client cert + signature | | `0x03` | `ClientAuth` | C→S | да | Рукопожатие 4: сертификат клиента + подпись |
| `0x04` | `ServerAuth` | S→C | yes | Handshake 3: server cert + signature | | `0x04` | `ServerAuth` | S→C | да | Рукопожатие 3: сертификат сервера + подпись |
| `0x05` | `Finished` | both | yes | Handshake 5/6: HMAC over the transcript | | `0x05` | `Finished` | оба | да | Рукопожатие 5/6: HMAC по transcript |
| `0x06` | `Data` | both | yes | Application record (AEAD-sealed `Frame`) | | `0x06` | `Data` | оба | да | Прикладная запись (AEAD-запечатанный `Frame`) |
| `0xFF` | `Alert` | both | no | Fatal alert; payload byte 0 is the code | | `0xFF` | `Alert` | оба | нет | Фатальный alert; байт 0 нагрузки — код |
> Note: the numeric byte values do **not** follow the send order. `ServerAuth` (`0x04`) is > Замечание: числовые значения байтов **не** соответствуют порядку отправки. `ServerAuth` (`0x04`)
> sent *before* `ClientAuth` (`0x03`). The send order is fixed by the state machine > отправляется *перед* `ClientAuth` (`0x03`). Порядок отправки задаётся конечным автоматом (ниже), а
> (below), not by the type byte. > не байтом-типом.
### Application frames ### Прикладные кадры
Once the session is established, the application payload carried inside each encrypted `Data` Когда сессия установлена, прикладная нагрузка внутри каждой зашифрованной записи `Data` — это `Frame`
record is a `Frame` (`crates/aura-proto/src/frame.rs`). All multi-byte integers are (`crates/aura-proto/src/frame.rs`). Все многобайтные целые — big-endian:
big-endian:
| Frame | Tag | Encoding | | Кадр | Тег | Кодирование |
|---------|--------|-----------------------------------------------------| |---------|--------|------------------------------------------------------|
| `Data` | `0x01` | `0x01 \|\| stream_id(u32) \|\| payload` | | `Data` | `0x01` | `0x01 \|\| stream_id(u32) \|\| payload` |
| `Ping` | `0x02` | `0x02 \|\| seq(u32)` | | `Ping` | `0x02` | `0x02 \|\| seq(u32)` |
| `Pong` | `0x03` | `0x03 \|\| seq(u32)` | | `Pong` | `0x03` | `0x03 \|\| seq(u32)` |
| `Close` | `0x04` | `0x04 \|\| code(u8) \|\| reason_len(u32) \|\| reason_utf8` | | `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`): (`crates/aura-proto/src/handshake.rs`):
``` ```
1. C -> S ClientHello (plaintext): x25519_pub[32] || mlkem_ek[1184] || client_nonce[32] 1. C -> S ClientHello (открытый текст): x25519_pub[32] || mlkem_ek[1184] || client_nonce[32]
2. S -> C ServerHello (plaintext): x25519_ephemeral[32] || mlkem_ct[1088] || server_nonce[32] 2. S -> C ServerHello (открытый текст): x25519_ephemeral[32] || mlkem_ct[1088] || server_nonce[32]
-- both sides derive the hybrid shared secret and the two directional SessionKeys -- -- обе стороны выводят гибридный общий секрет и два направленных SessionKeys --
3. S -> C ServerAuth (encrypted under s2c): u16(cert_der_len) || server_leaf_cert_der || sig(transcript) 3. S -> C ServerAuth (зашифровано под 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) 4. C -> S ClientAuth (зашифровано под c2s): u16(cert_der_len) || client_leaf_cert_der || sig(transcript)
5. C -> S Finished (encrypted under c2s): HMAC-SHA256(key_c2s, transcript) 5. C -> S Finished (зашифровано под c2s): HMAC-SHA256(key_c2s, transcript)
6. S -> C Finished (encrypted under s2c): HMAC-SHA256(key_s2c, transcript) 6. S -> C Finished (зашифровано под s2c): HMAC-SHA256(key_s2c, transcript)
-- encrypted Data channel is now open in both directions -- -- зашифрованный канал Data теперь открыт в обе стороны --
``` ```
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant C as Client participant C as Клиент
participant S as Server participant S as Сервер
Note over C,S: plaintext Note over C,S: открытый текст
C->>S: 1. ClientHello (x25519_pub, mlkem_ek, client_nonce) C->>S: 1. ClientHello (x25519_pub, mlkem_ek, client_nonce)
S->>C: 2. ServerHello (x25519_eph, mlkem_ct, server_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: обе стороны выводят общий секрет + SessionKeys<br/>transcript = SHA-256(CH_frame || SH_frame)
Note over C,S: encrypted (AEAD under directional keys) Note over C,S: зашифровано (AEAD под направленными ключами)
S->>C: 3. ServerAuth (server cert + sig over transcript) S->>C: 3. ServerAuth (сертификат сервера + подпись по transcript)
C->>S: 4. ClientAuth (client cert + sig over transcript) C->>S: 4. ClientAuth (сертификат клиента + подпись по transcript)
C->>S: 5. Finished (HMAC_c2s over transcript) C->>S: 5. Finished (HMAC_c2s по transcript)
S->>C: 6. Finished (HMAC_s2c over transcript) S->>C: 6. Finished (HMAC_s2c по transcript)
Note over C,S: session established; Data records flow both ways Note over C,S: сессия установлена; записи Data идут в обе стороны
``` ```
### Hello payloads (exact sizes) ### Полезные нагрузки Hello (точные размеры)
| Field | ClientHello | ServerHello | Bytes | | Поле | ClientHello | ServerHello | Байт |
|-------------------|:-----------:|:-----------:|------:| |-------------------|:-----------:|:-----------:|-----:|
| X25519 pub / eph | ✔ | ✔ | 32 | | X25519 pub / eph | ✔ | ✔ | 32 |
| ML-KEM-768 ek | ✔ | | 1184 | | ML-KEM-768 ek | ✔ | | 1184 |
| ML-KEM-768 ct | | ✔ | 1088 | | ML-KEM-768 ct | | ✔ | 1088 |
| nonce | ✔ | ✔ | 32 | | nonce | ✔ | ✔ | 32 |
| **Total payload** | **1248** | **1152** | | | **Итого нагрузка**| **1248** | **1152** | |
Hellos are sent in plaintext and validated for exact length on receipt; a wrong length is Hello отправляются в открытом виде и при приёме проверяются на точную длину; неверная длина
rejected with `MalformedHandshake`. отвергается с `MalformedHandshake`.
### Transcript hash ### Transcript-хеш
``` ```
transcript = SHA-256( ClientHello_frame_bytes || ServerHello_frame_bytes ) transcript = SHA-256( ClientHello_frame_bytes || ServerHello_frame_bytes )
``` ```
The hash covers the **full serialized frames** (5-byte header + payload) of ClientHello and Хеш покрывает **полные сериализованные кадры** (5-байтный заголовок + полезная нагрузка) ClientHello
ServerHello, exactly as transmitted on the wire. This binds the negotiated key material and и ServerHello, ровно как они передаются на проводе. Это привязывает согласованный ключевой материал и
the protocol version into both the signatures and the Finished MACs. версию протокола одновременно к подписям и к MAC-ам Finished.
### Authentication (ServerAuth / ClientAuth) ### Аутентификация (ServerAuth / ClientAuth)
Each Auth payload is: Каждая нагрузка Auth:
``` ```
u16_be(cert_der_len) || leaf_cert_der || signature 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 - `leaf_cert_der` — это **листовой сертификат** отправителя в DER (передаётся встроенным, без цепочки —
CA is the trust anchor on the receiving side). CA является якорем доверия на принимающей стороне).
- `signature` is an **ECDSA P-256 / SHA-256** signature, ASN.1 DER encoded - `signature` — это подпись **ECDSA P-256 / SHA-256**, в кодировке ASN.1 DER
(`ECDSA_P256_SHA256_ASN1`), computed over the 32-byte `transcript` (via `ring`). (`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 1. Принимающая сторона строит `AuraCertVerifier` из настроенного PEM своего CA и проверяет листовой
peer's leaf against the CA (chain + key-usage + validity; see `pki.md`). сертификат пира против CA (цепочка + назначение ключа + срок действия; см. `pki.md`).
- The **client** additionally requires the server leaf to be valid for the expected - **Клиент** дополнительно требует, чтобы листовой сертификат сервера был валиден для ожидаемого
`server_name` (DNS SAN match). `server_name` (совпадение DNS SAN).
- The **server** captures the verified **client id** (leaf Common Name) and stores it as - **Сервер** захватывает проверенный **id клиента** (Common Name листа) и сохраняет его как
the session's `peer_id`. `peer_id` сессии.
2. The receiver extracts the leaf's EC public-key point and verifies `signature` over 2. Принимающая сторона извлекает точку EC-публичного ключа из листа и проверяет `signature` по
`transcript`. A failure is `Signature(...)`. `transcript`. Неуспех — это `Signature(...)`.
Possession of the certificate's private key is therefore proven by the signature over the Таким образом, владение приватным ключом сертификата доказывается подписью по transcript, а личность
transcript; the certificate identity is proven by the CA chain check. сертификата — проверкой цепочки против CA.
### Finished ### 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_c2s = HMAC-SHA256(key_c2s, transcript) // отправляет клиент (сообщение 5), проверяет сервер
Finished_s2c = HMAC-SHA256(key_s2c, transcript) // server sends (msg 6), client verifies Finished_s2c = HMAC-SHA256(key_s2c, transcript) // отправляет сервер (сообщение 6), проверяет клиент
``` ```
Verification is constant-time (`Hmac::verify_slice`); a mismatch is `FinishedMismatch`. The Проверка выполняется за постоянное время (`Hmac::verify_slice`); несовпадение — это
Finished exchange confirms both sides derived identical keys and agree on the full transcript. `FinishedMismatch`. Обмен Finished подтверждает, что обе стороны вывели одинаковые ключи и согласны по
всему transcript.
### Encrypted handshake messages and counter continuity ### Зашифрованные сообщения рукопожатия и непрерывность счётчиков
Messages 36 are AEAD-sealed under the **same** two directional `AeadSession`s that protect Сообщения 3–6 запечатываются AEAD под **теми же** двумя направленными `AeadSession`, что защищают
application Data; their nonce counters are continuous across the handshake/data boundary. прикладные Data; их счётчики nonce непрерывны через границу рукопожатие/данные.
- The AAD for each encrypted handshake message is its 5-byte frame header (binding type + - AAD каждого зашифрованного сообщения рукопожатия — это его 5-байтный заголовок кадра (привязка типа
length), matching the Data-record convention. + длины), как и в записях Data.
- Each direction seals **exactly two** encrypted handshake messages before Data begins: - Каждое направление запечатывает **ровно два** зашифрованных сообщения рукопожатия до начала Data:
- c2s seals `ClientAuth` (counter 0) and `Finished` (counter 1) - c2s запечатывает `ClientAuth` (счётчик 0) и `Finished` (счётчик 1)
- s2c seals `ServerAuth` (counter 0) and `Finished` (counter 1) - s2c запечатывает `ServerAuth` (счётчик 0) и `Finished` (счётчик 1)
- Therefore both directions reach AEAD counter **2** at the end of the handshake, and the - Поэтому оба направления достигают AEAD-счётчика **2** в конце рукопожатия, и первая прикладная
first application Data record stamps `seq == 2` (`POST_HANDSHAKE_COUNTER`). This seeds the запись Data ставит `seq == 2` (`POST_HANDSHAKE_COUNTER`). Это задаёт начальное состояние
replay window (below). 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 Обмен ключами — гибрид классического X25519 ECDH и пост-квантового ML-KEM-768
(`crates/aura-crypto/src/kem/`). An attacker must break **both** primitives to recover the (`crates/aura-crypto/src/kem/`). Атакующему нужно сломать **оба** примитива, чтобы восстановить ключ
session key. сессии.
> **ML-KEM-768 (FIPS 203)**, via the RustCrypto `ml-kem` crate (v0.3) — this is the > **ML-KEM-768 (FIPS 203)** через крейт RustCrypto `ml-kem` (v0.3) — это стандартизованная схема
> standardized FIPS 203 scheme, **not** round-3 Kyber. > FIPS 203, а **не** Kyber раунда 3.
### Roles ### Роли
- The **client** owns the long-term `HybridPrivateKey` and publishes its `HybridPublicKey` - **Клиент** владеет долговременным `HybridPrivateKey` и публикует свой `HybridPublicKey` в
in ClientHello. ClientHello.
- The **server** calls `encapsulate()` against that public key: it generates an **ephemeral** - **Сервер** вызывает `encapsulate()` против этого публичного ключа: он генерирует **эфемерную** пару
X25519 keypair and an ML-KEM encapsulation, returns the `HybridCiphertext` in ServerHello, X25519 и ML-KEM-инкапсуляцию, возвращает `HybridCiphertext` в ServerHello и выводит общий секрет.
and derives the shared secret. - **Клиент** восстанавливает тот же секрет через `decapsulate()`.
- The **client** recovers the same secret via `decapsulate()`.
So X25519 is **ephemeralstatic** (server ephemeral against client static public), while То есть X25519 здесь **эфемерно–статический** (эфемерный сервера против статического публичного
ML-KEM is a standard KEM against the client's encapsulation key. клиента), а ML-KEM — это стандартный KEM против инкапсуляционного ключа клиента.
### Sizes ### Размеры
| Quantity | Bytes | Constant | | Величина | Байт | Константа |
|-----------------------------------|------:|---------------------| |-----------------------------------|------:|---------------------|
| X25519 public / ephemeral / secret| 32 | `X25519_LEN` | | X25519 публичный / эфемерный / секрет | 32 | `X25519_LEN` |
| ML-KEM-768 encapsulation key (ek) | 1184 | `EK_LEN` | | ML-KEM-768 инкапсуляционный ключ (ek) | 1184 | `EK_LEN` |
| ML-KEM-768 ciphertext (ct) | 1088 | `CT_LEN` | | ML-KEM-768 шифртекст (ct) | 1088 | `CT_LEN` |
| ML-KEM-768 shared secret | 32 | `SS_LEN` | | ML-KEM-768 общий секрет | 32 | `SS_LEN` |
| ML-KEM-768 decapsulation key (dk) | 2400 | `DK_LEN` | | ML-KEM-768 деинкапсуляционный ключ (dk) | 2400 | `DK_LEN` |
> **Implementation detail — dk encoding.** The decapsulation (secret) key is stored in the > **Деталь реализации — кодирование dk.** Деинкапсуляционный (секретный) ключ хранится в
> FIPS 203 **expanded 2400-byte** form (`ExpandedKeyEncoding`), not the 64-byte seed that > **развёрнутой 2400-байтной** форме FIPS 203 (`ExpandedKeyEncoding`), а не в 64-байтном seed,
> `ml-kem` 0.3 prefers. This is the encoding the project's ACVP / FIPS-203 known-answer test > который предпочитает `ml-kem` 0.3. Именно с этим кодированием работают KAT-векторы ACVP / FIPS 203
> vectors operate on, so it is used for interop/KAT compatibility. The dk never travels on the > проекта, поэтому оно используется для совместимости/interop. dk никогда не путешествует по проводу —
> wire — only `ek` (1184 B) and `ct` (1088 B) do. > по нему идут только `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 Деинкапсуляция ML-KEM не может завершиться ошибкой на корректно размерном шифртексте: подделанный
yields a pseudo-random secret (implicit rejection) rather than an error, which surfaces later шифртекст даёт псевдослучайный секрет (неявное отклонение), а не ошибку, что позже всплывает как сбой
as an AEAD/Finished failure. 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`): (`crates/aura-crypto/src/kdf.rs`):
``` ```
salt = client_nonce || server_nonce (64 bytes) salt = client_nonce || server_nonce (64 байта)
IKM = x25519_ss || mlkem_ss (64 bytes) IKM = x25519_ss || mlkem_ss (64 байта)
info = "aura-v1-session" 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_client_to_server = OKM[0..32]
key_server_to_client = OKM[32..64] key_server_to_client = OKM[32..64]
``` ```
The derivation is fully deterministic in its inputs. The `info` string provides domain Вывод полностью детерминирован по своим входам. Строка `info` обеспечивает доменное разделение.
separation. Intermediate secret material (`salt`, `IKM`, `OKM`) is zeroized after use, and Промежуточный секретный материал (`salt`, `IKM`, `OKM`) обнуляется после использования, а
`SessionKeys` zeroizes its keys on drop. `SessionKeys` обнуляет свои ключи при drop.
--- ---
## AEAD ## AEAD
The record cipher is **ChaCha20-Poly1305** (`crates/aura-crypto/src/aead.rs`). An Шифр записей — **ChaCha20-Poly1305** (`crates/aura-crypto/src/aead.rs`). Есть два режима:
`AeadSession` holds a 256-bit key and a 64-bit message counter; each direction has its own
session.
### 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 nonce[8..12] = 0x00 00 00 00
``` ```
The counter advances by one on every `seal` **and** every `open` (even on a failed `open`), В потоковом режиме nonce никогда не переиспользуется в рамках сессии (переполнение счётчика 2^64
so a paired seal/open stay aligned without transmitting the nonce. The nonce is never reused недостижимо; при переполнении происходит паника, а не повторное использование nonce). В датаграммном
within a session (the 2^64 counter wrap is unreachable; an overflow panics rather than режиме за уникальность счётчика отвечает отправитель (`DatagramSender` монотонно увеличивает `seq`).
reusing a nonce). The key is zeroized on drop. Ключ обнуляется при drop.
--- ---
## Data records and replay protection ## Записи данных и защита от повтора (replay)
After the handshake, application `Frame`s are exchanged as `Data` records После рукопожатия прикладные `Frame` передаются как записи `Data`
(`crates/aura-proto/src/session.rs`). Each `Data` record's **payload** is: (`crates/aura-proto/src/session.rs`). Кодирование зависит от того, потоковый транспорт или
датаграммный — но логика replay-окна общая.
### Потоковый путь (TCP / QUIC)
**Полезная нагрузка** каждой записи `Data`:
``` ```
seq (u64, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = header || seq ) 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 - `seq` — 8-байтный big-endian счётчик записи. На «счастливом пути» он равен счётчику запечатывающего
AEAD's counter (and the receiver's expected AEAD counter). AEAD (и ожидаемому счётчику AEAD приёмника).
- The AEAD **AAD** is the 5-byte frame `header` concatenated with the 8-byte `seq`, so the - **AAD** AEAD — это 5-байтный заголовок кадра `header`, сцепленный с 8-байтным `seq`, так что запись
record is cryptographically bound to both its declared length/type and its claimed position. криптографически привязана и к своей объявленной длине/типу, и к заявленной позиции.
- The ciphertext includes the 16-byte Poly1305 tag. - Шифртекст включает 16-байтный тег Poly1305.
So the full record on the wire is: Полная запись на проводе:
``` ```
[ header(5) ][ seq(8) ][ ciphertext + tag ] [ header(5) ][ seq(8) ][ ciphertext + tag ]
@@ -316,56 +438,74 @@ So the full record on the wire is:
header.length = 8 + len(ciphertext+tag) header.length = 8 + len(ciphertext+tag)
``` ```
### Sliding replay window ### Датаграммный путь (свой UDP)
The receiver runs a **64-wide sliding-window** replay check (`REPLAY_WINDOW = 64`) *before* Здесь нет 5-байтного потокового заголовка внутри записи. Датаграммная запись
touching the AEAD, so a duplicate or too-old record is rejected with `Replay(seq)` without (`DatagramSender::seal`):
disturbing the AEAD counter (the session stays usable). The window:
- 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 seq(8, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = seq )
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.
The window is seeded at the post-handshake counter (`start = 2`): everything strictly below То есть AAD — это **только** `seq` (а не `header || seq`). На проводе эта запись несётся внутри
`start` is treated as already-consumed, so the first legitimate Data record (`seq == 2`) is DATA-датаграммы UDP-транспорта как `0x02 || rec_len(u16 BE) || запись [|| паддинг]` (см. раздел про
accepted as "newer". транспорт v2 выше).
### Full-duplex split ### Скользящее replay-окно
A `Session` can be `split()` into independent `SessionSender` (writer + outbound AEAD + Приёмник запускает проверку повтора **скользящим окном шириной 64** (`REPLAY_WINDOW = 64`) *до*
send counter) and `SessionReceiver` (reader + inbound AEAD + replay window) halves, which can обращения к AEAD, так что дубликат или слишком старая запись отвергаются с `Replay(seq)`, не трогая
be driven from separate tasks for a concurrent read/write data path (e.g. the VPN tunnel). счётчик AEAD (сессия остаётся работоспособной). Окно:
`recv_frame` is **not** cancellation-safe and must be driven from a single owning task.
- отслеживает наибольший принятый `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 Внешний слой QUIC/TLS (`crates/aura-transport/`) существует исключительно для маскировки соединения
as browser HTTP/3 traffic. It is explicitly **not** the authentication boundary. под трафик браузера HTTP/3. Он явно **не** является границей аутентификации.
- **ALPN** advertises `h3` and `h3-29` (`ALPN_H3`) — exactly what Chrome offers for HTTP/3 — - **ALPN** объявляет `h3` и `h3-29` (`ALPN_H3`) — ровно то, что предлагает Chrome для HTTP/3 —
so the ALPN extension is indistinguishable from a real browser's. поэтому расширение ALPN неотличимо от реального браузерного.
- **Transport params** mirror a Chromium HTTP/3 connection: ~30 s idle timeout, ~15 s - **Параметры транспорта** зеркалят соединение Chromium HTTP/3: ~30 с idle-таймаут, ~15 с
keep-alive, 100 concurrent bidi/uni streams, ~10 MB flow-control receive windows keep-alive, 100 конкурентных bidi/uni-потоков, ~10 МБ окна управления потоком на приём
(`chrome_quic_transport_config`). (`chrome_quic_transport_config`).
- **SNI** defaults to a generic CDN-looking hostname (`cdn.example.com`) when the caller does - **SNI** по умолчанию — обобщённое CDN-подобное имя (`cdn.example.com`), если вызывающий его не задал;
not supply one; deployments pass their own camouflage hostname. развёртывания передают своё камуфляжное имя.
- The QUIC **client accepts any server certificate** (`AcceptAnyServerCert`all verifier - QUIC-**клиент принимает любой серверный сертификат** (`AcceptAnyServerCert`все методы
methods return success). This is safe *only* because the outer TLS is not authentication: верификатора возвращают успех). Это безопасно *только* потому, что внешний TLS не является
the real mutual auth is the inner Aura handshake. The server's outer TLS likewise disables аутентификацией: настоящая взаимная аутентификация — это внутреннее рукопожатие Aura. Внешний TLS
client auth (`with_no_client_auth`). сервера также отключает клиентскую аутентификацию (`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`, `Io`, `Crypto`, `Pki`, `UnknownMsgType`, `BadVersion`, `FrameTooLarge`, `UnexpectedMsg`,
`MalformedHandshake`, `MalformedFrame`, `Signature`, `FinishedMismatch`, `Replay`, and `MalformedHandshake`, `MalformedFrame`, `Signature`, `FinishedMismatch`, `Replay` и `Alert`. Пир может
`Alert`. A peer may send a fatal `Alert` frame (type `0xFF`); the first payload byte is the отправить фатальный кадр `Alert` (тип `0xFF`); первый байт полезной нагрузки — код alert, который
alert code, surfaced to the local side as `ProtoError::Alert(code)`. всплывает на локальной стороне как `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** Цель: дать клиенту на телефоне, работающему под **sing-box**, возможность подключиться к серверу
protocol (Aura's own tunneling — not a third party's). This is a short note on *how*; the wire Aura по протоколу **AuraVPN** (это собственное туннелирование Aura, не сторонний протокол). Это
protocol it must match is fully specified in [`protocol.md`](protocol.md). короткая заметка о том, *как* это сделать; сам wire-протокол, который должна повторять реализация,
полностью описан в [`protocol.md`](protocol.md).
sing-box is written in Go and has no generic "load an arbitrary external wire protocol" plugin, so sing-box написан на Go, и в нём нет универсального плагина «загрузить произвольный внешний wire-
integration means giving sing-box a Go implementation (or a bridge) of the Aura protocol. Three протокол», поэтому интеграция означает дать sing-box Go-реализацию (или мост) протокола Aura. Три
realistic paths, cheapest first: реалистичных пути, от самого дешёвого:
## 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 Запустить существующий Rust-клиент `aura` как локальный процесс и выставить локальный
sing-box at it: прокси/TUN, а затем направить sing-box на него:
- `aura client` already creates a TUN and routes through the Aura tunnel; sing-box can route selected - `aura client` уже создаёт TUN и направляет туда трафик через туннель Aura; sing-box может
traffic into that interface, **or** заворачивать выбранный трафик в этот интерфейс, **или**
- add a local **SOCKS5 inbound** to `aura-cli` (small addition) and configure a sing-box `socks` - добавить в `aura-cli` локальный **SOCKS5 inbound** (небольшое дополнение) и настроить в
outbound pointing at `127.0.0.1:<port>`. sing-box `socks` outbound на `127.0.0.1:<port>`.
Pros: reuses the audited Rust core verbatim; no crypto re-implementation. Cons: two processes; weak Плюсы: переиспользует прошедшую аудит Rust-сердцевину дословно; нет переписывания криптографии.
fit for mobile (sing-box mobile apps embed the core and don't spawn helpers easily). Минусы: два процесса; плохо ложится на мобильные платформы (мобильные приложения 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 Реализовать протокол AuraVPN на Go нативно и зарегистрировать его как sing-box **outbound**
**inbound** (server), so the phone's embedded sing-box core speaks AuraVPN directly. This is the (клиент) и **inbound** (сервер), чтобы встроенное в телефон ядро sing-box само говорило на
clean, performant, mobile-friendly path. Crypto maps cleanly to existing Go libraries: AuraVPN. Это чистый, производительный и mobile-friendly путь. Криптография чисто ложится на
существующие Go-библиотеки:
| Aura piece | Rust crate | Go equivalent | | Компонент Aura | Rust-крейт | Эквивалент в Go |
|---|---|---| |--------------------------------|---------------------|---------------------------------------------------------|
| X25519 ECDH | `x25519-dalek` | `crypto/ecdh` (stdlib) | | X25519 ECDH | `x25519-dalek` | `crypto/ecdh` (stdlib) |
| ML-KEM-768 (FIPS 203) | `ml-kem` | `crypto/mlkem` (Go 1.24+) or `cloudflare/circl` | | ML-KEM-768 (FIPS 203) | `ml-kem` | `crypto/mlkem` (Go 1.24+) или `cloudflare/circl` |
| ChaCha20-Poly1305 | `chacha20poly1305` | `golang.org/x/crypto/chacha20poly1305` | | ChaCha20-Poly1305 | `chacha20poly1305` | `golang.org/x/crypto/chacha20poly1305` |
| HKDF-SHA256 | `hkdf` | `golang.org/x/crypto/hkdf` | | HKDF-SHA256 | `hkdf` | `golang.org/x/crypto/hkdf` |
| HMAC-SHA256 (Finished) | `hmac` | `crypto/hmac` + `crypto/sha256` | | HMAC-SHA256 (Finished) | `hmac` | `crypto/hmac` + `crypto/sha256` |
| ECDSA P-256 sigs (cert auth) | `ring` | `crypto/ecdsa` + `crypto/x509` | | ECDSA P-256 signatures (cert) | `ring` | `crypto/ecdsa` + `crypto/x509` |
| X.509 verify + CRL | `rustls-webpki` | `crypto/x509` | | X.509 verify + CRL | `rustls-webpki` | `crypto/x509` |
What the Go code must reproduce **exactly** (see `protocol.md`): Что Go-код должен повторить **в точности** (см. `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 = - 5-байтный заголовок кадра: `msg_type(1) || len(u24 BE) || version=0x01`.
`SHA-256(ClientHello_frame || ServerHello_frame)`; ECDSA-P256/SHA-256 signature over the transcript; - Порядок рукопожатия `CH → SH → ServerAuth → ClientAuth → Finished(c→s) → Finished(s→c)`;
HMAC-SHA256 Finished. транскрипт = `SHA-256(ClientHello_frame || ServerHello_frame)`; подпись ECDSA-P256/SHA-256 по
- Hybrid shared secret = `x25519_ss || mlkem_ss`; HKDF salt = `client_nonce || server_nonce`, транскрипту; Finished — HMAC-SHA256.
- Гибридный общий секрет = `x25519_ss || mlkem_ss`; salt HKDF = `client_nonce || server_nonce`,
info = `b"aura-v1-session"`. info = `b"aura-v1-session"`.
- Data record (datagram/UDP) = `seq(8 BE) || ChaCha20Poly1305(frame, aad = seq)`, nonce = - Запись данных (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.) `LE(seq) || 0x00000000`; окно anti-replay 64. (Stream/TCP-запись дополнительно включает
- Transport selection: UDP (type `0x01` HS / `0x02` DATA) primary; TCP/443 and QUIC fallbacks. 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 Чтобы снизить риск порта в Go, экспортируйте со стороны Rust **known-answer test vectors**
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-реализация воспроизводит их побайтово. 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 Скомпилировать Rust-сердцевину Aura в C-ABI shared library и вызывать её из тонкого Go-shim'а
cgo. Reuses the audited crypto/handshake with no Go re-implementation, but cgo + per-platform sing-box через cgo. Переиспользует прошедшую аудит крипто/рукопожатие без Go-переписывания, но
(Android/iOS) packaging is fiddly and complicates sing-box's pure-Go build. cgo плюс упаковка под каждую платформу (Android/iOS) — занудно и усложняет чисто-Go-сборку
sing-box.
## Recommendation ## Рекомендация
- **Now:** Option A (process bridge) for desktop/server validation — minimal work, real protocol. - **Сейчас:** Option A (process bridge) для валидации на десктопе/сервере — минимум работы,
- **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. - **Для телефона:** Option B (нативный Go-outbound), написанный по `protocol.md` + по
- Keep `protocol.md` the single source of truth and version the wire protocol (the header already экспортированным из Rust тест-векторам. Это единственный вариант, который хорошо ложится на
carries `version = 0x01`) so the Rust and Go implementations stay in lockstep. встроенное мобильное ядро 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 > Статус: это проектная заметка. Go-кода и sing-box-модуля пока **нет** — это отдельный
> separate deliverable tracked for after the Rust transport stabilizes. > 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 Split-tunneling решает для каждого назначения IP, идёт ли пакет **через шифрованный VPN** или
VPN** or **egresses directly** (bypassing the tunnel). It lets you keep, say, RFC1918 LAN **уходит напрямую** (минуя туннель). Это позволяет, например, оставить трафик к RFC1918-сетям
traffic local while sending the rest through Aura — or the reverse. локальным, а остальное пустить через Aura — или наоборот.
It is implemented in the `aura-tunnel` crate (`routes.rs`, `router.rs`, `dns.rs`), configured Реализация лежит в крейте `aura-tunnel` (`routes.rs`, `router.rs`, `dns.rs`), статически
statically via the `[tunnel.split]` section of `client.toml` настраивается секцией `[tunnel.split]` в `client.toml` (`crates/aura-cli/src/config.rs`), а
(`crates/aura-cli/src/config.rs`), and managed live via the `aura route` / `aura status` управляется на лету командами `aura route` / `aura status` через admin-сокет
admin commands (`crates/aura-cli/src/admin.rs`). (`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`): (`RouteAction`):
- **`Vpn`** — encrypt and send the packet over the Aura connection to the server. - **`Vpn`** — зашифровать и отправить пакет по соединению Aura на сервер.
- **`Direct`** — let the packet egress directly, bypassing the tunnel. - **`Direct`** — выпустить пакет напрямую, минуя туннель.
The router (`AuraRouter::run`, `router.rs`) parses each packet's destination IP, classifies Маршрутизатор (`AuraRouter::run`, `router.rs`) парсит у каждого пакета IP назначения,
it, and dispatches: классифицирует его и диспетчеризует:
``` ```
TUN read --> parse dst IP --> RouteTable.classify(dst) --> Vpn? -> conn.send_packet() 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 > **Ограничение v1 — `Direct` это заглушка.** Текущая реализация `send_direct` **логирует и
> packet; real raw-socket / OS-stack re-injection is out of scope for v1. The method is > отбрасывает** пакет; реальный raw-socket / реинъекция в стек ОС в объём v1 не входят. Метод уже
> already `async` and fallible so a real egress path can slot in without changing call sites. > объявлен `async` и возвращает `Result`, чтобы реальный путь egress подключился без изменения
> The VPN path is fully functional end-to-end. Packets whose destination cannot be parsed > вызывающего кода. Путь через VPN полностью работоспособен сквозным образом. Пакеты, у которых
> (not IPv4/IPv6, or too short) are dropped with a trace. > не получилось разобрать назначение (не IPv4/IPv6 или слишком короткие), отбрасываются с trace-
> сообщением.
The inbound direction is straightforward: decrypted IP packets received from the peer are Входящее направление прямолинейно: расшифрованные IP-пакеты, полученные от пира, пишутся обратно
written back to the TUN device. на TUN-устройство.
--- ---
## Rules ## Правила
The routing table (`RouteTable`, `routes.rs`) holds three things: a set of **CIDR rules**, a Таблица маршрутизации (`RouteTable`, `routes.rs`) хранит три вещи: набор **CIDR-правил**, набор
set of **domain rules**, and a **default action**. **доменных правил** и **действие по умолчанию**.
### CIDR rules ### CIDR-правила
A CIDR rule is an `IpNetwork` (e.g. `10.0.0.0/8`) plus an action. CIDR rules are keyed by CIDR-правило — это `IpNetwork` (например `10.0.0.0/8`) плюс действие. CIDR-правила
network, so re-adding the same network **overwrites** its action. ключуются сетью, поэтому повторное добавление той же сети **перезаписывает** её действие.
### Domain rules ### Доменные правила
A domain rule is a domain name plus an action. Domains do **not** match IPs directly. Instead Доменное правило — это доменное имя плюс действие. Домены **не** сопоставляются с IP напрямую.
`AuraDns` (`dns.rs`) resolves the domain via the system resolver (hickory) and inserts each Вместо этого `AuraDns` (`dns.rs`) резолвит домен через системный резолвер (hickory) и вставляет
resulting address as a **host route**`/32` for IPv4, `/128` for IPv6 — so it participates каждый получившийся адрес как **host-маршрут**`/32` для IPv4, `/128` для IPv6,так что они
in the normal longest-prefix match. Resolution results are cached. участвуют в обычном longest-prefix matching. Результаты резолва кэшируются.
> Because domain rules become host routes at resolution time, they only take effect once the > Поскольку доменные правила становятся host-маршрутами в момент резолва, они действуют только
> 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. > увиденные в момент резолва, и не перерезолвятся непрерывно в v1.
### Default action ### Действие по умолчанию
If no CIDR rule (including resolved domain host routes) matches a destination, the table's Если ни одно CIDR-правило (включая host-маршруты от резолва доменов) не совпало с назначением,
**default action** applies. применяется **действие по умолчанию** таблицы.
--- ---
## 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 > Среди всех CIDR-правил, чьи сети содержат назначение, побеждает правило с **наибольшей длиной
> prefix length** (most specific) wins. If no rule matches, the default action is returned. > префикса** (наиболее специфичное). Если ни одно правило не совпало, возвращается действие по
> умолчанию.
This lets a specific range override a broader one regardless of insertion order. IPv4 rules Так специфичный диапазон может перекрыть более широкий независимо от порядка вставки. IPv4-правила
only match IPv4 destinations and IPv6 rules only match IPv6 destinations. совпадают только с 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`: `10.7.0.0/24 = Vpn`:
| Destination | Matched rule | Action | | Назначение | Сработавшее правило | Действие |
|--------------|----------------------|--------| |--------------|----------------------------------------------|----------|
| `10.1.2.3` | `10.0.0.0/8` | Direct | | `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 | | `10.7.0.9` | `10.7.0.0/24` (более специфичное, бьёт `/8`) | Vpn |
| `192.168.1.1`| `192.168.0.0/16` | Direct | | `192.168.1.1`| `192.168.0.0/16` | Direct |
| `8.8.8.8` | (none) → default | Vpn | | `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]` Split-tunnel настраивается в `client.toml` в секции `[tunnel.split]`
(`crates/aura-cli/src/config.rs`). `build_route_table` turns it into a `RouteTable`: CIDR (`crates/aura-cli/src/config.rs`). `build_route_table` превращает её в `RouteTable`: CIDR-правила
rules are applied directly; domain rules are recorded and returned for the client to resolve применяются напрямую; доменные правила сохраняются и возвращаются, чтобы клиент мог разрезолвить
at startup. их на старте.
### Schema ### Схема
| Key | Type | Default | Meaning | | Ключ | Тип | По умолчанию | Смысл |
|------------------------------|-----------------|---------|----------------------------------------------------| |------------------------------|---------------------|--------------|------------------------------------------------|
| `default` | string | `"VPN"` | Action when no rule matches: `VPN` / `DIRECT` (case-insensitive) | | `default` | строка | `"VPN"` | Действие, когда ни одно правило не совпало: `VPN` / `DIRECT` (регистронезависимо) |
| `[[tunnel.split.direct]]` | array of rules | `[]` | Rules forcing matching destinations to **Direct** | | `[[tunnel.split.direct]]` | массив правил | `[]` | Правила, отправляющие совпавшие назначения в **Direct** |
| `[[tunnel.split.vpn]]` | array of rules | `[]` | Rules forcing matching destinations through the **VPN** | | `[[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"` | | `cidr` | строка | `"192.168.0.0/16"` |
| `domain` | string | `"intranet.example.com"` | | `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 ```toml
# Split-tunnel routing: the default action plus per-destination overrides. # Split-tunnel routing: the default action plus per-destination overrides.
@@ -139,23 +142,23 @@ domain = "intranet.example.com"
cidr = "10.7.0.0/24" 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 Работающий `aura client` (или `aura server`) поднимает **admin-сокет**крошечный построчный
line-protocol over a **Unix domain socket** (`crates/aura-cli/src/admin.rs`). The `aura JSON-протокол поверх **Unix domain socket** (`crates/aura-cli/src/admin.rs`). Подкоманды `aura
route` and `aura status` subcommands connect to it to inspect and mutate the live routing route` и `aura status` подключаются к нему, чтобы инспектировать и менять живую таблицу
table without restarting the tunnel. The default socket path is `/tmp/aura-admin.sock` маршрутизации, не перезапуская туннель. Путь сокета по умолчанию — `/tmp/aura-admin.sock`
(override with `--admin-socket`). (перекрывается через `--admin-socket`).
> Platform note: the admin socket uses Unix domain sockets (Linux/macOS). On Windows it is a > Замечание про платформу: admin-сокет использует Unix domain sockets (Linux/macOS). На Windows
> `cfg`-gated stub that returns an explanatory error (a named-pipe transport is future work), > это `cfg`-заглушка, возвращающая поясняющую ошибку (named-pipe-транспорт — будущая работа), —
> so the rest of the CLI still compiles there. > остальная часть CLI там по-прежнему собирается.
### Commands ### Команды
``` ```
aura route add (--cidr <CIDR> | --domain <DOMAIN>) --action <vpn|direct> [--admin-socket <PATH>] 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>] aura status [--admin-socket <PATH>]
``` ```
`route add` takes **exactly one** of `--cidr` / `--domain` (they are mutually exclusive, and `route add` принимает **ровно один** из ключей `--cidr` / `--domain` (они взаимоисключающие, и
one is required), plus `--action vpn` or `--action direct`. один из них обязателен), плюс `--action vpn` или `--action direct`.
```bash ```bash
# Send a CIDR directly, live. # Отправить CIDR напрямую, на лету.
aura route add --cidr 8.8.8.0/24 --action direct aura route add --cidr 8.8.8.0/24 --action direct
# ok # ok
# Route a domain through the VPN (resolved into host routes). # Завернуть домен через VPN (разрезолвится в host-маршруты).
aura route add --domain example.com --action vpn aura route add --domain example.com --action vpn
# ok # ok
# Inspect the current rules and default. # Посмотреть текущие правила и действие по умолчанию.
aura route list aura route list
# default: vpn # default: vpn
# cidr 8.8.8.0/24 direct # cidr 8.8.8.0/24 direct
# domain example.com vpn # domain example.com vpn
# Remove a CIDR rule. # Удалить CIDR-правило.
aura route remove --cidr 8.8.8.0/24 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 status
# Aura tunnel status # Aura tunnel status
# peer: client-1 # peer: client-1
@@ -196,22 +199,23 @@ aura status
# tx packets: 0 # tx packets: 0
``` ```
### Behavior notes ### Особенности поведения
- **`route remove` only removes CIDR rules** — it takes `--cidr` and has no domain form. The - **`route remove` удаляет только CIDR-правила** — он принимает `--cidr` и не имеет доменной
library `RouteTable` has no per-rule remove API, so a removal **rebuilds** the table from формы. У библиотечного `RouteTable` нет API для покнопочного удаления, поэтому удаление
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. правила добавляются заново при перестройке, но их ранее разрезолвенные host-маршруты
- **`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 - **`route list` перечисляет зеркало правил.** Истиной для классификации остаётся живой
lockstep with every mutation; `list` echoes that mirror while `classify` still uses the real `RouteTable`, но он не отдаёт итерацию по правилам, поэтому admin-слой ведёт параллельное
table. зеркало, синхронизированное с каждой мутацией; `list` отдаёт это зеркало, а `classify`
- **`status`** reports the verified peer id, the default action, the total rule count по-прежнему ходит в реальную таблицу.
(CIDR + domain), and inbound/outbound packet counters. - **`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 ```text
-> {"cmd":"route_add","cidr":"8.8.8.0/24","action":"direct"} -> {"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} <- {"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 - **`Direct` — заглушка**: пакеты с действием `Direct` логируются и отбрасываются, а не
the OS stack. The VPN path is fully functional. реинъецируются в стек ОС. Путь через VPN полностью функционален.
- **Domain rules are resolved once** (at startup / on demand) into host routes; no continuous - **Доменные правила резолвятся один раз** (на старте или по требованию) в host-маршруты;
re-resolution. непрерывного перерезолва нет.
- **`route remove` is CIDR-only** and rebuilds the table (domain host routes are re-resolved - **`route remove` работает только с CIDR** и перестраивает таблицу (доменные host-маршруты потом
on demand afterward). разрезолвятся по требованию).
- **Admin socket is Unix-only**; Windows is a `cfg`-gated stub. - **Admin-сокет только под Unix**; на Windows `cfg`-заглушка.
- The server is a **single shared TUN** in v1, and the tunnel resolver `dns` config field is - Сервер в v1 — это **один общий TUN**, а поле `dns` в конфигурации туннеля носит информационный
informational (the system resolver is used). характер (используется системный резолвер).