Files
AWG2_harness/docs/ACCEPTANCE_CHECKLIST.md
T

36 KiB
Raw Blame History

Чек-лист приёмки перед production-выкладкой

Исторический synthetic validation и выполненный clean production acceptance версии 0.3.1 зафиксированы в VALIDATION_0.3.1.md. Этот checklist остаётся воспроизводимой процедурой приёмки для следующих установок.

Clean production acceptance release artifact v0.3.1 выполнен 2026-08-29 на Debian 13 + AmneziaWG 3.1. Итог: PASS; production GO подтверждён. Не наблюдались только фактический scheduler-triggered reboot и повторный multi-peer load-test; anti-torrent threshold выше 500 намеренно не создавался.

A. Базовая установка

  • Хост — Debian 13 amd64.
  • Сервер выделенный, без соседних сервисов.
  • AmneziaWG 3.1 установлена официальным self-hosted способом через AmneziaVPN 5.0.1.5 или новее.
  • Контейнер amnezia-awg2 запущен; amnezia-awg допустим только как осознанный legacy fallback.
  • docker ps видит контейнер.
  • Можно получить PID контейнера.
  • /proc/<pid>/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:<id> с traffic.peer_limit.
  • Для каждого peer есть fq_codel под 1:<id>.
  • Для каждо peer есть egress filter flower pref 10 dst_ip peer_ip/32 -> 1:<id>.
  • Есть 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:<id> с traffic.peer_limit.
  • Для каждого peer есть fq_codel под 2:<id>.
  • Для каждого peer есть ingress filter flower pref 10 src_ip peer_ip/32 -> 2:<id>.
  • Все 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 <swap.path>.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 <peer_ipv4_cidr>, 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_<version>_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_<version>_amd64.deb не устанавливает и не заменяет Docker.
  • apt install ./awg2-harness_<version>_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-запрос к <server-public-ip>: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 <n>/-D INPUT <n> не используются.
  • Чужое правило, конкурентно вставленное первым в 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:<port> и [::]:<port>; 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 <container> awg show <interface> 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 не падает при недоступности одной из команд.