# Чек-лист приёмки перед production-выкладкой Исторический synthetic validation для версии 0.3.1 зафиксирован в [`VALIDATION_0.3.1.md`](VALIDATION_0.3.1.md). Он подтверждает перечисленные там проверки, но не заменяет этот production acceptance checklist. ## A. Базовая установка - [ ] Хост — Debian 13 amd64. - [ ] Сервер выделенный, без соседних сервисов. - [ ] AmneziaWG 3.1 установлена официальным self-hosted способом через AmneziaVPN 5.0.1.5 или новее. - [ ] Контейнер `amnezia-awg2` запущен; `amnezia-awg` допустим только как осознанный legacy fallback. - [ ] `docker ps` видит контейнер. - [ ] Можно получить PID контейнера. - [ ] `/proc//ns/net` существует. - [ ] На хосте доступны `ip`, `tc`, `docker`, `nsenter`, `modprobe`. Хостовый `wg` не требуется и не проверяется. - [ ] `awg2-harness validate-config` проходит без ошибок. - [ ] `awg2-harness validate-runtime` проходит без ошибок и не требует установленного `wireguard-tools`. ## B. Обнаружение peers - [ ] `C=amnezia-awg2` соответствует фактически запущенному контейнеру. - [ ] `docker exec "$C" awg --version` зафиксирован в acceptance evidence. - [ ] `docker exec "$C" ip -o link show awg0` находит основной интерфейс. - [ ] `docker exec "$C" ip -d link show awg0` фиксирует фактический kernel/userspace runtime без требования конкретной реализации. - [ ] `docker exec "$C" awg show all dump | awk -F '\t' '{print NR, NF, $1}'` для reference AWG 3.1 tools показывает 30 полей interface-row и 9 полей peer-row. - [ ] Width используется только как acceptance evidence; parser не определяет по нему версию и не классифицирует peer. - [ ] `awg2-harness status` показывает найденный контейнер. - [ ] `awg2-harness status` показывает PID/netns контейнера. - [ ] `awg2-harness status` показывает найденный интерфейс. - [ ] Количество peers совпадает с `docker exec "$C" awg show all allowed-ips`. - [ ] Каждый peer имеет ровно один server-side IPv4 `/32` внутри `amnezia.peer_ipv4_cidr`; `(none)` допустим и не создаёт класс. - [ ] IPv4+IPv6, IPv6-only, несколько allowed IP, маска не `/32` и адрес вне configured CIDR дают явную `unsupported topology` до изменения traffic/anti-torrent state. - [ ] Одинаковый отказ проверен для `show all allowed-ips`, fallback `show` и `show all dump`; AWG 3.1 interface-row не ошибочно принимается за peer. - [ ] Offline peers с allowed IP тоже попадают в список shaping. - [ ] IP вне `amnezia.peer_ipv4_cidr` не игнорируется молча: весь topology reconcile завершается ошибкой. - [ ] В секции `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 `flower pref 10 dst_ip peer_ip/32 -> 1:`. - [ ] Есть ingress qdisc `ffff:` на AWG/WG-интерфейсе. - [ ] Есть ingress redirect через `mirred ... redirect dev ifb0`. - [ ] `ifb0` существует и находится в UP state. - [ ] `ip -details link show ifb0` показывает alias `awg2-harness:traffic`; чужой IFB без этого alias не удаляется и не заменяется. - [ ] На `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 `flower pref 10 src_ip peer_ip/32 -> 2:`. - [ ] Все peer-фильтры на каждом parent используют один `pref 10`; `u32` остаётся только у catch-all ingress redirect. - [ ] Class ids начинаются с `traffic.class_start_id`, default `10`. - [ ] `class_start_id` и последний фактический peer class остаются в textual-диапазоне `10..=9999`; конфигурация/число peers, требующие `10000`, отвергаются до apply. - [ ] 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 вся AWG2-owned traffic topology удаляется; повторный reconcile не выполняет teardown снова. - [ ] После добавления peer появляются новые class/filter. - [ ] Legacy peer-фильтры `u32` детектируются как migration drift и заменяются на `flower` через rebuild. - [ ] Ошибка в любой строке `tc -batch -` останавливает apply; после успеха проходит verify-after-apply. - [ ] Ошибка после частичного `tc -batch -` запускает cleanup только qdisc handles `1:`/`ffff:` и IFB с доказанным владением; в сообщении сохранены и primary, и cleanup error. - [ ] Чужой root qdisc, `clsact` или IFB с тем же именем вызывает явный ownership conflict и остаётся нетронутым. ## E. Самовосстановление в жизненном цикле - [ ] `docker restart amnezia-awg2` → лимиты восстанавливаются автоматически. - [ ] `systemctl restart docker` → после возвращения контейнера лимиты восстанавливаются автоматически. - [ ] `systemctl restart awg2-harness` → сервис сам находит контейнер и применяет/проверяет лимиты. - [ ] `systemd-analyze verify` проходит для обоих unit-файлов на целевом Debian 13. - [ ] Правка `config.toml` без restart не меняет поведение; после `systemctl restart awg2-harness.service` изменения применяются. - [ ] `ensure-swap` за один старт сервиса выполняется ровно один раз (в journal нет двух подряд swap-reconcile). - [ ] При устойчивом падении демона интервал перезапусков растёт (10 → 20 → 40 → 80 → 160 с), а не остаётся 10 с. - [ ] Reboot хоста → сервис стартует и восстанавливает лимиты. - [ ] Изменение имени интерфейса на допустимое fallback-имя не ломает detector. - [ ] Если контейнер временно отсутствует, daemon логирует ошибку reconcile и продолжает работу. - [ ] На старте daemon сбой `sysctl_tuning` или `swap` завершает процесс, а сбой traffic/firewall только логируется — сервис остаётся живым. - [ ] После обрыва потока `docker events` он перезапускается автоматически (проверяется, например, `systemctl restart docker`). - [ ] После восстановления потока `docker events` ошибка подсистемы снимается: в `events.log` появляется `docker_events=recovered`, а `last_action.state` не остаётся с этой ошибкой навсегда. - [ ] Readiness retry после start event повторяется только для typed `NotFound`; Docker permission/API/parse error немедленно отражается как ошибка, timeout не превращается в silent skip. - [ ] Start event lower-priority контейнера игнорируется, если уже выбран running container с более высоким приоритетом в `amnezia.container_names`. - [ ] Один startup/periodic/event-цикл использует один discovery `RuntimeTarget` для traffic и anti-torrent, а не повторяет `docker inspect` и interface discovery. - [ ] Успешный reconcile одной подсистемы не гасит незакрытую ошибку другой: при постоянно падающем `traffic` успешный `firewall` не очищает `last_error` и не пишет `firewall=recovered`. - [ ] Параллельный запуск `apply-limits` во время работы daemon не приводит к гонке: второй процесс ждёт блокировку до 10 секунд и затем сообщает `lock busy timeout`. - [ ] После `kill -9` блокировка освобождается ядром автоматически; оставшийся lock-файл не мешает следующему захвату `/run/awg2-harness/traffic.lock`. ## 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` показывает egress/ingress packet counters из `tc -s class show` по peer class или `n/a`; class-only flower action statistics не используются. - [ ] Логи доступны через `journalctl -u awg2-harness`. - [ ] Повтор одной и той же reconcile-ошибки не создаёт `error`-запись на каждом цикле; первая ошибка, изменение её текста и recovery видны как отдельные переходы. - [ ] Отсутствие обязательной 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 и **не** уходит в цикл перезапусков (`systemctl status` показывает завершение с кодом 0, а не рестарты). - [ ] Если `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`) и корректно монтируется. - [ ] Создан `/var/lib/awg2-harness/swap-ownership.conf` с `device`, `inode` и путём; каталог `/var/lib/awg2-harness` имеет режим `0700` и принадлежит root. - [ ] Старый sidecar `.awg2-harness-owned` после обновления удалён и больше не создаётся. - [ ] Если swapfile удалить вручную и создать по тому же пути другой файл, `ensure-swap` завершается ошибкой `StaleOwnership` и **не** удаляет новый файл. - [ ] `swap.path` вне `/swapfile` и `/var/lib/awg2-harness/` отвергается на `validate-config` (проверить `/tmp/awg.swap`). - [ ] Если до запуска был активен другой 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 желателен по configured policy и доступен, и отсутствует при `tcp_bbr = off`/недоступном `auto`. - [ ] `/etc/sysctl.conf` не изменён. - [ ] В обоих managed-файлах первая строка содержит marker `Managed by awg2-harness sysctl_tuning`. - [ ] Файл без marker по managed-пути не перезаписывается и не удаляется: команда завершается ошибкой. - [ ] В managed-файле отсутствует `udp_wmem_min`. - [ ] В shipped/default config `netdev_backlog_enabled = false`, `netdev_max_backlog = 10000`, а `net.core.netdev_max_backlog` отсутствует в managed-файле. - [ ] При смене `netdev_backlog_enabled = true` на `false` key удаляется из persistence; оператор учитывает, что текущее runtime-значение до reboot автоматически не сбрасывается. - [ ] До/после ручного включения backlog сняты `/proc/net/softnet_stat`, CPU и latency под реальной нагрузкой. - [ ] В shipped/default config `tcp_bbr = off`, `default_qdisc = fq`, `udp_rmem_min_enabled = false`; `default_qdisc = fq_codel` отвергается валидацией. - [ ] `net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` присутствуют в managed-файле только когда BBR желателен по policy. - [ ] Status отдельно показывает `bbr_available`, `bbr_desired` и фактический `bbr_runtime_active`; при available BBR и runtime `cubic` последнее поле равно `no`. - [ ] При `tcp_bbr = off` уже активный runtime BBR не выдаётся за desired и не маскируется как неактивный. - [ ] `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` удаляется, а swapfile, его активность и запись в `/etc/fstab` **остаются** — после перезагрузки swap снова включится; удаляется только persistence `vm.swappiness`. - [ ] Изменение только `swap.priority` не пересоздаёт swapfile (проверяется по неизменившемуся mtime файла). - [ ] При изменении `swap.size_mb` на активном swap хост не остаётся без swap во время `mkswap --file`: в `/proc/swaps` всё время присутствует хотя бы одна запись. - [ ] Если `swapoff` второго чужого swap падает, ранее отключённые чужие swap возвращаются в активное состояние. - [ ] При откате resize swap возвращается с **прежним** priority, а не с новым (проверяется заведомо неприменимым `swap.priority`). - [ ] Kill процесса в середине resize: следующий запуск с активным `.new` или `.previous` даёт понятную ошибку с командой `swapoff`, а не бесконечно падает на удалении активного файла. - [ ] Kill процесса **после** активации нового swapfile: следующий `ensure-swap` проходит быстрым путём и удаляет неактивный `.previous`; активный `.previous` при этом не трогается. - [ ] После отката в `/proc/swaps` присутствует ровно исходный swap; `.new` отключён и удалён. - [ ] Откат при недоступном `rename` обратно (например, основной путь занят каталогом): исходный swapfile активируется прямо по пути `.previous`, в `/proc/swaps` есть запись, ошибка содержит `is serving from` и путь обоих файлов. - [ ] Откат при неподнимающемся исходном swapfile: активируется отложенная замена, ошибка содержит `is left active so the host is not without swap`; ни один из файлов не удалён. - [ ] Откат при активной замене, которую не удаётся отключить: файлы не перемещаются, ошибка содержит `is left in place and serving`, замена продолжает обслуживать. - [ ] Формулировка «хост без управляемого swap» появляется только тогда, когда в `/proc/swaps` действительно нет ни одной managed-записи. - [ ] Нечитаемый managed sysctl-файл даёт в `status` причину `managed sysctl file is unreadable`, а не «файл отсутствует». - [ ] При `reboot.enabled = false` файл `/run/awg2-harness/reboot.plan` удалён, `status` не показывает устаревший план. - [ ] После принятия запроса на перезагрузку `reboot.plan` удалён. - [ ] Любой managed-файл под `/etc`, у которого первая строка не содержит маркер владельца, не перезаписывается и не удаляется. - [ ] Runtime apply меняет только drifted keys (`sysctl -w`), без `sysctl --system`. - [ ] При `tcp_bbr = auto` и недоступном BBR сервис не падает, в статусе есть warning. - [ ] При `tcp_bbr = require` и недоступном BBR команда/старт завершается с ошибкой. - [ ] Sustained per-peer cap проверен в двух конфигурациях, BBR-off и BBR-on; HTB `rate == ceil` даёт одинаковый лимит. - [ ] При `enabled=false` managed-файл удаляется только если содержит managed marker. ## I. Anti-torrent guard на nftables - [ ] Если `anti_torrent.enabled = true`, в Amnezia netns есть `table inet awg2_harness`. - [ ] На **хосте** таблицы `inet awg2_harness` при `apply_in_amnezia_netns = true` нет: `nft list table inet awg2_harness` в host namespace возвращает ошибку «No such file or directory». - [ ] При недоступном Docker `apply-firewall` с `anti_torrent.enabled = false` завершается ошибкой, а не молчаливым успехом. - [ ] `systemctl stop docker` не останавливает `awg2-harness.service` (проверка отсутствия `Requires=docker.service`). - [ ] Есть 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`; shipped production baseline — `500`. - [ ] В фактической nft rule присутствует `ct count over 500` для shipped config. - [ ] 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` показывает `count_namespace`, `nf_conntrack_count`, глобальный `nf_conntrack_max_global` и utilization; при netns anti-torrent count читается внутри Amnezia, а max — на host. ## J. Откат - [ ] Известно, как остановить и отключить `awg2-harness.service` и `awg2-reboot-scheduler.service`. - [ ] Известно, как удалить только AWG2-owned host firewall state (comment jump `awg2-harness:host-firewall`, chains `AWG2H_A`/`AWG2H_B` в обеих IP-family). - [ ] Известно, как удалить только 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`. - [ ] Release package без `--version` получает версию `0.3.1` из `traffic_core/Cargo.toml`. - [ ] `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 внешняя. - [ ] Пакет не имеет `Depends` на `wireguard-tools`: peer state читается внутри контейнера, host-side `wg` не используется. - [ ] Пакет имеет runtime `Depends` на `iptables` и `nftables`. - [ ] `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` или осознанный legacy `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`). - [ ] Встроенная policy `INPUT` не меняется; managed jump с comment `awg2-harness:host-firewall` стоит первым и ведёт в одну из цепочек `AWG2H_A`/`AWG2H_B` для IPv4 и IPv6. - [ ] Managed chain разрешает loopback, established/related, configured SSH и optional ICMP/ICMPv6, затем завершает обработку configured `DROP`/`ACCEPT` verdict. - [ ] Host firewall не добавляет allow-rule для published Amnezia UDP port: bridge publish проходит через Docker-managed `FORWARD`/NAT. - [ ] Если эксплуатационно требуется закрыть legacy SSH `22`, это явно подтверждено проверкой доступа на `ssh_port` перед закрытием `22`. - [ ] При `host_firewall.enabled = false` удаляются только помеченные jumps и доказанно owned `AWG2H_A`/`AWG2H_B`; чужие rules/chains остаются нетронутыми. - [ ] Повторный `apply-firewall` идемпотентен и не вносит лишних изменений в healthy state. - [ ] Legacy fields `host_firewall.amnezia_port`/`amnezia_protocols` отвергаются как `unknown field`. - [ ] Новая конфигурация строится в inactive A/B chain до switch; искусственный сбой `ip6tables` после IPv4 switch возвращает IPv4 на прежнюю цепочку. - [ ] Managed jumps переключаются только через insert-new + exact-spec delete; numeric `-R INPUT `/`-D INPUT ` не используются. - [ ] Чужое правило, конкурентно вставленное первым в `INPUT`, не заменяется и не удаляется; rollback удаляет только новый managed jump. - [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой. - [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке. - [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика. - [ ] `status` показывает блок `Docker/Amnezia port path` с полями `expected_host_udp_port`, `docker_userland_proxy`, `published_udp_port`, `awg_listen_udp_port`, `port_contract` и note про Docker-managed `FORWARD`. - [ ] В том же блоке перечислены все UDP `host_bindings`, включая одновременно `0.0.0.0:` и `[::]:`; IPv6 выводится в квадратных скобках. - [ ] Docker routed binding с пустым `HostPort` сохраняется в диагностике и не превращается в выдуманный numeric port. - [ ] `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` автоматически. - [ ] Сверено соответствие: `amnezia.expected_host_udp_port` ↔ опубликованный Docker UDP host port ↔ `docker exec awg show listen-port`; `port_contract: ok`. - [ ] При нескольких UDP bindings без однозначного соответствия `amnezia.expected_host_udp_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт. - [ ] Для диагностики FORWARD используется best-effort проверка: `iptables -S FORWARD` и fallback через `nft list ruleset`; `status` не падает при недоступности одной из команд.