# AWG2 Harness `AWG2 Harness` — host-side orchestrator для выделенного Debian-хоста с self-hosted AmneziaWG. Основной target релиза 0.3.1 — AmneziaWG 3.1; AWG 3.0/2.x сохраняются как regression compatibility. Пакет управляет инфраструктурным состоянием вокруг Amnezia: per-peer traffic shaping, IFB ingress shaping, Docker-совместимый host firewall через `iptables`/`ip6tables`, nftables anti-torrent guard, managed swap, диагностикой drift и плановым reboot. Релиз source-validated по официальной документации и исходникам AWG 3.1; фактическая live-приёмка выполняется после production deployment. Harness не заменяет AmneziaVPN, не генерирует ключи клиентов и не мигрирует AWG 2.0 на 3.1. Amnezia сначала ставится официальным способом, после этого устанавливается AWG2 Harness. Точная upstream-база зафиксирована в [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md). ## Что ставит пакет ```text /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` запускает только daemon; `ensure-swap` выполняется первым этапом его собственной startup-последовательности, а не отдельным `ExecStartPre`. Дальше 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. ## Репозиторий ```text 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: ```bash apt update apt install -y openssh-server ca-certificates curl timedatectl set-timezone Etc/UTC ``` ### 2. Настроить SSH до установки Amnezia Production-пример использует порт `2323` и вход только по ключу. Сгенерируйте отдельную пару ключей для Amnezia на Windows (RSA PEM-формат): ```cmd 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. ```bash install -d -m 0700 /root/.ssh nano /root/.ssh/authorized_keys chmod 0600 /root/.ssh/authorized_keys ``` Затем включите отдельный drop-in для порта и key-only login: ```bash 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-сессию, пока не проверили новый вход с другого терминала: ```bash ssh -i ~/.ssh/amnezia_key -p 2323 root@ ``` Windows-проверки: ```cmd type %USERPROFILE%\.ssh\amnezia_key.pub type %USERPROFILE%\.ssh\amnezia_key ssh -i %USERPROFILE%\.ssh\amnezia_key -p 2323 root@ ``` Поля в интерфейсе AmneziaVPN: ```text Server IP address [:port] :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: 1. Установите актуальное приложение AmneziaVPN на рабочую машину. 2. В приложении выберите self-hosted server. 3. Укажите публичный IPv4 сервера, SSH-порт, SSH-пользователя и приватный ключ. 4. Установите AmneziaWG 3.1. Для self-hosted deployment требуется AmneziaVPN 5.0.1.5 или новее. 5. После установки убедитесь, что контейнер поднят и UDP-порт опубликован. Проверка на сервере: ```bash docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" echo "container=$C" docker port "$C" docker exec "$C" awg show ``` Ожидаемые имена контейнера по умолчанию: основной `amnezia-awg2`, затем legacy fallback `amnezia-awg`. Ожидаемый интерфейс внутри container netns — `awg0`; дополнительные fallback-кандидаты описаны в `config.example.toml`. Любое другое корректное имя можно указать вручную в config. ### Совместимость с AmneziaWG 3.1 Канонические версии, commit SHA и официальные ссылки приведены в [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md). Релиз 0.3.1 source-validated against AWG 3.1; до отдельной live-приёмки это не означает «протестировано на production AWG 3.1». Обвязка не реализует AWG на уровне пакетов: `tc`, IFB и `nftables` работают с inner-IP уже после декапсуляции, поэтому header protection, content padding, RandomTrailers и DisableCookies её не касаются. Peer state читается через `docker exec` официальным `awg` внутри контейнера; `/usr/bin/wg` в reference image является symlink на него. Это единая container-side точка доступа независимо от того, использован kernel `amneziawg` или fallback `amneziawg-go`, и она не привязывает harness к host-side tools. Порядок задаётся `amnezia.wg_binaries` (по умолчанию `["awg", "wg"]`); host `wireguard-tools` не требуется. Smoke-проверка после обновления контейнера: ```bash C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|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" ip -d 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 }' ``` Для reference tools AWG 3.1 ожидаемо: `/usr/bin/wg` указывает на `/usr/bin/awg`, интерфейс `awg0` существует, peer-адреса отображаются, серверный порт опубликован по UDP, interface-row содержит 30 полей, peer-row — 9. Исторически WireGuard/AWG 2.x/AWG 3.0/AWG 3.1 tools печатают interface-row шириной 5/14/28/30 полей соответственно; текущий peer-row содержит 9. Это только диагностика: ширина зависит от producer tools, а не от версии параметров туннеля. По количеству колонок harness не определяет версию и не классифицирует peer. Полный вывод `awg show all dump` передавать нельзя: во второй колонке interface-строки находится приватный ключ устройства. ### 3.1 Установить AmneziaDNS (обязательный production-шаг) Для production-профиля AWG 3.1 установите AmneziaDNS из приложения AmneziaVPN после установки AmneziaWG: ```text AmneziaVPN: Servers → → Services → AmneziaDNS → Install ``` Затем включите использование DNS для подключения: ```text 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. Сборка делается отдельно. ```bash cd awg2_harness chmod +x ./tools/build.sh ./tools/build.sh --prepare-build-env --bootstrap-rust --yes ./tools/build.sh --package (cd dist && sha256sum -c SHA256SUMS) dpkg-deb -I dist/awg2-harness_0.3.1_amd64.deb dpkg-deb -c dist/awg2-harness_0.3.1_amd64.deb | sort ``` Release package берёт версию из `traffic_core/Cargo.toml`. `--version` предназначен только для осознанного Debian packaging override и не меняет версию crate. ### 5. Перенести `.deb` и config на production Есть два production-сценария установки. #### Вариант A: config передаётся вместе с `.deb` ```bash scp -P 2323 dist/awg2-harness_*_amd64.deb root@:/root/ scp -P 2323 dist/SHA256SUMS root@:/root/ scp -P 2323 config.example.toml root@:/root/awg2-harness.config.toml ``` Перед установкой отредактируйте `/root/awg2-harness.config.toml`: SSH-порт должен совпадать с реальным sshd, `amnezia.expected_host_udp_port` — с опубликованным Docker UDP-портом, `peer_ipv4_cidr` — с подсетью клиентов Amnezia. Это поле используется для диагностики и не создаёт host INPUT allow-rule: published bridge traffic проходит через Docker `FORWARD`/NAT path. #### Вариант B (основной): config создаётся после установки пакета ```bash scp -P 2323 dist/awg2-harness_*_amd64.deb root@:/root/ scp -P 2323 dist/SHA256SUMS root@:/root/ ``` ### 6. Установить пакет и config Вариант A: ```bash cd /root sha256sum -c SHA256SUMS chmod 644 /root/awg2-harness_0.3.1_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 (основной): ```bash cd /root sha256sum -c SHA256SUMS chmod 644 /root/awg2-harness_0.3.1_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-портом; - `amnezia.expected_host_udp_port` совпадает с published Docker UDP port; - `amnezia.peer_ipv4_cidr` и `anti_torrent.peer_ipv4_cidr` совпадают с реальной peer-подсетью. ### 7. Ручная preflight-проверка ```bash 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: ```text System: service: inactive # до enable --now нормально Traffic actual: drift: no Host firewall: drift: no Docker/Amnezia port path: port_contract: ok Anti-torrent: drift: no Swap: drift: no ``` Если peers ещё нет, это здоровое desired state. `apply-limits` удалит оставшуюся AWG2-owned traffic topology, если раньше peers были, и завершится успешно. В штатном цикле daemon это не создаёт повторяющийся `info`-шум. ### 8. Включить systemd services ```bash 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 секунд: ```bash 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 ```bash reboot ``` После возврата: ```bash 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: ```bash C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" PID="$(docker inspect -f '{{.State.Pid}}' "$C")" nsenter -t "$PID" -n -- tc -s class show dev awg0 nsenter -t "$PID" -n -- tc -s class show dev ifb0 nsenter -t "$PID" -n -- nft list table inet awg2_harness iptables -S INPUT iptables -S AWG2H_A 2>/dev/null || iptables -S AWG2H_B ip6tables -S INPUT ip6tables -S AWG2H_A 2>/dev/null || ip6tables -S AWG2H_B ``` Для default config на одном peer ожидаются классы `1:10` и `2:10` с `rate 50Mbit ceil 50Mbit`, root classes `1:1` и `2:1` с `500Mbit`, помеченный переход из host `INPUT` в одну из цепочек `AWG2H_A`/`AWG2H_B` и anti-torrent table `inet awg2_harness` внутри Amnezia netns. ## CLI команды Все команды принимают `--config`. Если параметр не указан, используется `/etc/awg2-harness/config.toml`. ```bash awg2-harness --config /etc/awg2-harness/config.toml ``` `validate-config` проверяет TOML и смысловые ограничения: пустые списки, формат rate (`50mbit`, `500mbit`), CIDR, reboot window, swap path, SSH/ожидаемый Amnezia UDP port и диапазон textual class id `10..=9999`. Удалённые поля `host_firewall.amnezia_port`/`amnezia_protocols` считаются ошибкой, а не молча игнорируются. `validate-runtime` проверяет host tools, которые реально нужны выбранным модулям: `tc`, `ip`, `docker`, `modprobe`, `nsenter`, а также `iptables`, `ip6tables`, `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 в изолированных `iptables`/`ip6tables` A/B-цепочках и nftables anti-torrent guard в выбранном namespace. Команда не меняет policy встроенного `INPUT`, Docker NAT или host `FORWARD`. `apply-limits` находит Amnezia container, заходит в его network namespace, читает peers через container-side `awg`/`wg` и до mutation проверяет строгий server-side контракт: один IPv4 `/32` внутри `amnezia.peer_ipv4_cidr` на peer; `(none)` допустим. IPv6, смешанные или множественные маршруты, другая маска и адрес вне подсети дают явную ошибку unsupported topology. После проверки команда строит per-peer HTB/fq_codel классы на AWG/WG интерфейсе и IFB ingress path. Peer filters создаются classifier-ом `flower` с единым `pref 10`; legacy peer-фильтры `u32` считаются drift и мигрируют через controlled rebuild. Перед удалением проверяются qdisc handles и alias IFB `awg2-harness:traffic`; чужой объект с тем же именем является ошибкой. Healthy state не печатает сообщение на каждом цикле daemon. `status` печатает фактическую диагностику: сервис, контейнер, netns, интерфейс, peers, WireGuard runtime, traffic desired/actual, host firewall drift, Docker/Amnezia port path со всеми UDP `HostIp:HostPort` bindings, 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:` и `fq_codel` под каждым peer class. Egress peer filter сопоставляет `dst_ip /32`, ingress peer filter — `src_ip /32`; оба используют `flower pref 10`. На ingress ставится `ingress ffff:` на AWG/WG интерфейс, входящие пакеты перенаправляются через `mirred` в configured IFB, а там строится отдельная HTB topology `2:` с такими же per-peer лимитами. Только catch-all redirect в IFB остаётся `u32`. `tc -batch -` является fail-fast, но не транзакционным: при любой ошибке Harness удаляет только доказанно свои частичные объекты, сохраняет первичную и cleanup-ошибку и затем обязательно выполняет verify. Счётчики peers берутся из `tc -s class`, потому что class-only `flower` не гарантирует action statistics. Peer discovery берётся из WireGuard/AmneziaWG allowed IPs, а не из handshake state. Поэтому offline peer с `allowed ips: 10.8.1.1/32` тоже получает лимит заранее. Ограничение относится только к адресации peers внутри server-side AWG topology. Клиентский маршрут `::/0` и внешний IPv6 Docker publish не означают IPv6 peer addressing: Docker может одновременно публиковать тот же UDP port на `0.0.0.0` и `[::]`, и status показывает оба binding. Host firewall владеет только помеченным переходом из встроенного `INPUT` и двумя A/B-цепочками `AWG2H_A`/`AWG2H_B` отдельно для IPv4 и IPv6. Цепочка разрешает loopback, established/related, SSH-порт из конфига и optionally ICMP/ICMPv6, затем применяет configured verdict. Новая цепочка полностью проверяется до переключения; новый jump вставляется первым, а старый удаляется только после успеха обеих IP-family по точной rule specification. Numeric replace/delete не применяется, поэтому чужое правило не затрагивается при конкурентном изменении `INPUT`. Ошибка IPv6 удаляет только новый IPv4 jump и оставляет прежний рабочим. Published Amnezia UDP port здесь намеренно не открывается: Docker bridge publish обслуживается через Docker-managed `FORWARD`/NAT path. 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: - `tc` topology с handles `1:`/`ffff:` на Amnezia interface и IFB с alias `awg2-harness:traffic`; - помеченный host `INPUT` jump и цепочки `AWG2H_A`/`AWG2H_B` в `iptables`/`ip6tables`; - 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`](docs/INSTALLATION.md) — установка и preflight. - [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md) — upstream-версии и контракт AWG 3.1. - [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) — параметры `config.toml`. - [`docs/ACCEPTANCE_CHECKLIST.md`](docs/ACCEPTANCE_CHECKLIST.md) — production checklist. - [`docs/ANTI_TORRENT.md`](docs/ANTI_TORRENT.md) — connection-count guard. - [`docs/PRODUCTION_EXAMPLE.md`](docs/PRODUCTION_EXAMPLE.md) — эталонный production-пример. - [`tools/README.md`](tools/README.md) — сборка и упаковка. - [`reboot_module/README.md`](reboot_module/README.md) — внешний reboot scheduler.