# Чек-лист приёмки перед 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`, `docker`, `nsenter`, `modprobe`. Хостовый `wg` не требуется и не проверяется. - [ ] `awg2-harness validate-config` проходит без ошибок. - [ ] `awg2-harness validate-runtime` проходит без ошибок и не требует установленного `wireguard-tools`. ## B. Обнаружение peers - [ ] `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 - [ ] `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. Идемпотентность и восстановление после drift - [ ] Повторный `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. Самовосстановление в жизненном цикле - [ ] `docker restart amnezia-awg2` → лимиты восстанавливаются автоматически. - [ ] `systemctl restart docker` → после возвращения контейнера лимиты восстанавливаются автоматически. - [ ] `systemctl restart awg2-harness` → сервис сам находит контейнер и применяет/проверяет лимиты. - [ ] Reboot хоста → сервис стартует и восстанавливает лимиты. - [ ] Изменение имени интерфейса на допустимое fallback-имя не ломает detector. - [ ] Если контейнер временно отсутствует, daemon логирует ошибку reconcile и продолжает работу. - [ ] На старте daemon сбой `sysctl_tuning` или `swap` завершает процесс, а сбой traffic/firewall только логируется — сервис остаётся живым. - [ ] После обрыва потока `docker events` он перезапускается автоматически (проверяется, например, `systemctl restart docker`). - [ ] Параллельный запуск `apply-limits` во время работы daemon не приводит к гонке: второй процесс ждёт блокировку до 10 секунд и затем сообщает `lock busy timeout`. - [ ] Устаревший lock-файл после `kill -9` не блокирует работу: следующий запуск перезахватывает `/run/awg2-harness/traffic.lock` по отсутствию `/proc/`. ## F. Статус и наблюдаемость - [ ] `status` показывает desired peer limit из конфига, а не hardcoded 70. - [ ] `status` показывает egress state. - [ ] `status` показывает ingress/ifb state. - [ ] `status` показывает WireGuard clients (online/offline, latest handshake, RX/TX, endpoint). - [ ] `status` показывает недостающие и устаревшие правила peers: в `Traffic actual` — строка `reason` (например, `missing egress class`, `egress per-peer class set mismatch`, `duplicate egress class flowid mapping detected`), в `peer_table` — колонка `drift` по каждому peer. - [ ] `status` явно показывает частичное состояние и drift, а не общий признак «всё хорошо». - [ ] `status` показывает tc filter counters по peer или `filter statistics: unavailable`. - [ ] Логи доступны через `journalctl -u awg2-harness`. - [ ] Отсутствие обязательной host-утилиты (`tc`, `ip`, `docker`, `nsenter`) логируется явно с именем утилиты. - [ ] Последний reconcile result виден в status или journal. - [ ] `status` показывает последние события reconcile/firewall из `/run/awg2-harness/events.log`. - [ ] Если контейнер не найден, `status` не падает, а переходит в degraded-режим: `Peers`/`WireGuard clients` показывают `unavailable`, `Traffic actual` — `drift: unknown`, а секции host firewall, anti-torrent, swap и conntrack продолжают выводиться. - [ ] Секции деградируют независимо: ошибка чтения peer state не мешает вывести traffic, firewall и swap. ## G. Планировщик перезагрузки - [ ] Если `reboot.enabled = false`, сервис не планирует reboot. - [ ] Если `reboot.enabled = true`, в journal видна запланированная перезагрузка. - [ ] Время попадает в окно `window_start_utc`–`window_end_utc`. - [ ] Вызов reboot выполняется через `systemctl --no-block reboot`. ## H. Swap - [ ] `ensure-swap` создаёт файл по пути `swap.path` (в примере `/swapfile`) размером `swap.size_mb` (в примере 2048 MB). - [ ] Права на swap-файл — `0600`. - [ ] Перед перезаписью `/etc/fstab` создана резервная копия `/etc/fstab.awg2-harness.bak`. - [ ] После первой правки существует `/etc/fstab.awg2-harness.original` и он **не** меняется при последующих запусках. - [ ] Нечитаемый `/etc/fstab` (например, `chmod 000`) приводит к явной ошибке, а не к перезаписи файла одной managed-строкой. - [ ] Отсутствующий `/etc/fstab` приводит к ошибке, а не к созданию нового файла. - [ ] Если резервную копию записать не удалось, `/etc/fstab` остаётся неизменным. - [ ] `status` при нечитаемом `/etc/fstab` показывает ошибку, а не «нет swap-записей». - [ ] Путь swap с пробелом в имени попадает в `/etc/fstab` экранированным (`\040`) и корректно монтируется. - [ ] Рядом с управляемым swapfile создан маркер `.awg2-harness-owned`. - [ ] Если до запуска был активен другой swap, после `ensure-swap` он выключен. - [ ] После `ensure-swap` активен ровно один swap: `/swapfile`. - [ ] Все старые swap entries удалены из `/etc/fstab`. - [ ] Чужой swap **не удаляется как файл** — ни обычный файл, ни block device; выполняется только `swapoff` и удаление записи из `/etc/fstab`. - [ ] Существующий по `swap.path` файл без маркера владения приводит к явной ошибке, а не к пересозданию; его содержимое остаётся нетронутым. - [ ] Если создание или активация нового swap провалились, ранее активный swap **остаётся включённым** (проверяется, например, недостижимым размером `swap.size_mb`). - [ ] `swap.path` с `..` (`/etc/../etc/fstab`), с пробелом, с управляющим символом или под `/proc`, `/sys`, `/dev` отвергается на `validate-config`. - [ ] `swap.path`, у которого один из родительских каталогов — symlink, отвергается на `ensure-swap`. - [ ] `vm.swappiness` из конфига применён в runtime и сохранён persistently. - [ ] Повторный `ensure-swap` ничего не пересоздаёт и не добавляет дублей. ## H2. Сетевой baseline через sysctl - [ ] `awg2-harness apply-network-tuning` успешно завершается. - [ ] `awg2-harness inspect-network-tuning` показывает `enabled: true` и `drift: no`. - [ ] Создаётся `/etc/sysctl.d/90-awg2-harness-vpn.conf`. - [ ] `/etc/modules-load.d/90-awg2-harness-bbr.conf` создан, если BBR активен, и отсутствует, если BBR неактивен. - [ ] `/etc/sysctl.conf` не изменён. - [ ] В обоих managed-файлах первая строка содержит marker `Managed by awg2-harness sysctl_tuning`. - [ ] Файл без marker по managed-пути не перезаписывается и не удаляется: команда завершается ошибкой. - [ ] В managed-файле отсутствует `udp_wmem_min`. - [ ] `net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` присутствуют в managed-файле только при активном BBR. - [ ] `swap` пишет `vm.swappiness` в `/etc/sysctl.d/90-awg2-harness-swap.conf`, а не в `99-awg2-harness.conf`. - [ ] При обновлении с прежней версии `/etc/sysctl.d/99-awg2-harness.conf` удаляется, если несёт маркер `# Managed by awg2-harness`. - [ ] Файл по пути `99-awg2-harness.conf` без этого маркера остаётся нетронутым и не ломает `ensure-swap`. - [ ] При `swap.enabled = false` файл `90-awg2-harness-swap.conf` удаляется, активный swap **не** выключается, runtime `vm.swappiness` не переопределяется. - [ ] Любой managed-файл под `/etc`, у которого первая строка не содержит маркер владельца, не перезаписывается и не удаляется. - [ ] 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 guard на nftables - [ ] Если `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. Откат - [ ] Известно, как остановить и отключить `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. Упаковка и релиз - [ ] 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. Документация и 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 (порты и безопасность SSH) - [ ] `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` не падает при недоступности одной из команд.