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