# Acceptance checklist перед production-выкладкой ## A. Базовая установка - [ ] Хост — Debian 13 amd64. - [ ] Сервер выделенный, без соседних сервисов. - [ ] AmneziaWG/AWG 2.0 или AmneziaWG/AWG 1.5 установлена официальным способом. - [ ] Контейнер `amnezia-awg2` или `amnezia-awg` запущен (`amnezia-awg3` — резерв на будущее, в AmneziaVPN 5.0 такого типа контейнера ещё нет). - [ ] `docker ps` видит контейнер. - [ ] Можно получить PID контейнера. - [ ] `/proc//ns/net` существует. - [ ] На хосте доступны `ip`, `tc`, `nsenter`, `modprobe`. Хостовый `wg` не требуется. - [ ] `awg2-harness validate-config` проходит без ошибок. ## B. Peer discovery - [ ] `awg2-harness status` показывает найденный контейнер. - [ ] `awg2-harness status` показывает PID/netns контейнера. - [ ] `awg2-harness status` показывает найденный интерфейс. - [ ] Количество peers совпадает с `docker exec "$C" awg show all allowed-ips`. - [ ] Peer с несколькими allowed IP даёт отдельный класс на каждый адрес (в этой команде адреса разделены пробелом). - [ ] Offline peers с allowed IP тоже попадают в список shaping. - [ ] IP вне `amnezia.peer_ipv4_cidr` не попадают в traffic shaping. - [ ] В секции `WireGuard clients` вывода `status` нет строки, соответствующей самому интерфейсу (столбец `Public key` не содержит приватный ключ устройства). - [ ] `docker exec "$C" readlink -f /usr/bin/wg` возвращает `/usr/bin/awg`. ## C. Traffic topology contract - [ ] `awg2-harness apply-limits` успешно завершается. - [ ] На AWG/WG-интерфейсе есть root qdisc `1: htb`. - [ ] На AWG/WG-интерфейсе есть root class `1:1` с `traffic.root_rate`. - [ ] Для каждого peer есть class `1:` с `traffic.peer_limit`. - [ ] Для каждого peer есть `fq_codel` под `1:`. - [ ] Для каждого peer есть egress filter `dst peer_ip -> 1:`. - [ ] Есть ingress qdisc `ffff:` на AWG/WG-интерфейсе. - [ ] Есть ingress redirect через `mirred ... redirect dev ifb0`. - [ ] `ifb0` существует и находится в UP state. - [ ] На `ifb0` есть root qdisc `2: htb`. - [ ] На `ifb0` есть root class `2:1` с `traffic.root_rate`. - [ ] Для каждого peer есть class `2:` с `traffic.peer_limit`. - [ ] Для каждого peer есть `fq_codel` под `2:`. - [ ] Для каждого peer есть ingress filter `src peer_ip -> 2:`. - [ ] Class ids начинаются с `traffic.class_start_id`, default `10`. - [ ] Peer IP → class id mapping использует стабильный лексикографический контракт: IP сортируются как строки, не numeric IPv4. ## D. Idempotency and drift recovery - [ ] Повторный `apply-limits` не создаёт дубли. - [ ] Повторный `apply-limits` не меняет healthy state без причины. - [ ] Если удалить root qdisc на AWG/WG-интерфейсе, daemon восстанавливает topology. - [ ] Если удалить ingress qdisc, daemon восстанавливает topology. - [ ] Если удалить ingress redirect filter (`parent ffff:`), drift детектируется и topology восстанавливается. - [ ] Если удалить `ifb0`, daemon восстанавливает topology. - [ ] Если сделать `ifb0` DOWN, drift детектируется и topology восстанавливается. - [ ] Если удалить один peer filter, daemon восстанавливает topology. - [ ] Если изменить `rate/ceil` peer class, drift детектируется и topology восстанавливается. - [ ] Если добавить stale peer class/filter, drift детектируется и topology восстанавливается. - [ ] После удаления peer устаревшие class/filter исчезают через rebuild. - [ ] После добавления peer появляются новые class/filter. ## E. Auto-healing lifecycle - [ ] `docker restart amnezia-awg2` → лимиты восстанавливаются автоматически. - [ ] `systemctl restart docker` → после возвращения контейнера лимиты восстанавливаются автоматически. - [ ] `systemctl restart awg2-harness` → сервис сам находит контейнер и применяет/проверяет лимиты. - [ ] Reboot хоста → сервис стартует и восстанавливает лимиты. - [ ] Изменение имени интерфейса на допустимое fallback-имя не ломает detector. - [ ] Если контейнер временно отсутствует, daemon логирует ожидание и продолжает работу. ## F. Status and observability - [ ] `status` показывает desired peer limit из конфига, а не hardcoded 70. - [ ] `status` показывает egress state. - [ ] `status` показывает ingress/ifb state. - [ ] `status` показывает WireGuard clients (online/offline, latest handshake, RX/TX, endpoint). - [ ] `status` показывает missing/stale peer rules. - [ ] `status` явно показывает partial/drift state. - [ ] `status` показывает tc filter counters по peer или `filter statistics: unavailable`. - [ ] Логи доступны через `journalctl -u awg2-harness`. - [ ] Ошибка отсутствия `tc`/`ip`/`wg` логируется явно. - [ ] Последний reconcile result виден в status или journal. - [ ] `status` показывает последние события reconcile/firewall из `/run/awg2-harness/events.log`. ## G. Reboot scheduler - [ ] Если `reboot.enabled = false`, сервис не планирует reboot. - [ ] Если `reboot.enabled = true`, в journal видна запланированная перезагрузка. - [ ] Время попадает в окно `window_start_utc`–`window_end_utc`. - [ ] Вызов reboot выполняется через `systemctl --no-block reboot`. ## H. Swap - [ ] `ensure-swap` создаёт `/swapfile` размером 2048 MB. - [ ] Права `/swapfile` — `0600`. - [ ] Если до запуска был активен другой swap, после `ensure-swap` он выключен. - [ ] После `ensure-swap` активен ровно один swap: `/swapfile`. - [ ] Все старые swap entries удалены из `/etc/fstab`. - [ ] Для block-device swap выполняется только `swapoff` и удаление `fstab` entry; block device не удаляется. - [ ] `vm.swappiness = 1` применён. - [ ] Повторный `ensure-swap` ничего не пересоздаёт и не добавляет дублей. ## H2. Sysctl network tuning - [ ] `awg2-harness apply-network-tuning` успешно завершается. - [ ] `awg2-harness inspect-network-tuning` показывает `enabled: true` и `drift: no`. - [ ] Создаётся только `/etc/sysctl.d/90-awg2-harness-vpn.conf`. - [ ] В managed-файле есть marker `Managed by awg2-harness sysctl_tuning`. - [ ] В managed-файле отсутствует `udp_wmem_min`. - [ ] Runtime apply меняет только drifted keys (`sysctl -w`), без `sysctl --system`. - [ ] При `tcp_bbr = auto` и недоступном BBR сервис не падает, в статусе есть warning. - [ ] При `tcp_bbr = require` и недоступном BBR команда/старт завершается с ошибкой. - [ ] При `enabled=false` managed-файл удаляется только если содержит managed marker. ## I. Anti-torrent nftables guard - [ ] Если `anti_torrent.enabled = true`, в Amnezia netns есть `table inet awg2_harness`. - [ ] Есть chain `forward_antitorrent` с hook `forward` и policy `accept`. - [ ] Правило ограничивает `ip saddr `, default `10.8.1.0/24`. - [ ] Лимит считается per source VPN IP через meter/set, а не глобально по всей подсети. - [ ] Порог совпадает с `anti_torrent.max_connections_per_peer`; текущий пример config использует `150`, более строгий вариант — `100`. - [ ] Drop counter виден в `status`. - [ ] На first-run без table `awg2-harness apply-firewall` создаёт `table inet awg2_harness`, а не падает. - [ ] При превышении порога новые flows дропаются, established flows ниже порога не ломаются. - [ ] После restart контейнера nftables rule пересоздаётся в новом netns. - [ ] При `anti_torrent.enabled = false` удаляется/отключается только AWG2-owned nftables state; Docker/Amnezia rules не flush-ятся. - [ ] `status` показывает `nf_conntrack_count` / `nf_conntrack_max`, если sysctl доступны. ## J. Rollback - [ ] Известно, как остановить и отключить `awg2-harness.service` и `awg2-reboot-scheduler.service`. - [ ] Известно, как удалить только AWG2-owned host firewall state (`inet awg2_host_firewall`). - [ ] Известно, как удалить только AWG2-owned anti-torrent state (`inet awg2_harness`) в выбранном namespace. - [ ] Известно, как удалить AWG2-owned `tc` topology (`root`, `ingress`, `ifb0`) в netns Amnezia. - [ ] При необходимости rollback включает удаление пакета `awg2-harness` после остановки сервисов. - [ ] Rollback проверен на тестовом сервере. ## K. Packaging / release - [ ] Package собирается на отдельной Debian 13 amd64 build-machine. - [ ] Production host не требует Rust/cargo/source tree. - [ ] `./tools/build.sh --package` создаёт `dist/awg2-harness__amd64.deb`. - [ ] `dist/SHA256SUMS` валидирует checksum пакета. - [ ] Пакет ставит бинарь в `/usr/sbin/awg2-harness`. - [ ] Пакет ставит unit в `/lib/systemd/system/awg2-harness.service`. - [ ] Пакет не устанавливает `/etc/awg2-harness/config.toml` автоматически. - [ ] Пакет не включает/не стартует сервис автоматически. - [ ] Пакет не имеет `Depends`/`Recommends` на `docker.io`; установка Amnezia/Docker внешняя. - [ ] `apt install ./awg2-harness__amd64.deb` не устанавливает и не заменяет Docker. - [ ] `apt install ./awg2-harness__amd64.deb` проходит на clean Debian 13 Amnezia host. - [ ] После ручной установки config `awg2-harness validate-config` проходит. - [ ] Сервис стартует только после явного `systemctl enable --now awg2-harness.service`. ## K2. Documentation / install UX - [ ] README не утверждает, что AmneziaVPN ожидает public key. - [ ] Документация явно разделяет private key для AmneziaVPN и public key для `authorized_keys`. - [ ] Есть пример заполнения AmneziaVPN fields: `Server IP address [:port]`, `SSH Username`, `Password or SSH private key`. - [ ] Описаны Windows пути `%USERPROFILE%\.ssh\amnezia_key` и `%USERPROFILE%\.ssh\amnezia_key.pub`. - [ ] Описан runtime config bootstrap из `/usr/share/doc/awg2-harness/repo/config.example.toml`. - [ ] Clean install подтверждает, что `/etc/awg2-harness/config.toml` не создаётся автоматически. ## K3. AmneziaDNS - [ ] AmneziaDNS установлен из приложения AmneziaVPN. - [ ] Контейнер `amnezia-dns` в состоянии healthy. - [ ] `docker inspect amnezia-dns` показывает `"53/tcp":null` и `"53/udp":null`. - [ ] `amnezia-dns` и активный AWG-контейнер (`amnezia-awg2`, `amnezia-awg3` или `amnezia-awg`) подключены к одной Amnezia DNS Docker-сети. - [ ] Публичный DNS-запрос к `:53` не резолвит домены; допустимы `connection refused`, `timeout` или `no servers could be reached`. - [ ] DNS-резолвинг у VPN-клиента работает при активном подключении. - [ ] AWG2 host firewall не открывает `53/tcp` и `53/udp`. ## L. Host firewall (ports/SSH safety) - [ ] `awg2-harness validate-config` падает, если отсутствует секция `[host_firewall]`. - [ ] `apply-firewall` не блокирует SSH на `host_firewall.ssh_port` (default `2323`). - [ ] После `apply-firewall` порт `host_firewall.amnezia_port` (default `443`) доступен по всем протоколам из `host_firewall.amnezia_protocols`. - [ ] Если эксплуатационно требуется закрыть legacy SSH `22`, это явно подтверждено проверкой доступа на `ssh_port` перед закрытием `22`. - [ ] При `host_firewall.enabled = false` удаляется только таблица `inet awg2_host_firewall`. - [ ] Повторный `apply-firewall` идемпотентен и не вносит лишних изменений в healthy state. - [ ] При невалидном `host_firewall.amnezia_protocols` (`icmp`, `foo`, дубли) `validate-config` завершается ошибкой. - [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой. - [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке. - [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика. - [ ] `status` показывает блок `Docker/Amnezia port path` с полями `docker_userland_proxy`, `published_udp_port` и note про Docker-managed `FORWARD`. - [ ] `status` показывает поля `host_forward_policy_drop` и `docker_forward_rules_present` в блоке `Docker/Amnezia port path`. - [ ] Если `docker_userland_proxy=false` и `FORWARD policy DROP`, при отсутствии явных Docker accept-признаков `status` выводит warning, но `awg2-harness` не изменяет `FORWARD` автоматически. - [ ] Сверено соответствие: `config host_firewall.amnezia_port` ↔ опубликованный Docker UDP host port ↔ `docker exec amnezia-awg2 awg show` (`listening port`). - [ ] При нескольких UDP bindings без однозначного соответствия `host_firewall.amnezia_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт. - [ ] Для диагностики FORWARD используется best-effort проверка: `iptables -S FORWARD` и fallback через `nft list ruleset`; `status` не падает при недоступности одной из команд.