Two release blockers, both in the most destructive operation the package performs. 1. An unreadable /etc/fstab was treated as an empty one. let content = fs::read_to_string(fstab_path).unwrap_or_default(); PermissionDenied, an I/O error, a damaged filesystem and "file absent" all collapsed into an empty string. The rewrite then emitted a fstab containing only the managed swap line and installed it atomically and successfully, dropping the root and boot mounts. Consequence: a host that does not boot, guarded by one unwrap_or_default. The same masking was present in read_fstab_swap_entries, so an unreadable fstab was reported by status as "no swap entries", which reads as healthy drift-free state. 2. The backup was best-effort. let _ = fs::write(bak_path, &content); If it failed, the rewrite proceeded anyway. IMPLEMENTATION_SPEC and the acceptance checklist both asserted the backup exists. The scenario where the backup fails is exactly the scenario where it is needed. New module fstab.rs owns the operation: - read_required distinguishes NotFound from every other error and fails on both. A missing /etc/fstab on Debian is an anomaly, not a blank slate, so it is never created; - a blank candidate is refused outright; - the backup is mandatory and flushed before the temp file is created, so there is no window where the target is about to be replaced and no recovery copy exists; - two copies are kept: .awg2-harness.bak is the state before the current rewrite, .awg2-harness.original is written once with create_new and holds the host as it was before this package first touched it; - the temp file is a sibling of the target, created with create_new, sync_all'd, and inherits the original permissions; a stale temp from a killed run is removed explicitly rather than reused; - the candidate is validated with findmnt --verify --tab-file before the swap. The step is skipped when findmnt cannot be spawned; a rejection is fatal; - commit is an atomic rename followed by a directory fsync; - after the rename the target is re-read and compared to what was written. A mismatch restores the backup and fails. Also fixed: render_fstab_line interpolated the path raw. fstab fields are whitespace-separated, so a space or tab in swap.path would shift every following column. Paths now go through octal escaping (\040, \011, \012, \134). Ten tests, all portable - the "unreadable but present" case is exercised by pointing the reader at a directory, which fails as a non-NotFound error on every platform. Covered: unreadable target is an error rather than an empty file, missing target is an error, backup failure leaves the target byte-identical, blank candidate refused, unchanged candidate is a no-op that writes no backup, the pristine copy survives repeated rewrites, a stale temp does not block commit, and field escaping. Docs updated: IMPLEMENTATION_SPEC section 17 gained the full rewrite contract, the acceptance checklist gained seven checks including the two failure modes above. cargo test --workspace: 124 + 4 passed, ten new. clippy -D warnings clean.
AWG2 Harness
AWG2 Harness — host-side orchestrator для выделенного Debian-хоста с self-hosted AmneziaWG. Пакет управляет инфраструктурным состоянием вокруг Amnezia: per-peer traffic shaping, IFB ingress shaping, nftables host firewall, anti-torrent connection guard, managed swap, диагностикой drift и плановым reboot.
Проект разрабатывался и проверялся под AmneziaWG/AWG 2.0 и совместимый legacy-сценарий AmneziaWG/AWG 1.5. Он не заменяет AmneziaVPN, не генерирует ключи клиентов и не меняет продуктовую логику Amnezia. Amnezia сначала ставится официальным способом, после этого устанавливается AWG2 Harness.
Что ставит пакет
/usr/sbin/awg2-harness # основной orchestrator
/usr/sbin/awg2-reboot-scheduler # отдельный reboot scheduler
/lib/systemd/system/awg2-harness.service
/lib/systemd/system/awg2-reboot-scheduler.service
awg2-harness.service запускает ensure-swap перед стартом daemon, затем в фоне держит drift recovery. Daemon слушает Docker events и дополнительно делает периодический reconcile для traffic/firewall/swap; sysctl_tuning применяется только на старте daemon (если включён) или вручную отдельной командой. awg2-reboot-scheduler.service отдельно читает общий config, выбирает случайное время в заданном UTC-окне и запрашивает reboot через systemctl --no-block reboot.
Пакет не устанавливает Docker, не устанавливает AmneziaVPN, не включает сервисы автоматически и не создаёт боевой /etc/awg2-harness/config.toml. Это сделано намеренно: SSH, Amnezia и боевой config должны быть подготовлены оператором до включения firewall/reconcile.
Репозиторий
awg2_harness/
├── reboot_module/ # исходники awg2-reboot-scheduler
├── traffic_core/ # исходники awg2-harness
├── tools/ # build/package tooling
├── docs/ # эксплуатационная документация
├── config.example.toml # пример боевого конфига
├── awg2-harness.service # основной systemd unit
└── awg2-reboot-scheduler.service # reboot scheduler unit
Runtime-план production-развёртывания
Ниже целевой порядок установки. Его смысл простой: сначала безопасный SSH, потом официальная Amnezia, потом AWG2 Harness.
1. Подготовить Debian-хост
Целевая платформа AWG2 Harness — только Debian 13 amd64 на KVM/полноценной VM или dedicated host.
Минимальный baseline перед установкой Amnezia:
apt update
apt install -y openssh-server ca-certificates curl
timedatectl set-timezone Etc/UTC
2. Настроить SSH до установки Amnezia
Production-пример использует порт 2323 и вход только по ключу.
Сгенерируйте отдельную пару ключей для Amnezia на Windows (RSA PEM-формат):
ssh-keygen -t rsa -b 4096 -m PEM -f %USERPROFILE%\.ssh\amnezia_key
Дальше контракт строго такой:
- на сервер добавляется публичный ключ: содержимое
%USERPROFILE%\.ssh\amnezia_key.pubв/root/.ssh/authorized_keys; - в приложении AmneziaVPN в поле Password or SSH private key вставляется приватный ключ: содержимое
%USERPROFILE%\.ssh\amnezia_key(без.pub).
Критично: .pub никогда не вставляется в AmneziaVPN как private key.
install -d -m 0700 /root/.ssh
nano /root/.ssh/authorized_keys
chmod 0600 /root/.ssh/authorized_keys
Затем включите отдельный drop-in для порта и key-only login:
install -d -m 0755 /etc/ssh/sshd_config.d
cat >/etc/ssh/sshd_config.d/99-awg2-production.conf <<'EOF'
Port 2323
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
EOF
sshd -t
systemctl reload ssh 2>/dev/null || systemctl reload sshd
ss -ltnp | grep ':2323'
Критично: не закрывайте старую SSH-сессию, пока не проверили новый вход с другого терминала:
ssh -i ~/.ssh/amnezia_key -p 2323 root@<server-ip>
Windows-проверки:
type %USERPROFILE%\.ssh\amnezia_key.pub
type %USERPROFILE%\.ssh\amnezia_key
ssh -i %USERPROFILE%\.ssh\amnezia_key -p 2323 root@<server-ip>
Поля в интерфейсе AmneziaVPN:
Server IP address [:port] <server-ip>:2323
SSH Username root
Password or SSH private key содержимое %USERPROFILE%\.ssh\amnezia_key
На сервер содержимое %USERPROFILE%\.ssh\amnezia_key.pub в /root/.ssh/authorized_keys
Если сервер уже переведён на 2323 случайно и нужно временно вернуть 22 для тестов, поменяйте Port 2323 на Port 22, выполните sshd -t, systemctl reload ssh и проверьте ss -ltnp | grep ':22'.
3. Установить AmneziaVPN официальным способом
AWG2 Harness не устанавливает Amnezia. Используйте официальный self-hosted flow AmneziaVPN:
- Установите актуальное приложение AmneziaVPN на рабочую машину.
- В приложении выберите self-hosted server.
- Укажите публичный IPv4 сервера, SSH-порт, SSH-пользователя и приватный ключ.
- Установите AmneziaWG/AWG. Для AWG 2.0 нужен актуальный AmneziaVPN, где протокол AmneziaWG 2.0 доступен в списке протоколов.
- После установки убедитесь, что контейнер поднят и UDP-порт опубликован.
Проверка на сервере:
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)"
echo "container=$C"
docker port "$C"
docker exec "$C" awg show
Ожидаемые имена контейнера по умолчанию: amnezia-awg2 или amnezia-awg. amnezia-awg3 перечислен на будущее — в AmneziaVPN 5.0 такого типа контейнера ещё нет. Ожидаемый интерфейс внутри container netns: обычно awg0, fallback-кандидаты описаны в config.example.toml.
Совместимость с AmneziaWG 3.0
Обвязка не реализует AWG на уровне пакетов: tc, IFB и nftables работают с inner-IP уже после декапсуляции, поэтому header protection, content padding и рандомизация таймеров AWG 3.0 её не касаются.
Читается только peer state, всегда через docker exec. Реальный бинарь в официальном образе — /usr/bin/awg, /usr/bin/wg является symlink на него; порядок задаётся amnezia.wg_binaries (по умолчанию ["awg", "wg"]). На хосте wireguard-tools не требуется и не проверяется в preflight: AmneziaWG — userspace-реализация, её UAPI-сокет лежит в mount namespace контейнера, и хостовый wg через nsenter -n до него не достаёт.
Smoke-проверка после обновления контейнера:
C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)"
docker exec "$C" readlink -f /usr/bin/wg # ожидается /usr/bin/awg
docker exec "$C" awg --version
docker exec "$C" ip -o link show awg0
docker exec "$C" awg show all allowed-ips
docker port "$C"
# Структура dump без раскрытия значений:
docker exec "$C" awg show all dump | awk -F '\t' '{ print "row=" NR, "fields=" NF, "interface=" $1 }'
Ожидаемо: /usr/bin/wg указывает на /usr/bin/awg, интерфейс awg0 существует, peer-адреса отображаются, серверный порт опубликован по UDP. Interface-строка даст 14 полей на AWG 2.0 и 28 на AWG 3.0, peer-строки — 9.
Полный вывод awg show all dump передавать нельзя: во второй колонке interface-строки находится приватный ключ устройства.
3.1 Установить AmneziaDNS (обязательный production-шаг)
Для production-профиля AWG2 установите AmneziaDNS из приложения AmneziaVPN после установки AmneziaWG/AWG:
AmneziaVPN:
Servers → <server> → Services → AmneziaDNS → Install
Затем включите использование DNS для подключения:
Settings → Connection → DNS servers → Use AmneziaDNS
Security note:
В ожидаемой production-схеме AmneziaDNS слушает 53/tcp и 53/udp внутри Docker и не публикуется на host interface. Не открывайте публично 53/tcp или 53/udp в host firewall; DNS должен быть доступен только через VPN-path.
4. Собрать пакет на build-machine
На production-хосте не нужны Rust, cargo и source tree. Сборка делается отдельно.
cd awg2_harness
./tools/build.sh --prepare-build-env --bootstrap-rust --yes
./tools/build.sh --package --version 0.1.6
(cd dist && sha256sum -c SHA256SUMS)
dpkg-deb -I dist/awg2-harness_0.1.6_amd64.deb
dpkg-deb -c dist/awg2-harness_0.1.6_amd64.deb | sort
--version можно не указывать, если версия в traffic_core/Cargo.toml уже соответствует release. Если version override указан, он меняет версию Debian-пакета, а не версию crate в Cargo.toml.
5. Перенести .deb и config на production
Есть два production-сценария установки.
Вариант A: config передаётся вместе с .deb
scp -P 2323 dist/awg2-harness_*_amd64.deb root@<server-ip>:/root/
scp -P 2323 dist/SHA256SUMS root@<server-ip>:/root/
scp -P 2323 config.example.toml root@<server-ip>:/root/awg2-harness.config.toml
Перед установкой отредактируйте /root/awg2-harness.config.toml: SSH-порт должен совпадать с реальным sshd, amnezia_port — с опубликованным UDP-портом контейнера, peer_ipv4_cidr — с подсетью клиентов Amnezia.
Вариант B (основной): config создаётся после установки пакета
scp -P 2323 dist/awg2-harness_*_amd64.deb root@<server-ip>:/root/
scp -P 2323 dist/SHA256SUMS root@<server-ip>:/root/
6. Установить пакет и config
Вариант A:
cd /root
sha256sum -c SHA256SUMS
chmod 644 /root/awg2-harness_0.2.6_amd64.deb
apt install ./awg2-harness_*_amd64.deb
install -d -m 0755 /etc/awg2-harness
install -m 0644 /root/awg2-harness.config.toml /etc/awg2-harness/config.toml
Пакет ставит пример config в /usr/share/doc/awg2-harness/repo/config.example.toml, но не копирует его в /etc автоматически.
Вариант B (основной):
cd /root
sha256sum -c SHA256SUMS
chmod 644 /root/awg2-harness_0.2.6_amd64.deb
apt install ./awg2-harness_*_amd64.deb
install -d -m 0755 /etc/awg2-harness
cp /usr/share/doc/awg2-harness/repo/config.example.toml /etc/awg2-harness/config.toml
nano /etc/awg2-harness/config.toml
В варианте B после cp обязательно проверьте перед первым apply-firewall:
host_firewall.ssh_portсовпадает с реальным sshd-портом;host_firewall.amnezia_portсовпадает с published Docker UDP port;amnezia.peer_ipv4_cidrиanti_torrent.peer_ipv4_cidrсовпадают с реальной peer-подсетью.
7. Ручная preflight-проверка
awg2-harness --config /etc/awg2-harness/config.toml validate-config
awg2-harness --config /etc/awg2-harness/config.toml validate-runtime
awg2-harness --config /etc/awg2-harness/config.toml ensure-swap
awg2-harness --config /etc/awg2-harness/config.toml apply-firewall
awg2-harness --config /etc/awg2-harness/config.toml apply-limits
awg2-harness --config /etc/awg2-harness/config.toml status
Ожидаемый healthy state:
System:
service: inactive # до enable --now нормально
Traffic actual:
drift: no
Host firewall:
drift: no
Anti-torrent:
drift: no
Swap:
drift: no
Если peers ещё нет, apply-limits корректно напишет no peers found, skip tc apply. Это не ошибка: traffic topology строится, когда есть хотя бы один peer allowed IP в целевой подсети.
8. Включить systemd services
systemctl daemon-reload
systemctl enable --now awg2-harness.service
systemctl enable --now awg2-reboot-scheduler.service
systemctl status awg2-harness.service --no-pager -l
systemctl status awg2-reboot-scheduler.service --no-pager -l
journalctl -u awg2-harness.service --since '5 minutes ago' --no-pager
journalctl -u awg2-reboot-scheduler.service --since '5 minutes ago' --no-pager
После 30–60 секунд:
awg2-harness --config /etc/awg2-harness/config.toml status
Healthy daemon показывает service: active, traffic=ok, firewall=ok, swap=ok в recent events и пустой last_error.
9. Проверить после reboot
reboot
После возврата:
systemctl status docker --no-pager -l
systemctl status awg2-harness.service --no-pager -l
systemctl status awg2-reboot-scheduler.service --no-pager -l
awg2-harness --config /etc/awg2-harness/config.toml status
Дополнительно можно проверить фактическую tc topology внутри network namespace Amnezia:
C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)"
PID="$(docker inspect -f '{{.State.Pid}}' "$C")"
nsenter -t "$PID" -n -- tc class show dev awg0
nsenter -t "$PID" -n -- tc class show dev ifb0
nsenter -t "$PID" -n -- nft list table inet awg2_harness
nft list table inet awg2_host_firewall
Для default config на одном peer ожидаются классы 1:10 и 2:10 с rate 50Mbit ceil 50Mbit, root classes 1:1 и 2:1 с 500Mbit, host firewall table inet awg2_host_firewall и anti-torrent table inet awg2_harness внутри Amnezia netns.
CLI команды
Все команды принимают --config. Если параметр не указан, используется /etc/awg2-harness/config.toml.
awg2-harness --config /etc/awg2-harness/config.toml <command>
validate-config проверяет TOML и смысловые ограничения: пустые списки, формат rate (50mbit, 500mbit), CIDR, reboot window, swap path, SSH/Amnezia ports, protocol list.
validate-runtime проверяет host tools, которые реально нужны выбранным модулям: tc, ip, docker, modprobe, nsenter, а также nft, systemctl, swap tooling при включённых firewall/reboot/swap. Хостовый wg в этот список не входит: peer state читается внутри контейнера через docker exec (amnezia.wg_binaries).
ensure-swap приводит swap к desired state: создаёт/проверяет swapfile, включает его с заданным priority, применяет vm.swappiness, делает swap state exclusive и переписывает swap entries в /etc/fstab под управляемый swapfile.
apply-firewall делает два reconcile: host INPUT firewall в таблице inet awg2_host_firewall и anti-torrent guard в выбранном namespace. Команда не управляет Docker NAT и host FORWARD.
apply-limits находит Amnezia container, заходит в его network namespace, читает peers через wg, строит per-peer HTB/fq_codel классы на AWG/WG интерфейсе и IFB ingress path. Если drift не найден, пишет traffic rules up-to-date, skip apply.
status печатает фактическую диагностику: сервис, контейнер, netns, интерфейс, peers, WireGuard runtime, traffic desired/actual, host firewall drift, Docker/Amnezia port path, anti-torrent counters, swap state, conntrack, recent events и planned reboot.
daemon — режим для systemd. На старте делает startup reconcile, затем слушает Docker events и периодически сверяет traffic/swap/firewall. Вручную обычно не запускается, кроме отладки.
awg2-reboot-scheduler --config /etc/awg2-harness/config.toml читает только секцию [reboot], пишет план в /run/awg2-harness/reboot.plan и ждёт назначенного UTC-времени.
Что происходит под капотом
Traffic shaping реализован через tc в network namespace контейнера Amnezia. На egress используется root qdisc htb 1: на AWG/WG интерфейсе, root class 1:1, per-peer classes 1:<id> и fq_codel под каждым peer class. На ingress ставится ingress ffff: на AWG/WG интерфейс, входящие пакеты перенаправляются через mirred в ifb0, а там строится отдельная HTB topology 2: с такими же per-peer лимитами.
Peer discovery берётся из WireGuard/AmneziaWG allowed IPs, а не из handshake state. Поэтому offline peer с allowed ips: 10.8.1.1/32 тоже получает лимит заранее.
Host firewall — отдельная AWG2-owned таблица inet awg2_host_firewall. Она открывает loopback, established/related, SSH-порт из конфига, Amnezia-порт по указанным протоколам и optionally ICMP/ICMPv6. Docker-managed FORWARD и NAT не трогаются.
Anti-torrent guard — отдельная AWG2-owned таблица inet awg2_harness обычно внутри Amnezia netns. Правило ограничивает число новых conntrack flows per source VPN IP. Это не DPI и не распознавание BitTorrent-протокола; это грубый, дешёвый guard против сотен одновременных P2P-соединений.
Swap module намеренно строгий: если swap.enabled=true, desired state — один управляемый swapfile. Старые swap entries удаляются из /etc/fstab, block-device swap выключается, но block device не удаляется.
Production-границы ответственности
AWG2 Harness владеет только собственным state:
tctopology на Amnezia interface и configuredifb_name;- nft table
inet awg2_host_firewallв host namespace; - nft table
inet awg2_harnessдля anti-torrent; /run/awg2-harness/*runtime state;- managed swapfile и swap entries, если
swap.enabled=true.
AWG2 Harness не владеет:
- установкой AmneziaVPN;
- Docker daemon lifecycle;
- Docker NAT/FORWARD chains;
- генерацией client configs/keys;
- политикой пользователя внутри Amnezia app;
- сторонними firewall-таблицами.
Документация
docs/INSTALLATION.md— установка и preflight.docs/CONFIGURATION.md— параметрыconfig.toml.docs/ACCEPTANCE_CHECKLIST.md— production checklist.docs/ANTI_TORRENT.md— connection-count guard.docs/PRODUCTION_EXAMPLE.md— эталонный production-пример.tools/README.md— сборка и упаковка.reboot_module/README.md— внешний reboot scheduler.