- config.example: host_firewall.ssh_port=2323 - docs: синхронизированы CONFIGURATION/MIGRATION/ACCEPTANCE_CHECKLIST
20 KiB
Миграция 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, затем fallbackamnezia-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:
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:
- На хосте определить контейнер через Docker API или
docker inspect. - Получить PID контейнера и путь к его network namespace:
/proc/<pid>/ns/net. - Все операции
wg,ip,tc,ifbвыполнять в правильном namespace. - Для реализации можно выбрать один из двух путей:
- через
nsenter -t <pid> -n .../ip netns exec-эквивалент и системные команды; - через netlink/native Rust libraries после входа в namespace.
- через
docker execдопустим только как fallback или transitional adapter, но не как архитектурный центр. Причина: это делает нас зависимыми от shell/tools внутри контейнера и хуже соответствует модели “наш бинарь управляет состоянием сервера”.
Для первой production-версии нормальный компромисс: Rust-бинарь владеет orchestration, locking, state comparison, retry, status и lifecycle, а низкоуровневые tc/ip/wg операции выполняет через команды в namespace. Нативный netlink можно вводить после стабилизации, если это даст реальный выигрыш по надёжности или нагрузке.
6. Обязательные функции Rust-версии
Минимум для parity со старой системой:
daemon— основной systemd-процесс.apply-limits— ручное применение/reconcile traffic limits.status— диагностика уровняcheck-vpn-limits, но с проверкой обоих направлений и drift.validate-config— проверка конфига без изменения системы.apply-firewall— ручной reconcile AWG2-owned nftables state, если anti-torrent включён.ensure-swap— идемпотентный setup swap-файла 2 ГБ.- Внешний reboot scheduler
awg2-reboot-schedulerкак отдельный production service. - Locking, чтобы одновременно не шли два apply/reconcile.
- Docker event watcher.
- Периодический reconciliation loop, потому что Docker events могут быть пропущены.
- Start-up reconciliation: при запуске daemon сразу проверить и применить лимиты и AWG2-owned firewall state.
- Graceful behavior, если контейнер временно отсутствует: логировать и продолжать loop, а не падать насмерть.
7. Подкоманды
awg2-harness daemon
awg2-harness apply-limits
awg2-harness apply-firewall
awg2-harness status
awg2-harness ensure-swap
awg2-harness validate-config
awg2-harness validate-runtime
daemon — основной production-режим. Он делает initial reconcile, слушает Docker events, периодически проверяет state и пишет runtime-логи в journald. Планирование reboot выполняется отдельным сервисом awg2-reboot-scheduler.
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 и содержать только параметры, которые реально меняются оператором.
[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 = "50mbit"
root_rate = "500mbit"
reconcile_interval_secs = 15
container_ready_timeout_secs = 120
class_start_id = 10
ifb_name = "ifb0"
[host_firewall]
enabled = true
ssh_port = 2323
amnezia_port = 443
amnezia_protocols = ["udp"]
allow_icmp = true
default_input_policy = "drop"
[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 = 1
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:
[Unit]
Description=AWG2 Harness multi-orchestrator
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=root
ExecStartPre=/usr/sbin/awg2-harness --config /etc/awg2-harness/config.toml ensure-swap
ExecStart=/usr/sbin/awg2-harness --config /etc/awg2-harness/config.toml daemon
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Сервис должен запускаться от root. На выделенном сервере не нужно искусственно ограничивать его так, чтобы он не мог управлять сетью. Hardening можно добавлять после реальной проверки, но нельзя включать PrivateNetwork, PrivateDevices или слишком жёсткий CapabilityBoundingSet, если они ломают tc, ifb, Docker или reboot.
12. Reboot и swap
Логика reboot вынесена в отдельный production-бинарь reboot_module (awg2-reboot-scheduler). Окно перезагрузки задаётся в конфиге, включая корректные окна через полночь. Механика systemctl --no-block reboot сохраняется.
ensure-swap работает как exclusive reconciler: при swap.enabled=true пакет полностью владеет swap-state хоста. После reconcile активен ровно один swap (swap.path), все прочие активные swap entries выключены (swapoff), а все старые swap-строки в /etc/fstab заменены на одну managed-запись AWG2. Для block-device swap выполняются только swapoff и удаление записи из fstab; device не удаляется.
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.