175 lines
14 KiB
Markdown
175 lines
14 KiB
Markdown
# Acceptance checklist перед production-выкладкой
|
||
|
||
## A. Базовая установка
|
||
|
||
- [ ] Хост — Debian 13 amd64.
|
||
- [ ] Сервер выделенный, без соседних сервисов.
|
||
- [ ] AmneziaWG/AWG 2.0 или AmneziaWG/AWG 1.5 установлена официальным способом.
|
||
- [ ] Контейнер `amnezia-awg2` или `amnezia-awg` запущен.
|
||
- [ ] `docker ps` видит контейнер.
|
||
- [ ] Можно получить PID контейнера.
|
||
- [ ] `/proc/<pid>/ns/net` существует.
|
||
- [ ] На хосте доступны `ip`, `tc`, `wg`, `nsenter`, `modprobe`.
|
||
- [ ] `awg2-harness validate-config` проходит без ошибок.
|
||
|
||
## B. Peer discovery
|
||
|
||
- [ ] `awg2-harness status` показывает найденный контейнер.
|
||
- [ ] `awg2-harness status` показывает PID/netns контейнера.
|
||
- [ ] `awg2-harness status` показывает найденный интерфейс.
|
||
- [ ] Количество peers совпадает с `wg show all allowed-ips`.
|
||
- [ ] Offline peers с allowed IP тоже попадают в список shaping.
|
||
- [ ] IP вне `amnezia.peer_ipv4_cidr` не попадают в traffic shaping.
|
||
|
||
## 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:<id>` с `traffic.peer_limit`.
|
||
- [ ] Для каждого peer есть `fq_codel` под `1:<id>`.
|
||
- [ ] Для каждого peer есть egress filter `dst peer_ip -> 1:<id>`.
|
||
- [ ] Есть 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:<id>` с `traffic.peer_limit`.
|
||
- [ ] Для каждого peer есть `fq_codel` под `2:<id>`.
|
||
- [ ] Для каждого peer есть ingress filter `src peer_ip -> 2:<id>`.
|
||
- [ ] 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` ничего не пересоздаёт и не добавляет дублей.
|
||
|
||
## I. Anti-torrent nftables guard
|
||
|
||
- [ ] Если `anti_torrent.enabled = true`, в Amnezia netns есть `table inet awg2_harness`.
|
||
- [ ] Есть 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`; текущий пример 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_<version>_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_<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. 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` и `amnezia-awg2` подключены к одной 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 (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 wg 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` не падает при недоступности одной из команд.
|