Files
AWG2_harness/docs/MIGRATION.md
T

269 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Миграция AWG2 Harness с bash-системы на Rust multi-orchestrator
## 1. Исправленный scope
AWG2 Harness не должен быть просто Rust-эквивалентом пары bash-скриптов. Цель шире: сделать единый production multi-orchestrator, который сначала приводит сервер и Amnezia network namespace к нужному состоянию, а затем постоянно следит за drift и возвращает систему в desired state.
Источник поведения — папка `Amnezia limits (old bash)/`. Всё, что там реально настроено и используется в production, должно быть явно перенесено в Rust-модель: обнаружение контейнера, обнаружение интерфейса, получение peers, `tc` rules, `ifb0`, ingress redirect, per-peer HTB classes, `fq_codel`, auto-healing, статусная диагностика и логи. Новые правила, которых не было в old bash, например anti-torrent через nftables, должны быть описаны отдельно как расширения, а не смешаны с parity contract.
Сервер выделенный. На нём нет соседних сервисов, которые надо защищать от наших сетевых правил. Поэтому бинарь может и должен владеть host/network rules, необходимыми для Amnezia. Ограничение только одно: не ломать саму Amnezia как продукт и не менять её конфиги без явной задачи. Но системные правила, созданные old bash для лимитов, становятся зоной ответственности Rust-бинаря.
## 2. Production-контекст
Целевой хост:
- Debian 13 amd64.
- SSH настроен заранее владельцем сервера.
- AmneziaWG/AWG 2.0 устанавливается официальным способом до установки AWG2 Harness.
- После Amnezia доставляется один Rust-бинарь и единый конфиг.
- Пакет ставится один раз; автообновления самого пакета не проектируются.
- Бинарь запускается от root через systemd, потому что ему нужны Docker/systemd/net-admin/swap возможности.
Система должна переживать:
- reboot хоста;
- restart Docker;
- restart контейнера Amnezia;
- update контейнера Amnezia;
- изменение имени интерфейса `awg0` / `wg0` / `amnezia0` / `tun0`;
- добавление peer;
- удаление peer;
- частично применённое или повреждённое `tc`-состояние;
- частично применённое или повреждённое AWG2-owned nftables-состояние.
## 3. Что именно делает старая bash-система
Старая система состоит из нескольких файлов, и все они важны.
`limit_peers.sh` — главный apply/reconcile-скрипт. Он:
- берёт lock через `/tmp/amnezia-tc.lock`, чтобы не было параллельных применений;
- ищет запущенный контейнер `amnezia-awg2`, затем fallback `amnezia-awg`;
- проверяет интерфейс `WG_IF`, по умолчанию `wg0`, и если его нет, авто-детектит `wg*`, `awg*`, `amnezia*`, `tun*`;
- получает client IP из `wg show all allowed-ips`, fallback — regex из `wg show`;
- фильтрует текущую production-подсеть `10.8.1.x`;
- проверяет существующие `tc filter` по hex-адресам client IP;
- если состояние не совпадает, полностью удаляет старую topology и создаёт новую;
- создаёт root HTB на WG/AWG-интерфейсе: `tc qdisc add dev <iface> root handle 1: htb`;
- создаёт root class `1:1` с `rate 1000mbit ceil 1000mbit`;
- загружает/создаёт `ifb0`;
- добавляет ingress qdisc `ffff:` на WG/AWG-интерфейс;
- добавляет ingress redirect через `mirred egress redirect dev ifb0`;
- создаёт root HTB на `ifb0`: `handle 2:`;
- создаёт root class `2:1` с `rate 1000mbit ceil 1000mbit`;
- для каждого peer создаёт egress class `1:<class_id>` с `rate LIMIT ceil LIMIT`;
- для каждого peer создаёт `fq_codel` под egress class;
- для каждого peer добавляет egress filter `match ip dst <peer_ip> flowid 1:<class_id>`;
- для каждого peer создаёт ingress class `2:<class_id>` с тем же лимитом;
- для каждого peer создаёт `fq_codel` под ingress class;
- для каждого peer добавляет ingress filter на `ifb0`: `match ip src <peer_ip> flowid 2:<class_id>`;
- начинает `class_id` с 10;
- пишет события в `/var/log/amnezia-tc.log`.
`docker_watcher.sh` слушает `docker events --filter event=start`. При старте `amnezia-awg2` или `amnezia-awg` он ждёт появления `awg0` до 20 секунд и запускает `limit_peers.sh`. В Rust это нужно исправить: watcher не должен ждать только `awg0`, он обязан использовать общий detector интерфейса.
`check-vpn-limits` — CLI-диагностика. Он показывает systemd статус, контейнер, peers из `wg show`, классы с лимитом 70 Mbit/s, filter stats и последние строки `/var/log/amnezia-tc.log`. Rust `status` должен заменить этот отчёт, но проверять шире: оба направления, `ifb0`, ingress redirect и соответствие desired state.
`amnezia-tc.service` — oneshot apply после Docker/network-online.
`amnezia-tc-watcher.service` — постоянно запущенный watcher Docker events.
`traffic_control-was.txt`, `wg-peers.txt`, `iproute2_a_core.txt`, `permissions.txt`, `Нюансы.txt` — production-снимки. Они фиксируют реальные условия: Debian 13 cloud kernel, Docker 26.1.5, iproute2 6.15.0, интерфейс `awg0`, 23 peer IP в `10.8.1.0/24`, HTB classes `1:10..1:32`, `fq_codel` leaves и enabled systemd units.
## 4. Desired state Rust-бинаря
Rust-бинарь должен иметь explicit desired state model. Нельзя ограничиться последовательным запуском команд “как в bash”. Нужно уметь сравнить expected и actual, а затем принять решение: no-op, incremental fix или full rebuild.
Базовый desired state для traffic limits:
```text
container: first running from configured names
netns: namespace of selected container
interface: detected AWG/WG interface inside that netns
peers: IPv4 /32 allowed IPs inside configured peer CIDR
root egress qdisc: <iface> root handle 1: htb
root egress class: 1:1 rate root_rate ceil root_rate
per-peer egress class: 1:<id> rate peer_limit ceil peer_limit
per-peer egress qdisc: fq_codel under 1:<id>
per-peer egress filter: dst peer_ip -> 1:<id>
ingress qdisc: <iface> handle ffff:
ingress redirect: <iface> ingress -> ifb0
ifb device: ifb0 up in the same namespace where shaping is applied
root ingress qdisc: ifb0 root handle 2: htb
root ingress class: 2:1 rate root_rate ceil root_rate
per-peer ingress class: 2:<id> rate peer_limit ceil peer_limit
per-peer ingress qdisc: fq_codel under 2:<id>
per-peer ingress filter: src peer_ip -> 2:<id>
```
Любое отклонение от этого состояния считается drift. Если drift простой, можно исправить точечно. Для v1 допустим и предпочтителен full rebuild всей AWG traffic topology, потому что он проще, предсказуемее и повторяет текущую production-модель.
## 5. Модель управления host/network state
Исправление к предыдущей версии документации: бинарь не должен рассматриваться как осторожная внешняя обёртка, которая только вызывает `docker exec` и не владеет правилами. Правильная модель — host-level orchestrator with namespace control.
Практический порядок для v1:
1. На хосте определить контейнер через Docker API или `docker inspect`.
2. Получить PID контейнера и путь к его network namespace: `/proc/<pid>/ns/net`.
3. Все операции `wg`, `ip`, `tc`, `ifb` выполнять в правильном namespace.
4. Для реализации можно выбрать один из двух путей:
- через `nsenter -t <pid> -n ...` / `ip netns exec`-эквивалент и системные команды;
- через netlink/native Rust libraries после входа в namespace.
5. `docker exec` допустим только как fallback или transitional adapter, но не как архитектурный центр. Причина: это делает нас зависимыми от shell/tools внутри контейнера и хуже соответствует модели “наш бинарь управляет состоянием сервера”.
Для первой production-версии нормальный компромисс: Rust-бинарь владеет orchestration, locking, state comparison, retry, status и lifecycle, а низкоуровневые `tc/ip/wg` операции выполняет через команды в namespace. Нативный netlink можно вводить после стабилизации, если это даст реальный выигрыш по надёжности или нагрузке.
## 6. Обязательные функции Rust-версии
Минимум для parity со старой системой:
1. `daemon` — основной systemd-процесс.
2. `apply-limits` — ручное применение/reconcile traffic limits.
3. `status` — диагностика уровня `check-vpn-limits`, но с проверкой обоих направлений и drift.
4. `validate-config` — проверка конфига без изменения системы.
5. `apply-firewall` — ручной reconcile AWG2-owned nftables state, если anti-torrent включён.
6. `ensure-swap` — идемпотентный setup swap-файла 2 ГБ.
7. Встроенный reboot scheduler в `traffic_core` (единый production path, без отдельного runtime-модуля).
8. Locking, чтобы одновременно не шли два apply/reconcile.
9. Docker event watcher.
10. Периодический reconciliation loop, потому что Docker events могут быть пропущены.
11. Start-up reconciliation: при запуске daemon сразу проверить и применить лимиты и AWG2-owned firewall state.
12. Graceful behavior, если контейнер временно отсутствует: логировать и продолжать loop, а не падать насмерть.
## 7. Подкоманды
```bash
awg2-harness daemon
awg2-harness apply-limits
awg2-harness apply-firewall
awg2-harness status
awg2-harness ensure-swap
awg2-harness validate-config
```
`daemon` — основной production-режим. Он делает initial reconcile, слушает Docker events, периодически проверяет state, интегрирует reboot scheduler и пишет runtime-логи в journald.
`apply-limits` — немедленный reconcile traffic limits. Команда должна быть безопасной для повторного запуска.
`apply-firewall` — немедленный reconcile nftables anti-torrent rules. Команда должна менять только AWG2-owned table/chain.
`status` — отчёт о текущем состоянии: контейнер, PID/netns, интерфейс, peers, egress topology, ingress/ifb topology, drift summary, последние ошибки.
`ensure-swap` — setup-задача, не постоянный loop.
`validate-config` — синтаксическая и семантическая проверка.
## 8. Что нужно исправить относительно bash
Bash работает, но в Rust нужно убрать известные слабые места.
Первое: единый detector. Сейчас `limit_peers.sh` умеет авто-детектить интерфейс, а `docker_watcher.sh` ждёт только `awg0`. В Rust detector должен быть один и использоваться везде.
Второе: проверка полного state. Старый skip decision в основном смотрит фильтры на WG-интерфейсе. Rust должен проверять также `ifb0`, ingress redirect, classes на `ifb0`, qdisc leaves и соответствие class_id ↔ peer_ip.
Третье: парсинг peers. Текущий regex жёстко ищет `10.8.1.x`. В Rust подсеть должна быть конфигурируемой, а парсинг `wg show all allowed-ips` — структурным.
Четвёртое: наблюдаемость. Status должен отличать “container отсутствует”, “interface отсутствует”, “wg не читается”, “tc отсутствует”, “rules partial”, “rules match desired”.
Пятое: namespace ownership. Не надо полагаться на shell внутри контейнера как на единственный способ управления. Бинарь должен уметь работать от host authority и заходить в namespace контейнера.
## 9. Единый конфиг
Конфиг должен жить в `/etc/awg2-harness/config.toml` и содержать только параметры, которые реально меняются оператором.
```toml
[amnezia]
container_names = ["amnezia-awg2", "amnezia-awg"]
interface_candidates = ["awg0", "wg0", "amnezia0", "tun0"]
interface_patterns = ["awg*", "wg*", "amnezia*", "tun*"]
peer_ipv4_cidr = "10.8.1.0/24"
[traffic]
enabled = true
peer_limit = "70mbit"
root_rate = "1000mbit"
reconcile_interval_secs = 15
container_ready_timeout_secs = 120
class_start_id = 10
ifb_name = "ifb0"
[anti_torrent]
enabled = true
max_connections_per_peer = 100
peer_ipv4_cidr = "10.8.1.0/24"
apply_in_amnezia_netns = true
[reboot]
enabled = true
window_start_utc = "01:00"
window_end_utc = "02:00"
[swap]
enabled = true
path = "/swapfile"
size_mb = 2048
priority = -2
swappiness = 1
```
`ifb_name` можно оставить в конфиге, потому что это имя системного интерфейса, которым владеет orchestrator. Handles `1:`, `2:`, `ffff:` лучше не выносить в конфиг: это implementation contract AWG traffic module, а не эксплуатационная настройка.
## 10. Anti-torrent extension
Anti-torrent не входит в старую bash-систему, но хорошо ложится в целевую модель multi-orchestrator. Бинарь уже владеет нужным network namespace и должен уметь применять, проверять и восстанавливать nftables state так же, как он восстанавливает `tc` topology.
Правильная v1-модель — не protocol detection и не strike-система, а простой per-peer connection-count guard через nftables `ct count`. Правило должно быть scoped к VPN peer CIDR и keyed by source peer IP через `meter`, иначе можно случайно получить общий лимит на всю подсеть.
Правило должно жить в отдельной AWG2-owned таблице, например `table inet awg2_harness`, и применяться в Amnezia network namespace. Нельзя делать `flush ruleset`; удалять можно только owned table/chain.
См. `docs/ANTI_TORRENT.md`.
## 11. Systemd-модель
Вместо двух старых units нужен один daemon-unit:
```ini
[Unit]
Description=AWG2 Harness multi-orchestrator
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/awg2-harness daemon --config /etc/awg2-harness/config.toml
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
Сервис должен запускаться от root. На выделенном сервере не нужно искусственно ограничивать его так, чтобы он не мог управлять сетью. Hardening можно добавлять после реальной проверки, но нельзя включать `PrivateNetwork`, `PrivateDevices` или слишком жёсткий `CapabilityBoundingSet`, если они ломают `tc`, `ifb`, Docker или reboot.
## 12. Reboot и swap
Логика reboot работает только через встроенный scheduler `traffic_core`. Окно перезагрузки задаётся в конфиге, включая корректные окна через полночь. Механика `systemctl --no-block reboot` сохраняется.
`ensure-swap` должен создать 2 ГБ swap-файл с правами `0600`, добавить одну запись в `/etc/fstab`, включить swap и выставить низкую склонность к использованию (`vm.swappiness = 1`, низкий priority). Если на сервере уже есть несовместимый swap, команда должна сообщить об этом явно, а не разрушать чужое состояние без подтверждения.
## 13. Acceptance критерий миграции
Миграция считается корректной только если Rust-бинарь на реальном Debian 13 хосте:
- создаёт ту же traffic-control topology, что старая bash-система;
- ограничивает каждого peer в обе стороны на `traffic.peer_limit`;
- после повторного запуска не создаёт дубли и не ломает state;
- после restart контейнера сам восстанавливает rules;
- после restart Docker сам восстанавливает rules;
- после reboot хоста сам восстанавливает rules;
- после добавления peer создаёт новый class/filter;
- после удаления peer убирает устаревший class/filter через rebuild;
- `status` показывает не только “есть 70Mbit classes”, а полную картину desired vs actual;
- swap и reboot работают как части одного пакета;
- anti-torrent, если включён, применяет per-peer flow limit и не затрагивает unrelated nftables rules.