383e67605a
The rollback still had a path that ended with two intact swapfiles on disk and neither of them running. With the original already off and moved to .previous, a failed rename back returned immediately - "the host may currently have no swap" - without trying to activate anything. Both candidates were sitting right there: the old swapfile under its .previous name, and the replacement that the earlier fix had preserved. That return threw away the whole point of preserving them. The rename back was never the requirement. The kernel activates a swap area by path and does not care that the path ends in .previous, so the old swapfile is brought up where it lies. The rollback now tracks both files by where they currently are rather than by a flag, and works down the candidates: original at the managed path, original at .previous, replacement wherever it ended up. Only when none of them activates does it report that the host has no managed swap - and then that statement is true. Two consequences fall out of tracking locations: - A replacement that could not be moved aside is still on the managed path, so the restore no longer renames the original over it. That rename would have unlinked one of the two files this rollback exists to keep. - When the original ends up serving from .previous the replacement is not released and nothing is deleted. The host has swap, so nothing is urgent, but the managed path does not hold what the ownership record describes - that is an operator's call, and the error names the location of both files and the commands that finish it. Three tests: the rename back fails and the previous swapfile is activated in place; it fails and the previous swapfile will not activate either, so the replacement takes over; a live replacement that cannot be released is left in place and nothing else is attempted. The last one needed a swapoff failure, which the fake could not express before. fix(daemon): let the docker events subsystem return to healthy [P1] The outer loop recorded an error every time the events stream ended, and nothing ever recorded a success for it. One Docker restart therefore pinned a failure in status permanently while the replacement stream was running normally. The reconnect now clears it and logs the recovery once. The per-subsystem transition logic moved onto LastActionState as record_ok/record_err, so it can be tested without the /run writes that set_ok/set_err perform. Clearing the last error also clears its timestamp - otherwise status showed an empty error next to the time it stopped being true. fix(swap): drop transition leftovers once the managed swap is confirmed live [P1] Leftovers were only cleared on the way *into* a resize. A process killed after the replacement went live left .previous on disk indefinitely: the next run finds a valid, active, correctly owned swapfile at the managed path, takes the fast path, and never looks. A swapfile is commonly sized after RAM, so that is not a rounding error. They are now dropped right after the managed swap is confirmed active, and only when inactive - an active leftover means the kernel is still swapping to it, which is the interrupted-rollback case that needs an operator. This also replaces two tests that asserted nothing about production code: both rebuilt the active-leftover check inline instead of calling it. Docs: the resize transition and its three degraded outcomes are now written down in IMPLEMENTATION_SPEC rather than living only in the code, the reconcile order gained the cleanup step, per-subsystem error tracking is described, and the acceptance checklist gained the fault cases to exercise on Debian - each one keyed to the exact wording the error must carry. cargo fmt --check, clippy -D warnings, cargo test --locked --workspace --release: 199 + 4 passed.
28 KiB
28 KiB
Чек-лист приёмки перед 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/<pid>/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:<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 qdisc2: htb. - На
ifb0есть root class2: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, default10. - 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 появляются новые class/filter.
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не остаётся с этой ошибкой навсегда. - Успешный 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показывает 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 и не уходит в цикл перезапусков (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. 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; текущий пример 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
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.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не используется. 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,amnezia-awg3или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(default2323).- После
apply-firewallпортhost_firewall.amnezia_port(default443) доступен по всем протоколам из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 restartanti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой. - Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (
INPUTvsFORWARD) и это отражено в эксплуатационной заметке. - Для release-кандидата зафиксирован итог проверки
INPUTvsFORWARDна реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика. statusпоказывает блокDocker/Amnezia port pathс полямиdocker_userland_proxy,published_udp_portи 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автоматически. - Сверено соответствие:
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не падает при недоступности одной из команд.