33 KiB
33 KiB
Чек-лист приёмки перед production-выкладкой
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 с несколькими 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:<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показывает aliasawg2-harness:traffic; чужой IFB без этого alias не удаляется и не заменяется.- На
ifb0есть root qdisc2: htb. - На
ifb0есть root class2: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, default10. 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. - Если сделать
ifb0DOWN, drift детектируется и topology восстанавливается. - Если удалить один peer filter, daemon восстанавливает topology.
- Если изменить
rate/ceilpeer 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 handles1:/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 активен, и отсутствует, если BBR неактивен./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наfalsekey удаляется из persistence; оператор учитывает, что текущее runtime-значение до reboot автоматически не сбрасывается. - До/после ручного включения backlog сняты
/proc/net/softnet_stat, CPU и latency под реальной нагрузкой. 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удаляется, а swapfile, его активность и запись в/etc/fstabостаются — после перезагрузки swap снова включится; удаляется только persistencevm.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 команда/старт завершается с ошибкой. - При
enabled=falsemanaged-файл удаляется только если содержит 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с hookforwardи policyaccept. - Правило ограничивает
ip saddr <peer_ipv4_cidr>, default10.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, chainsAWG2H_A/AWG2H_Bв обеих IP-family). - Известно, как удалить только AWG2-owned anti-torrent state (
inet awg2_harness) в выбранном namespace. - Известно, как удалить AWG2-owned
tctopology (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-sidewgне используется. - Пакет имеет 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или осознанный legacyamnezia-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(default2323).- Встроенная policy
INPUTне меняется; managed jump с commentawg2-harness:host-firewallстоит первым и ведёт в одну из цепочекAWG2H_A/AWG2H_Bдля IPv4 и IPv6. - Managed chain разрешает loopback, established/related, configured SSH и optional ICMP/ICMPv6, затем завершает обработку configured
DROP/ACCEPTverdict. - 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 и доказанно ownedAWG2H_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 на прежнюю цепочку. - После
docker restartanti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой. - Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (
INPUTvsFORWARD) и это отражено в эксплуатационной заметке. - Для release-кандидата зафиксирован итог проверки
INPUTvsFORWARDна реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика. statusпоказывает блокDocker/Amnezia port pathс полямиexpected_host_udp_port,docker_userland_proxy,published_udp_port,awg_listen_udp_port,port_contractи note про Docker-managedFORWARD.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не падает при недоступности одной из команд.