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