From d98ade42cea9234d835c9d319d158122522a1e20 Mon Sep 17 00:00:00 2001 From: Crimson Date: Thu, 6 Aug 2026 16:45:00 +0500 Subject: [PATCH] docs: unify language contract to Russian and reconcile the spec with the code The docs folder had drifted into a mixed-language contract: IMPLEMENTATION_SPEC.md was entirely in English while the rest was Russian with English headings. Prose is now Russian throughout; English is kept only where it is technically load-bearing (commands, tool output, config keys, file/module/table names, tc handles) and for terms with no settled Russian equivalent in this context (drift, reconcile, peer, namespace, handshake). docs/README.md now states that contract explicitly. Rewriting the spec surfaced several places where it described intent rather than the code. Corrected against traffic_core/src: - module layout: the spec showed traffic/ and firewall/ as directories with model/inspect/apply submodules. They are single files, and sysctl_tuning.rs was missing from the tree entirely. - data model: Peer was documented as carrying public key, handshake and counters. It carries only ipv4; runtime state lives in a separate PeerRuntime, and there is no desired-state struct at all - it is derived inline from config plus sorted peers. RuntimeTarget.netns_path is a String, not a PathBuf. - interface detection: the spec listed a third step taking the first non-loopback interface with an IPv4 address. No such fallback exists; detection bails with an explicit error, which is safer and is now documented as deliberate. - reconcile: the pseudo-flow folded firewall into reconcile_traffic. They are two independent reconciles with separate locks. - locking: only traffic.lock was documented. There are three locks, and sysctl_tuning.lock has a 30s timeout rather than 10s. Stale-lock recovery via /proc/ was undocumented. - sysctl_tuning: the module also manages /etc/modules-load.d/90-awg2-harness-bbr.conf when BBR is active, and refuses to touch either managed file without its marker. Both docs and the acceptance checklist claimed only the sysctl.d file was written. - status contract: the documented output was outdated. All 15 printed sections are now listed in the order status.rs emits them, including the degraded path when no container is found, and the fact that the Network tuning block is printed between the System service line and the container lines. - swap: size was documented as a constant 2048 MB instead of swap.size_mb, and the /etc/fstab.awg2-harness.bak backup was undocumented. - apply/verify: the tolerated-error lists are now the actual closed sets from the code. Acceptance checklist gained checks for the daemon startup failure policy (sysctl/swap fatal, traffic/firewall not), lock contention and stale-lock recovery, degraded status, the modules-load file, and the marker guard. The stale host wg requirement was dropped from its host-tools item. Every path, table, counter and log string quoted in the docs was verified to exist in the source. cargo test --workspace: 110 + 4 passed. --- docs/ACCEPTANCE_CHECKLIST.md | 58 ++-- docs/ANTI_TORRENT.md | 8 +- docs/CONFIGURATION.md | 3 + docs/IMPLEMENTATION_SPEC.md | 656 ++++++++++++++++++----------------- docs/INSTALLATION.md | 36 +- docs/PRODUCTION_EXAMPLE.md | 4 +- docs/README.md | 20 +- 7 files changed, 419 insertions(+), 366 deletions(-) diff --git a/docs/ACCEPTANCE_CHECKLIST.md b/docs/ACCEPTANCE_CHECKLIST.md index cd2b82f..71c3ac9 100644 --- a/docs/ACCEPTANCE_CHECKLIST.md +++ b/docs/ACCEPTANCE_CHECKLIST.md @@ -1,4 +1,4 @@ -# Acceptance checklist перед production-выкладкой +# Чек-лист приёмки перед production-выкладкой ## A. Базовая установка @@ -9,10 +9,11 @@ - [ ] `docker ps` видит контейнер. - [ ] Можно получить PID контейнера. - [ ] `/proc//ns/net` существует. -- [ ] На хосте доступны `ip`, `tc`, `nsenter`, `modprobe`. Хостовый `wg` не требуется. +- [ ] На хосте доступны `ip`, `tc`, `docker`, `nsenter`, `modprobe`. Хостовый `wg` не требуется и не проверяется. - [ ] `awg2-harness validate-config` проходит без ошибок. +- [ ] `awg2-harness validate-runtime` проходит без ошибок и не требует установленного `wireguard-tools`. -## B. Peer discovery +## B. Обнаружение peers - [ ] `awg2-harness status` показывает найденный контейнер. - [ ] `awg2-harness status` показывает PID/netns контейнера. @@ -24,7 +25,7 @@ - [ ] В секции `WireGuard clients` вывода `status` нет строки, соответствующей самому интерфейсу (столбец `Public key` не содержит приватный ключ устройства). - [ ] `docker exec "$C" readlink -f /usr/bin/wg` возвращает `/usr/bin/awg`. -## C. Traffic topology contract +## C. Контракт топологии traffic - [ ] `awg2-harness apply-limits` успешно завершается. - [ ] На AWG/WG-интерфейсе есть root qdisc `1: htb`. @@ -43,7 +44,7 @@ - [ ] Class ids начинаются с `traffic.class_start_id`, default `10`. - [ ] Peer IP → class id mapping использует стабильный лексикографический контракт: IP сортируются как строки, не numeric IPv4. -## D. Idempotency and drift recovery +## D. Идемпотентность и восстановление после drift - [ ] Повторный `apply-limits` не создаёт дубли. - [ ] Повторный `apply-limits` не меняет healthy state без причины. @@ -58,30 +59,36 @@ - [ ] После удаления peer устаревшие class/filter исчезают через rebuild. - [ ] После добавления peer появляются новые class/filter. -## E. Auto-healing lifecycle +## E. Самовосстановление в жизненном цикле - [ ] `docker restart amnezia-awg2` → лимиты восстанавливаются автоматически. - [ ] `systemctl restart docker` → после возвращения контейнера лимиты восстанавливаются автоматически. - [ ] `systemctl restart awg2-harness` → сервис сам находит контейнер и применяет/проверяет лимиты. - [ ] Reboot хоста → сервис стартует и восстанавливает лимиты. - [ ] Изменение имени интерфейса на допустимое fallback-имя не ломает detector. -- [ ] Если контейнер временно отсутствует, daemon логирует ожидание и продолжает работу. +- [ ] Если контейнер временно отсутствует, daemon логирует ошибку reconcile и продолжает работу. +- [ ] На старте daemon сбой `sysctl_tuning` или `swap` завершает процесс, а сбой traffic/firewall только логируется — сервис остаётся живым. +- [ ] После обрыва потока `docker events` он перезапускается автоматически (проверяется, например, `systemctl restart docker`). +- [ ] Параллельный запуск `apply-limits` во время работы daemon не приводит к гонке: второй процесс ждёт блокировку до 10 секунд и затем сообщает `lock busy timeout`. +- [ ] Устаревший lock-файл после `kill -9` не блокирует работу: следующий запуск перезахватывает `/run/awg2-harness/traffic.lock` по отсутствию `/proc/`. -## F. Status and observability +## 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` показывает missing/stale peer rules. -- [ ] `status` явно показывает partial/drift state. +- [ ] `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`. -- [ ] Ошибка отсутствия `tc`/`ip`/`wg` логируется явно. +- [ ] Отсутствие обязательной 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 scheduler +## G. Планировщик перезагрузки - [ ] Если `reboot.enabled = false`, сервис не планирует reboot. - [ ] Если `reboot.enabled = true`, в journal видна запланированная перезагрузка. @@ -90,28 +97,33 @@ ## H. Swap -- [ ] `ensure-swap` создаёт `/swapfile` размером 2048 MB. -- [ ] Права `/swapfile` — `0600`. +- [ ] `ensure-swap` создаёт файл по пути `swap.path` (в примере `/swapfile`) размером `swap.size_mb` (в примере 2048 MB). +- [ ] Права на swap-файл — `0600`. +- [ ] Перед перезаписью `/etc/fstab` создана резервная копия `/etc/fstab.awg2-harness.bak`. - [ ] Если до запуска был активен другой swap, после `ensure-swap` он выключен. - [ ] После `ensure-swap` активен ровно один swap: `/swapfile`. - [ ] Все старые swap entries удалены из `/etc/fstab`. - [ ] Для block-device swap выполняется только `swapoff` и удаление `fstab` entry; block device не удаляется. -- [ ] `vm.swappiness = 1` применён. +- [ ] `vm.swappiness` из конфига применён в runtime и сохранён persistently. - [ ] Повторный `ensure-swap` ничего не пересоздаёт и не добавляет дублей. -## H2. Sysctl network tuning +## 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`. -- [ ] В managed-файле есть marker `Managed by awg2-harness sysctl_tuning`. +- [ ] Создаётся `/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. - [ ] Runtime apply меняет только drifted keys (`sysctl -w`), без `sysctl --system`. - [ ] При `tcp_bbr = auto` и недоступном BBR сервис не падает, в статусе есть warning. - [ ] При `tcp_bbr = require` и недоступном BBR команда/старт завершается с ошибкой. - [ ] При `enabled=false` managed-файл удаляется только если содержит managed marker. -## I. Anti-torrent nftables guard +## I. Anti-torrent guard на nftables - [ ] Если `anti_torrent.enabled = true`, в Amnezia netns есть `table inet awg2_harness`. - [ ] Есть chain `forward_antitorrent` с hook `forward` и policy `accept`. @@ -125,7 +137,7 @@ - [ ] При `anti_torrent.enabled = false` удаляется/отключается только AWG2-owned nftables state; Docker/Amnezia rules не flush-ятся. - [ ] `status` показывает `nf_conntrack_count` / `nf_conntrack_max`, если sysctl доступны. -## J. Rollback +## J. Откат - [ ] Известно, как остановить и отключить `awg2-harness.service` и `awg2-reboot-scheduler.service`. - [ ] Известно, как удалить только AWG2-owned host firewall state (`inet awg2_host_firewall`). @@ -134,7 +146,7 @@ - [ ] При необходимости rollback включает удаление пакета `awg2-harness` после остановки сервисов. - [ ] Rollback проверен на тестовом сервере. -## K. Packaging / release +## K. Упаковка и релиз - [ ] Package собирается на отдельной Debian 13 amd64 build-machine. - [ ] Production host не требует Rust/cargo/source tree. @@ -150,7 +162,7 @@ - [ ] После ручной установки config `awg2-harness validate-config` проходит. - [ ] Сервис стартует только после явного `systemctl enable --now awg2-harness.service`. -## K2. Documentation / install UX +## K2. Документация и UX установки - [ ] README не утверждает, что AmneziaVPN ожидает public key. - [ ] Документация явно разделяет private key для AmneziaVPN и public key для `authorized_keys`. @@ -169,7 +181,7 @@ - [ ] DNS-резолвинг у VPN-клиента работает при активном подключении. - [ ] AWG2 host firewall не открывает `53/tcp` и `53/udp`. -## L. Host firewall (ports/SSH safety) +## L. Host firewall (порты и безопасность SSH) - [ ] `awg2-harness validate-config` падает, если отсутствует секция `[host_firewall]`. - [ ] `apply-firewall` не блокирует SSH на `host_firewall.ssh_port` (default `2323`). diff --git a/docs/ANTI_TORRENT.md b/docs/ANTI_TORRENT.md index c320304..26fa8d3 100644 --- a/docs/ANTI_TORRENT.md +++ b/docs/ANTI_TORRENT.md @@ -1,4 +1,4 @@ -# Anti-torrent connection-count guard +# Anti-torrent: ограничение числа соединений Этот модуль — часть текущего production-контракта AWG2 Harness. Он вписывается в multi-orchestrator модель, потому что сервер выделенный, а бинарь уже владеет AWG2-owned network state вокруг Amnezia: `tc`, `ifb0`, lifecycle recovery и диагностика. @@ -25,7 +25,7 @@ nft add rule inet filter forward ip saddr 10.8.1.0/24 ct count over 200 drop Нужен per-source key через meter/set и отдельная таблица, которой владеет только AWG2 Harness. -## 3. Recommended nftables shape +## 3. Рекомендуемая форма правила nftables Правило должно применяться в том же network namespace, где живёт Amnezia AWG/WG interface и где уже применяются `tc`/`ifb0` rules. На практике это namespace контейнера Amnezia, полученный через `/proc//ns/net`. @@ -63,7 +63,7 @@ table inet awg2_harness { `ct state new` важен: ограничиваются новые flow attempts сверх порога, а уже установленные flows не начинают внезапно дропаться только потому, что счётчик стал высоким. -## 4. Threshold +## 4. Порог Рекомендуемый стартовый production baseline: @@ -104,7 +104,7 @@ nft list table inet awg2_harness Если `nf_conntrack_count` приближается к `nf_conntrack_max`, проблема уже системная: надо разбираться с timeout, abuse или capacity. Не надо автоматически увеличивать `nf_conntrack_max` из orchestrator без отдельного решения. -## 7. Acceptance criteria +## 7. Критерии приёмки - В правильном Amnezia netns существует `table inet awg2_harness`. - В таблице есть `chain forward_antitorrent` с hook `forward`. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e75d4b3..5cecd8e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -119,6 +119,9 @@ swappiness = 1 `sysctl_tuning` — отдельный host-level модуль управляемого сетевого baseline через `sysctl`: - persistent-файл: `/etc/sysctl.d/90-awg2-harness-vpn.conf`; +- дополнительный managed-файл `/etc/modules-load.d/90-awg2-harness-bbr.conf` создаётся только при фактически активном BBR и удаляется, когда BBR неактивен; +- оба managed-файла несут в первой строке маркер `# Managed by awg2-harness sysctl_tuning`; файл без маркера не перезаписывается и не удаляется, вместо этого возвращается ошибка; +- `net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` управляются только при активном BBR; - runtime apply: только `sysctl -w` и только для drifted keys; - на установке `.deb` модуль не применяется; - на старте daemon модуль применяется, если `enabled=true` и `apply_on_daemon_start=true`; diff --git a/docs/IMPLEMENTATION_SPEC.md b/docs/IMPLEMENTATION_SPEC.md index e06591b..dd85a7a 100644 --- a/docs/IMPLEMENTATION_SPEC.md +++ b/docs/IMPLEMENTATION_SPEC.md @@ -1,45 +1,47 @@ -# AWG2 Harness implementation spec +# Спецификация реализации AWG2 Harness -## 1. Architectural role +Документ описывает архитектуру и контракт reconciliation. Он отражает фактическое состояние кода, а не только замысел: расхождения между этим документом и `traffic_core/src` считаются дефектом документа. -AWG2 Harness production package contains two Rust binaries: +## 1. Архитектурная роль -- `awg2-harness` — host-side traffic/firewall/status/swap orchestrator; -- `awg2-reboot-scheduler` — dedicated reboot scheduler service. +Production-пакет AWG2 Harness содержит два Rust-бинаря: -`awg2-harness` remains the main host-side multi-orchestrator for a dedicated Debian 13 amd64 AmneziaWG/AWG 2.0 server. +- `awg2-harness` — host-side оркестратор traffic/firewall/status/swap; +- `awg2-reboot-scheduler` — отдельный сервис планировщика перезагрузки. -It owns the infrastructure state needed by the VPN service: +`awg2-harness` остаётся основным host-side мульти-оркестратором для выделенного сервера Debian 13 amd64 с AmneziaWG/AWG 2.0. -- Amnezia container discovery; -- container network namespace discovery; -- AWG/WG interface discovery; -- peer discovery; -- traffic-control desired state; -- IFB ingress shaping; -- nftables connection-count anti-torrent guard; -- Docker lifecycle recovery; -- periodic reconciliation; -- status/diagnostics; -- planned reboot; -- emergency swap setup. +Он владеет инфраструктурным состоянием, которое нужно VPN-сервису: -It does not replace Amnezia. It controls the surrounding host-side infrastructure state required by the Amnezia service. +- обнаружение контейнера Amnezia; +- обнаружение network namespace контейнера; +- обнаружение интерфейса AWG/WG; +- обнаружение peers; +- целевое состояние traffic control; +- ingress shaping через IFB; +- nftables connection-count guard (anti-torrent); +- восстановление после Docker lifecycle events; +- периодический reconcile; +- status и диагностика; +- плановая перезагрузка; +- аварийная настройка swap. -## 2. Process model +Он не заменяет Amnezia. Он управляет окружающим host-side состоянием, которое нужно сервису Amnezia. -Two systemd services run: +## 2. Модель процессов + +Работают два systemd-сервиса: ```bash /usr/sbin/awg2-harness --config /etc/awg2-harness/config.toml daemon /usr/sbin/awg2-reboot-scheduler --config /etc/awg2-harness/config.toml ``` -`awg2-harness` daemon starts as root and keeps running. On startup it performs initial reconcile. During runtime it reacts to Docker events and also runs a periodic reconcile loop. +Демон `awg2-harness` стартует от root и работает постоянно. На старте выполняет начальный reconcile. В runtime реагирует на Docker events и параллельно крутит периодический цикл reconcile. -Reboot scheduling authority belongs to `awg2-reboot-scheduler` service. `awg2-harness` does not schedule reboots internally. +Право планировать перезагрузку принадлежит сервису `awg2-reboot-scheduler`. `awg2-harness` не планирует перезагрузки внутри себя. -Subcommands: +Подкоманды: ```bash awg2-harness daemon @@ -53,193 +55,195 @@ awg2-harness validate-config awg2-harness validate-runtime ``` -`apply-network-tuning`/`inspect-network-tuning` относятся к отдельному host-level модулю `sysctl_tuning`. -Этот модуль: +Глобальный флаг `--config`; если не задан, используется `/etc/awg2-harness/config.toml`. -- управляет только sysctl параметрами хоста; -- пишет только `/etc/sysctl.d/90-awg2-harness-vpn.conf`; -- применяет runtime-изменения точечно (`sysctl -w`) только для drifted keys; +Уровень валидации у подкоманд разный, и это часть контракта: + +| Подкоманда | Валидация перед выполнением | +| --- | --- | +| `validate-config` | `validate_config` | +| `validate-runtime` | `validate_runtime` (поддерживаемый хост + host tools) | +| `status`, `inspect-network-tuning` | `validate_config` | +| `apply-network-tuning` | `validate_sysctl_tuning_runtime` | +| `apply-limits`, `apply-firewall`, `ensure-swap`, `daemon` | `validate_all` | + +`apply-network-tuning` и `inspect-network-tuning` относятся к отдельному host-level модулю `sysctl_tuning`. Этот модуль: + +- управляет только sysctl-параметрами хоста; +- пишет `/etc/sysctl.d/90-awg2-harness-vpn.conf`; +- дополнительно пишет `/etc/modules-load.d/90-awg2-harness-bbr.conf`, но только когда BBR фактически активен; если BBR неактивен, этот файл удаляется; +- применяет runtime-изменения точечно (`sysctl -w`) и только для drifted keys; - не использует `/etc/sysctl.conf` и `sysctl --system`; - не управляет `udp_wmem_min`. -## 3. Module layout +Оба управляемых файла несут в первой строке маркер `# Managed by awg2-harness sysctl_tuning`. Файл без маркера не перезаписывается и не удаляется — вместо этого возвращается ошибка. Это защищает чужие настройки, случайно оказавшиеся по тому же пути. -Recommended Rust module split: +`net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` попадают в целевое состояние только при активном BBR. + +## 3. Структура модулей + +Фактическая раскладка — плоская, без вложенных каталогов модулей: ```text -src/ -├── main.rs -├── cli.rs -├── config.rs -├── daemon.rs -├── lock.rs -├── docker.rs -├── netns.rs -├── iface.rs -├── wg.rs -├── traffic/ -│ ├── mod.rs -│ ├── model.rs -│ ├── inspect.rs -│ ├── apply.rs -│ ├── parse_tc.rs -│ └── commands.rs -├── firewall/ -│ ├── mod.rs -│ ├── model.rs -│ ├── inspect.rs -│ ├── apply.rs -│ └── nft.rs -├── status.rs -├── swap.rs -└── error.rs +traffic_core/src/ +├── main.rs точка входа и диспетчер подкоманд +├── cli.rs определение CLI (clap) +├── config.rs модель конфига, валидация, проверка host tools +├── daemon.rs цикл демона, Docker events, журнал событий +├── docker.rs обнаружение контейнера, диагностика port path +├── netns.rs транспорты выполнения команд +├── iface.rs обнаружение интерфейса +├── wg.rs обнаружение peers и разбор вывода awg/wg +├── traffic.rs tc/IFB: инспекция, diff, teardown, apply +├── firewall.rs nftables: host firewall и anti-torrent +├── sysctl_tuning.rs управляемый sysctl baseline +├── swap.rs управляемый swap +├── status.rs сборка и печать статуса +├── lock.rs файловые блокировки +└── error.rs типы ошибок + +reboot_module/src/ +└── main.rs планировщик перезагрузки ``` -## 4. Core data model +Разбиение `traffic` и `firewall` на подкаталоги (`model.rs`, `inspect.rs`, `apply.rs`) обсуждалось, но не выполнено. Пока эти модули остаются одиночными файлами, любые изменения должны сохранять разделение на инспекцию, diff и apply внутри файла. -The traffic module should operate on explicit model structs, not raw strings passed around the codebase. +## 4. Модель данных -Conceptual model: +Модули работают на явных структурах, а не на строках, которые передаются по всему коду. + +Фактические типы: ```rust +// docker.rs struct RuntimeTarget { container_name: String, container_id: String, container_pid: u32, - netns_path: PathBuf, + netns_path: String, // /proc//ns/net interface_name: String, } +// wg.rs — состав peers для shaping struct Peer { - public_key: Option, ipv4: Ipv4Addr, - cidr: u8, // normally 32 - latest_handshake: Option, - transfer_rx: Option, - transfer_tx: Option, } -struct TrafficDesiredState { - iface: String, - ifb_name: String, - root_rate: Rate, - peer_limit: Rate, - class_start_id: u16, - peers: Vec, +// wg.rs — runtime-состояние peers для status +struct PeerRuntime { + public_key: String, + allowed_ips: Vec, + endpoint: Option, + latest_handshake_epoch: Option, + transfer_rx_bytes: Option, + transfer_tx_bytes: Option, } -struct PeerTrafficClass { - peer_ip: Ipv4Addr, +// traffic.rs — построчный статус peer в выводе status +struct PeerTrafficStatus { + ip: String, class_id: u16, + egress_filter_present: bool, + ingress_filter_present: bool, + egress_rate: Option, + egress_ceil: Option, + ingress_rate: Option, + ingress_ceil: Option, + drift_reason: Option, } ``` -Class assignment must be deterministic. Peer IP strings are sorted lexicographically, then assigned `class_start_id + index`. Do not silently switch this to numeric IPv4 sorting: the production class-id contract maps `10.8.1.10` before `10.8.1.2` because sorting is textual, not numeric. +Разделение `Peer` и `PeerRuntime` намеренное: shaping зависит только от адреса, а handshake/RX/TX нужны исключительно для диагностики. Ошибка чтения runtime-состояния не должна ломать применение лимитов. -## 5. Namespace strategy +Отдельной структуры desired state нет. Целевое состояние выводится детерминированно из конфига и отсортированного списка peers прямо в `inspect_and_diff` и `apply_full_topology`. -Preferred strategy: +Присвоение class id обязано быть детерминированным. Строки peer IP сортируются лексикографически, затем присваивается `class_start_id + index`. Это нельзя молча заменить на числовую сортировку IPv4: production-контракт class-id ставит `10.8.1.10` раньше `10.8.1.2`, потому что сортировка текстовая, а не числовая. -1. Detect running container through Docker API or `docker ps` / `docker inspect`. -2. Read container PID. -3. Use `/proc//ns/net` as the target namespace. -4. Execute network operations in that namespace. +## 5. Стратегия namespace -Implementation options: +Основной путь: -- current path: spawn host tools with namespace entry, for example `nsenter -t -n tc ...`, `nsenter -t -n ip ...`; -- later/native: use netlink crates and explicit namespace switching where practical. +1. Найти запущенный контейнер через `docker ps` / `docker inspect`. +2. Прочитать PID контейнера. +3. Использовать `/proc//ns/net` как целевой namespace. +4. Выполнять сетевые операции в этом namespace. -Peer state is the one exception and must never be read with a host-side binary. -AmneziaWG is a userspace implementation whose UAPI socket lives under -`RUNSTATEDIR/amneziawg/.sock` inside the container mount namespace, while host -`wireguard-tools` looks under `/var/run/wireguard/`. `nsenter -n` enters only the network -namespace, so a host `wg` reaches neither the socket nor a kernel device and always -returns an empty peer list. Peer discovery therefore uses `docker exec` exclusively, and -`wireguard-tools` is not a host dependency. +Реализация: -`docker exec` should not be the architectural default. It can remain as compatibility fallback if `nsenter`/host tools are unavailable, but the design assumption is that the Rust binary has host authority and controls the relevant namespace. +- текущий путь — запуск host tools со входом в namespace: `nsenter -t -n -- tc ...`, `nsenter -t -n -- ip ...`, `nsenter -t -n -- nft ...`; +- в перспективе — netlink-крейты и явное переключение namespace там, где это практично. -This also reduces operational risk: if the Amnezia container drops `tc` or `iproute2`, host-side tools can still apply rules inside the container netns. +`docker exec` не является архитектурным умолчанием для этих операций. Он остаётся fallback-транспортом, если `nsenter` недоступен. На практике `nsenter` доступен всегда: `validate-runtime` завершается с ошибкой при его отсутствии, а `validate_all` вызывается перед `daemon`, `apply-limits`, `apply-firewall` и `ensure-swap`. -## 6. Discovery flow +Это снижает эксплуатационный риск: если контейнер Amnezia перестанет содержать `tc` или `iproute2`, host-side утилиты всё равно применят правила внутри его netns. + +Чтение состояния peers — единственное исключение, и его нельзя выполнять host-side бинарём. AmneziaWG — userspace-реализация, её UAPI-сокет находится в `RUNSTATEDIR/amneziawg/.sock` внутри mount namespace контейнера, тогда как хостовый `wireguard-tools` ищет сокет в `/var/run/wireguard/`. `nsenter -n` входит только в network namespace, поэтому хостовый `wg` не достаёт ни до сокета, ни до kernel-устройства и всегда возвращает пустой список peers. Поэтому обнаружение peers использует исключительно `docker exec`, а `wireguard-tools` не является зависимостью хоста и не проверяется в preflight. + +## 6. Порядок discovery `discover_target()`: -1. Load ordered `amnezia.container_names`. -2. Find the first running matching container. -3. Get container ID, status and PID. -4. Build `netns_path`. -5. Detect interface in that namespace. -6. Return `RuntimeTarget`. +1. Прочитать упорядоченный `amnezia.container_names`. +2. Найти первый запущенный контейнер с точным совпадением имени. +3. Получить container ID и PID. +4. Построить `netns_path`. +5. Определить интерфейс в этом namespace. +6. Вернуть `RuntimeTarget`. -Interface detection order: +Порядок определения интерфейса: -1. Exact names from `amnezia.interface_candidates`. -2. Pattern scan from `amnezia.interface_patterns`. -3. Fallback: first non-loopback interface with IPv4 address, only if it is clearly inside the Amnezia namespace. -4. If ambiguous, return an explicit error and do not apply rules. +1. Точные имена из `amnezia.interface_candidates` — проверяются через `ip link show `. +2. Сканирование `ip -o link show` и сопоставление с `amnezia.interface_patterns`. Поддерживается только суффиксный `*` (`awg*`); имена вида `veth0@if8` нормализуются до `veth0`. +3. Если ничего не совпало — явная ошибка `unable to detect amnezia interface`, правила не применяются. -## 7. Peer discovery +Эвристики «взять первый non-loopback интерфейс с IPv4» нет и быть не должно: молчаливый выбор произвольного интерфейса опаснее явной ошибки. -Transport is always `docker exec ` — see section 5 for why a host-side `wg` -cannot work. +## 7. Обнаружение peers -Binary resolution uses ordered `amnezia.wg_binaries`, default `["awg", "wg"]`. The first -binary that exits successfully wins. The official AmneziaWG image ships `/usr/bin/awg` and -symlinks `/usr/bin/wg` to it, so `awg` is the real dependency; `wg` remains for plain -WireGuard containers where `awg` does not exist. There is no probe call: the resolution is -a property of the command that is being run anyway. +Транспорт — всегда `docker exec `. Обоснование в разделе 5. -Primary command: +Выбор бинаря идёт по упорядоченному `amnezia.wg_binaries`, default `["awg", "wg"]`. Побеждает первый, который завершился успешно. Официальный образ AmneziaWG содержит `/usr/bin/awg` и symlink `/usr/bin/wg` на него, поэтому `awg` — настоящая зависимость; `wg` остаётся для контейнеров с чистым WireGuard, где `awg` отсутствует. Отдельного probe-вызова нет: выбор бинаря является побочным результатом той команды, которая и так должна выполниться. + +Значения `amnezia.wg_binaries` исполняются внутри контейнера и валидируются как голые имена команд: непустые, без пробелов, без разделителей пути. + +Основная команда: ```bash awg show all allowed-ips ``` -Parsing rules: +Правила разбора: -- parse lines structurally into peer public key and allowed IPs; -- a peer's allowed ips are **space-separated** in this command, comma-separated in - `show all dump` and `", "`-separated in plain `show`; all tokens from column 3 onward - must be consumed, otherwise peers with more than one allowed IP are shaped only on their - first address; -- keep IPv4 `/32` addresses within `amnezia.peer_ipv4_cidr`; -- include offline peers; online state is not a requirement for shaping; -- sort peer IP strings lexicographically before class assignment to preserve the current lexicographic class-id contract. +- строки разбираются структурно на public key peer и его allowed IPs; +- в этой команде allowed ips одного peer разделены **пробелом**, в `show all dump` — запятой, в обычном `show` — `", "`; нужно потреблять все токены начиная с третьей колонки, иначе peers с несколькими allowed IP получат shaping только по первому адресу; +- оставляются только IPv4 `/32` внутри `amnezia.peer_ipv4_cidr`; +- offline peers включаются: online-состояние не является требованием для shaping; +- перед присвоением class id строки peer IP сортируются лексикографически, чтобы сохранить действующий контракт class-id. -Fallback: +Резервная команда: ```bash awg show ``` -The fallback parser should support standard `wg show` text output by extracting `allowed ips:` lines. +Fallback-парсер разбирает текстовый вывод, извлекая строки `allowed ips:`. Он используется, если основная команда завершилась ошибкой или вернула пустой результат. -`show all dump` line classification: +Классификация строк `show all dump`: -- a line is a peer line only if it has at least 9 tab-separated columns, column 5 - (allowed ips) is `(none)` or a comma-separated CIDR list, and columns 6-8 (handshake, - rx, tx) all parse as integers; -- column count is not a discriminator on its own: the AmneziaWG interface line carries the - AWG parameters and is *longer* than a peer line (14 columns for AWG 2.0, 28 for AWG 3.0), - and its handshake/rx/tx positions hold junk-packet sizes that parse as integers; -- this matters beyond correctness — column 2 of an interface line is the device private - key, so a misclassified interface line would surface it as a peer public key; -- AWG 3.0 prints protocol timers as ranges (`20-30`). The peer keepalive column is not - read, and a range value must not break parsing. +- строка считается peer-строкой, только если в ней не менее 9 tab-колонок, колонка 5 (allowed ips) равна `(none)` или является списком CIDR через запятую, а колонки 6–8 (handshake, rx, tx) целиком парсятся как целые; +- количество колонок само по себе не является дискриминатором: interface-строка AmneziaWG содержит AWG-параметры и *длиннее* peer-строки (14 колонок на AWG 2.0, 28 на AWG 3.0), а на позициях handshake/rx/tx у неё стоят размеры junk-пакетов, которые тоже парсятся как целые; +- это важно не только для корректности: колонка 2 interface-строки — приватный ключ устройства, и ошибочно классифицированная interface-строка выдала бы его как public key peer; +- AWG 3.0 печатает протокольные таймеры диапазонами (`20-30`). Колонка keepalive у peer не читается, и диапазон в ней не должен ломать разбор. -Execution policy for production: +Политика выполнения для production: -- do not use shell wrappers (`sh -c`) for `wg` invocation; -- do not suppress stderr via redirection in command strings; -- execute the binary directly and handle non-zero exit codes in Rust; -- `amnezia.wg_binaries` values are executed inside the container and are validated as bare - command names: non-empty, no whitespace, no path separators. +- не использовать shell-обёртки (`sh -c`) для вызова `awg`/`wg`; +- не подавлять stderr редиректом внутри строки команды; +- вызывать бинарь напрямую и обрабатывать ненулевые коды возврата в Rust. -## 8. Desired traffic topology +## 8. Целевая топология traffic -The expected topology is the AWG2 traffic topology contract. +Ожидаемая топология — контракт traffic-топологии AWG2. Egress: @@ -252,7 +256,7 @@ for each peer: tc filter add dev parent 1: protocol ip u32 match ip dst flowid 1: ``` -Ingress through IFB: +Ingress через IFB: ```text modprobe ifb @@ -268,86 +272,82 @@ for each peer: tc filter add dev parent 2: protocol ip u32 match ip src flowid 2: ``` -## 9. Reconcile algorithm +## 9. Алгоритм reconcile -Pseudo-flow: +Traffic и firewall — два независимых reconcile с отдельными блокировками. Они не объединены в одну транзакцию: сбой одного не должен блокировать другой. ```text reconcile_traffic(): - acquire global lock + если traffic.enabled = false: выйти без изменений + взять /run/awg2-harness/traffic.lock (timeout 10 c) target = discover_target() - peers = discover_peers(target) - if peers empty: - log and return NoPeers - desired = build_desired_state(target, peers, config) - actual = inspect_actual_state(target, desired) - drift = diff(desired, actual) - firewall_desired = build_firewall_desired_state(target, config) - firewall_actual = inspect_firewall_state(target, firewall_desired) - firewall_drift = diff(firewall_desired, firewall_actual) - - if drift.none and firewall_drift.none: - log "rules already up-to-date" - return NoChange - - if drift.exists: - teardown_owned_topology(target, desired) - apply_full_topology(target, desired) - verify_after_apply(target, desired) - - if firewall_drift.exists: - apply_owned_firewall_table(target, firewall_desired) - verify_firewall_after_apply(target, firewall_desired) + peers = discover_peers(config, target) + если peers пуст: + залогировать "no peers found, skip tc apply" и выйти + drift = inspect_and_diff(target, peers, config) + если drift отсутствует: + залогировать "traffic rules up-to-date, skip apply" и выйти + teardown_owned_topology(target) + apply_full_topology(target, peers, config) # внутри — verify-after-apply ``` -For production, controlled full rebuild on drift is the only supported path. +```text +apply_firewall(): + взять /run/awg2-harness/firewall.lock (timeout 10 c) + apply_host_firewall(config) # host namespace, таблица inet awg2_host_firewall + apply_antitorrent_firewall(config) # namespace по anti_torrent.apply_in_amnezia_netns +``` -Parser hardening requirements: +Каждый из firewall-путей сначала инспектирует фактическое состояние и выходит без изменений, если drift отсутствует. При наличии drift владеемая таблица удаляется и создаётся заново, после чего выполняется обязательная повторная инспекция. -- `tc filter show` must be parsed block-by-block (state-machine), not by naive token scanning across unrelated lines; -- mapping `peer_ip -> class_id` must be validated for both egress (`1:`) and ingress (`2:`) sides; -- verification must fail when classes/filters exist but mapping differs from deterministic assignment. +Для production контролируемая полная пересборка при drift — единственный поддерживаемый путь. -Apply/verify policy: +Требования к устойчивости парсеров: -- all apply steps are explicit command invocations; -- expected idempotency conflicts (`already exists`) may be tolerated selectively; -- any other apply error is fatal; -- `verify_after_apply` is mandatory for both traffic and anti-torrent subsystems. +- `tc filter show` разбирается блоками (конечный автомат), а не наивным сканированием токенов по несвязанным строкам; +- отображение `peer_ip -> class_id` проверяется для обеих сторон: egress (`1:`) и ingress (`2:`); +- проверка обязана падать, если классы и фильтры существуют, но отображение отличается от детерминированного присвоения. -Anti-torrent observability contract: +Политика apply/verify: -- named counter `antitorrent_dropped_v4` must be present in owned table; -- status output must expose both dropped packets and dropped bytes. +- все шаги apply — явные вызовы команд; +- ожидаемые конфликты идемпотентности допускаются выборочно, через отдельную обёртку, и по закрытому списку признаков: `file exists`, `eexist`, `object already exists`, `exclusive flag on`; +- ошибки «объект не найден» (`no such`, `not found`, `cannot find`, `does not exist`, `handle of zero`) допускаются только на шагах удаления — в teardown и при отключении владеемых таблиц; +- любая другая ошибка apply фатальна; +- `verify-after-apply` обязателен и для traffic, и для anti-torrent. -## 10. Owned state and teardown rules +Контракт наблюдаемости anti-torrent: -The binary owns: +- в владеемой таблице обязан присутствовать именованный counter `antitorrent_dropped_v4`; +- вывод status обязан показывать и дропнутые пакеты, и дропнутые байты. -- root qdisc `1:` on selected AWG/WG interface; -- ingress qdisc `ffff:` on selected AWG/WG interface when traffic limits are enabled; -- IFB device named `traffic.ifb_name`, default `ifb0`, in the target namespace; -- root qdisc `2:` on IFB; -- classes and filters under these handles. +## 10. Owned state и правила teardown -Teardown may delete and recreate these owned objects. Because the server is dedicated, this is acceptable and preferable to leaving stale or partial rules. +Бинарь владеет: -The binary should not blindly flush unrelated host firewall rules, Docker NAT rules, or unrelated interfaces. Firewall ownership must be explicit and scoped to the AWG2 Harness table, for example `table inet awg2_harness`, inside the target network namespace. +- root qdisc `1:` на выбранном интерфейсе AWG/WG; +- ingress qdisc `ffff:` на выбранном интерфейсе AWG/WG, когда лимиты включены; +- IFB-устройством с именем `traffic.ifb_name` (default `ifb0`) в целевом namespace; +- root qdisc `2:` на IFB; +- классами и фильтрами под этими handles. +Teardown может удалять и пересоздавать эти владеемые объекты. Поскольку сервер выделенный, это допустимо и предпочтительнее, чем оставлять устаревшие или частичные правила. + +Бинарь не должен вслепую сбрасывать посторонние host firewall rules, Docker NAT rules или чужие интерфейсы. Владение firewall обязано быть явным и ограниченным таблицами AWG2 Harness: `inet awg2_harness` в целевом network namespace и `inet awg2_host_firewall` в host namespace. ## 11. Anti-torrent firewall guard -This is part of the AWG2 production contract. It fits the multi-orchestrator model because the server is dedicated and the binary is allowed to own host/network rules required for the VPN product. The rule must still be scoped narrowly and observable. +Это часть production-контракта AWG2. Модуль вписывается в мульти-оркестраторную модель, потому что сервер выделенный и бинарю разрешено владеть host/network правилами, нужными VPN-продукту. Правило всё равно обязано быть узко ограниченным и наблюдаемым. -Goal: limit concurrent conntrack flows per VPN peer IP. This is not torrent protocol detection. It is a generic connection-count guard that makes high-peer-count P2P workloads unattractive while leaving normal browsing, streaming, gaming and messaging traffic unaffected. +Цель — ограничить количество одновременных conntrack flows на один VPN peer IP. Это не детектирование торрент-протокола, а обобщённый connection-count guard, который делает P2P-нагрузку с большим числом peers непривлекательной, не задевая обычные browsing, streaming, gaming и messaging. -The rule must not be implemented as a global subnet limit: +Правило не должно быть глобальным лимитом на подсеть: ```nft ip saddr 10.8.1.0/24 ct count over 200 drop ``` -That form can become one shared counter for the whole matched traffic set, depending on how it is written and where it is placed. For per-peer behavior, key the stateful object by source VPN IP. Recommended shape: +Такая форма может превратиться в один общий счётчик на весь совпавший трафик — в зависимости от того, как она написана и куда помещена. Для per-peer поведения stateful-объект нужно ключевать по source VPN IP. Рекомендуемая форма: ```nft table inet awg2_harness { @@ -364,166 +364,198 @@ table inet awg2_harness { } ``` -Target namespace: the same Amnezia network namespace where `tc` shaping is applied. Do not assume the initial host namespace sees original peer source addresses. If Amnezia performs NAT inside the container namespace, the host Docker bridge may only see the container address, not `10.8.1.x`. +Целевой namespace — тот же Amnezia network namespace, где применяется `tc` shaping. Нельзя предполагать, что начальный host namespace видит исходные source-адреса peers. Если Amnezia выполняет NAT внутри namespace контейнера, host Docker bridge может видеть только адрес контейнера, а не `10.8.1.x`. -Default threshold: `100` concurrent flows per peer IP. `150` is the conservative compatibility setting if there are complaints from unusually connection-heavy WebSocket/WebRTC/trading-browser workloads. `70` is more aggressive and should only be used after real traffic observation. +Порог в примере конфигурации — `150` одновременных flows на peer IP. `100` — более строгий режим. `70` агрессивнее и применяется только после наблюдения реального трафика. `200`/`250` — более permissive варианты. -Rule placement: use `forward` hook with priority `filter` or any priority after conntrack lookup. Restrict evaluation to `ct state new` so established flows continue normally and only new flow attempts beyond the threshold are dropped. +Размещение правила: hook `forward` с приоритетом `filter` или любым приоритетом после conntrack lookup. Оценка ограничивается `ct state new`, чтобы установленные flows продолжали работать и дропались только новые попытки сверх порога. -Owned state: +Владеемое состояние: -- table `inet awg2_harness`; +- таблица `inet awg2_harness`; - chain `forward_antitorrent`; - counter `antitorrent_dropped_v4`; -- meter/set state created by the rule. +- meter/set state, создаваемый правилом. -Status must show whether the table/chain/rule exists, the configured threshold, the peer CIDR, and the drop counter. It should also expose host conntrack pressure from `net.netfilter.nf_conntrack_count` and `net.netfilter.nf_conntrack_max` when available. +Status обязан показывать наличие таблицы/chain/правила, настроенный порог и drop counter. Также он показывает давление на conntrack хоста через `net.netfilter.nf_conntrack_count` и `net.netfilter.nf_conntrack_max`, когда они доступны. -If `anti_torrent.enabled = false`, the daemon should remove or leave inactive only the AWG2-owned anti-torrent table according to config policy. It must never flush the global nftables ruleset. +При `anti_torrent.enabled = false` демон удаляет только владеемую anti-torrent таблицу. Глобальный ruleset nftables не сбрасывается никогда. Если контейнер в этот момент отсутствует, удаление в его namespace просто пропускается — это не ошибка. -## 12. Actual-state inspection +## 12. Инспекция фактического состояния -`inspect_actual_state()` must collect enough information to diagnose drift: +`inspect_and_diff()` собирает достаточно информации для диагностики drift: -- qdisc on ``; -- classes on ``; -- qdiscs attached to per-peer classes on ``; -- filters under parent `1:` on ``; -- ingress qdisc on ``; -- ingress redirect filter on ``; -- existence and state of ``; -- qdisc on ``; -- classes on ``; -- filters under parent `2:` on ``. +- qdisc на ``; +- классы на ``; +- qdiscs, привязанные к per-peer классам на ``; +- фильтры под parent `1:` на ``; +- ingress qdisc на ``; +- ingress redirect filter на ``; +- существование и состояние ``; +- qdisc на ``; +- классы на ``; +- фильтры под parent `2:` на ``. -`status` should display a compact summary and optionally a verbose mode for raw `tc` sections. +Дополнительно детектируются дубли: один и тот же peer IP в нескольких фильтрах и один и тот же class id у нескольких IP. Оба случая считаются drift. -## 13. Docker events and periodic reconciliation +`status` показывает компактную сводку и таблицу по peers. -The daemon should use two triggers: +## 13. Docker events и периодический reconcile -1. Docker events: container start/restart signals cause delayed reconcile after interface readiness. -2. Timer: periodic reconcile every `traffic.reconcile_interval_secs`. +Демон использует два триггера: -The timer is mandatory. Events can be missed during daemon restart, Docker restart or transient errors. +1. Docker events: `docker events --filter event=start --format '{{.Actor.Attributes.name}}'`. Имя сверяется с `amnezia.container_names`, после чего выполняется ожидание готовности интерфейса и reconcile traffic + firewall. Если поток событий завершился, он перезапускается через 10 секунд. +2. Таймер: периодический reconcile каждые `traffic.reconcile_interval_secs`, но не чаще одного раза в 5 секунд. -Readiness check must not hardcode `awg0`. It must call the shared interface detector until timeout. +Таймер обязателен. События могут быть пропущены при рестарте демона, рестарте Docker или транзиентных ошибках. -## 14. Locking +Проверка готовности не должна хардкодить `awg0`. Она вызывает общий детектор интерфейса с интервалом 2 секунды до истечения `traffic.container_ready_timeout_secs`. -Only one traffic reconcile may run at a time across all subcommands and daemon tasks. +Порядок на старте демона и разница в критичности ошибок: -Recommended lock: +1. `sysctl_tuning`, если `apply_on_daemon_start = true` — ошибка **фатальна**, демон завершается; +2. `swap` — ошибка **фатальна**, демон завершается; +3. `traffic` — ошибка логируется, демон продолжает работу; +4. `firewall` — ошибка логируется, демон продолжает работу. -```text -/run/awg2-harness/traffic.lock -``` +Такое разделение намеренное: некорректный baseline хоста или swap означают неверные предпосылки, тогда как отсутствие контейнера Amnezia — нормальное транзиентное состояние. -Behavior: +Результаты пишутся в два файла: -- `daemon` skips overlapping reconcile and logs it; -- manual `apply-limits` either waits briefly or exits with a clear message; -- lock release is automatic when process exits. +- `/run/awg2-harness/last_action.state` — последний успех и последняя ошибка; +- `/run/awg2-harness/events.log` — журнал событий, ограниченный 1000 строками и 256 KiB. -## 15. Status output contract +## 14. Блокировки -`awg2-harness status` should show: +Одновременно может выполняться только один reconcile каждого вида — по всем подкомандам и задачам демона. + +Блокировки: + +| Файл | Таймаут ожидания | Кого защищает | +| --- | --- | --- | +| `/run/awg2-harness/traffic.lock` | 10 с | `reconcile_traffic` | +| `/run/awg2-harness/firewall.lock` | 10 с | `apply_firewall` (host + anti-torrent) | +| `/run/awg2-harness/sysctl_tuning.lock` | 30 с | `ensure_sysctl_tuning` | + +Поведение: + +- ожидание захвата — с интервалом 100 мс до таймаута, затем ошибка `lock busy timeout`; +- в файл записывается PID владельца; +- если процесс-владелец мёртв (`/proc/` отсутствует) или содержимое файла не парсится как PID, блокировка считается устаревшей и перезахватывается; +- освобождение автоматическое при выходе из области видимости, включая пути с ошибкой. + +## 15. Контракт вывода status + +`awg2-harness status` печатает блоки в следующем порядке. Имена полей стабильны и пригодны для grep. ```text System: - service: active/inactive/unknown - docker: available/unavailable - container: name, id, status, pid + service: active|inactive|unknown +Network tuning: + enabled, apply_on_daemon_start, tcp_bbr_mode, conf_path, modules_load_path, + значения управляемых ключей, bbr_available, bbr_active, drift[, reason][, warning] + container: + container_id: + pid: netns: /proc//ns/net - interface: awg0/wg0/... - + interface: Peers: total: N - each peer: ip, optional public key prefix, handshake, rx/tx - + - +WireGuard clients: + IP, State, Latest handshake, RX, TX, Public key, Endpoint Traffic desired: peer_limit, root_rate, ifb_name, class_start_id - Traffic actual: - egress qdisc/classes/filters: ok/drift - ingress redirect: ok/drift - ifb topology: ok/drift - stale peers: list - missing peers: list - -Anti-torrent actual: - enabled: true/false - nft table/chain/rule: ok/drift - max_connections_per_peer - dropped_new_flows counter - conntrack_count / conntrack_max if available - + drift: no|yes[, reason] + topology: egress_htb_present, ingress_present, ifb_htb_present + peer_table: ip, class_id, egress_filter, ingress_filter, egress, ingress, drift +Peer traffic counters: + IP, Egress hits, Ingress hits +Host firewall: + enabled, ssh_port, amnezia_port, input_policy, drift[, reason] +Docker/Amnezia port path: + docker_userland_proxy, published_udp_port, host_forward_policy_drop, + docker_forward_rules_present, note[, warning] +Anti-torrent: + enabled, max_connections_per_peer, drift[, reason], dropped_packets, dropped_bytes +Swap: + enabled, desired_path, desired_size_mb, desired_priority, active_entries, + desired_active, exclusive, fstab_exclusive, fstab_swap_entries, + swappiness_current, swappiness_persisted, drift[, drift_reason] +Conntrack: + nf_conntrack_count, nf_conntrack_max Last actions: - last reconcile result - last error if any + содержимое /run/awg2-harness/last_action.state +Recent events: + последние 10 строк /run/awg2-harness/events.log +Reboot scheduler: + enabled, window_start_utc, window_end_utc, service, planned ``` -A generic status banner like “system ready” is not enough. Rust status must be falsifiable: it should clearly say when state is partial. +Блок `Network tuning` печатается между строкой `service` и строками `container`/`pid`/`netns`/`interface`, хотя последние логически относятся к `System`. Это текущее фактическое поведение; порядок зафиксирован здесь, чтобы автоматические проверки опирались на реальность. -## 16. Reboot module integration +Если контейнер обнаружить не удалось, `status` не завершается ошибкой, а переходит в degraded-режим: секции `Peers` и `WireGuard clients` печатают `unavailable`, `Traffic actual` показывает `drift: unknown`, блок `Docker/Amnezia port path` заполняется значениями `unknown`, а блок `Peer traffic counters` не печатается вовсе. Секции host firewall, anti-torrent, swap, conntrack, событий и reboot остаются на месте. -Production reboot path is external binary/service `awg2-reboot-scheduler`: +Отдельные секции деградируют независимо: ошибка чтения peer state не мешает вывести traffic, firewall и swap. -- window from config, not constants; -- random point inside reboot window (including cross-midnight windows); -- `systemctl --no-block reboot` remains; -- disabled when `reboot.enabled = false`; -- planned reboot timestamp is persisted in `/run/awg2-harness/reboot.plan`. +Универсальный баннер вида «system ready» недопустим. Status обязан быть фальсифицируемым: он должен явно сообщать, когда состояние частичное. -## 17. Swap module +## 16. Интеграция reboot-модуля -`ensure-swap` desired state: +Production-путь перезагрузки — внешний бинарь/сервис `awg2-reboot-scheduler`: -- path from config, default `/swapfile`; -- size 2048 MB; -- mode `0600`; -- `mkswap` completed; -- active in `swapon --show`; -- one `/etc/fstab` entry with configured priority; -- `vm.swappiness = 1` persisted and applied. +- окно берётся из конфига, а не из констант; +- случайная точка внутри окна, включая окна через полночь; +- вызов остаётся `systemctl --no-block reboot`; +- при `reboot.enabled = false` планирование отключено; +- запланированная отметка времени сохраняется в `/run/awg2-harness/reboot.plan`. -When `swap.enabled=true`, AWG2 Harness owns host swap state exclusively. -The desired state is exactly one active swap entry: `swap.path`. -All other active swap devices/files are disabled via `swapoff`. -All non-comment swap entries in `/etc/fstab` are removed and replaced by one managed AWG2 entry. -Regular-file swapfiles that are not the configured `swap.path` may be deleted only after successful `swapoff`. -Block devices are never deleted, only disabled and removed from fstab. -The operation must be idempotent. +## 17. Модуль swap -## 18. Testing +Целевое состояние `ensure-swap`: -Unit tests: +- путь из конфига, default `/swapfile`; +- размер `swap.size_mb`, в примере конфигурации — 2048 MB; +- права `0600`; +- выполнен `mkswap`; +- запись активна в `swapon --show`; +- одна запись в `/etc/fstab` с настроенным приоритетом; +- `vm.swappiness` из конфига применён в runtime и сохранён persistently. -- config validation, including `amnezia.wg_binaries` shape; -- `awg|wg show all allowed-ips` parser, including multiple space-separated allowed ips; -- `awg|wg show` fallback parser; -- `awg|wg show all dump` peer/interface line classification against AWG 3.0 (28 columns), - AWG 2.0 (14 columns) and plain WireGuard (5 columns) interface lines; -- wg binary resolution order and the error raised when no binary is available; -- CIDR filtering; -- deterministic class assignment with lexicographic class-id stability contract; -- nftables ruleset rendering and parsing for the AWG2-owned table; -- `tc` parser for qdisc/class/filter snapshots; -- drift diff; -- reboot window calculation; -- fstab/sysctl idempotency. +При `swap.enabled = true` AWG2 Harness владеет состоянием swap на хосте эксклюзивно. Целевое состояние — ровно одна активная swap-запись: `swap.path`. Все прочие активные swap-устройства и файлы выключаются через `swapoff`. Все не-комментарные swap-записи в `/etc/fstab` удаляются и заменяются одной управляемой записью AWG2; перед перезаписью создаётся резервная копия `/etc/fstab.awg2-harness.bak`. Обычные swap-файлы, не равные настроенному `swap.path`, могут быть удалены только после успешного `swapoff`. Блочные устройства не удаляются никогда — только выключаются и убираются из fstab. Операция обязана быть идемпотентной. -Integration tests on real host: +Создание файла: `fallocate`, при неудаче — fallback на `dd`. -- initial apply; -- repeated apply; -- partial state recovery after deleting only ingress qdisc; -- partial state recovery after deleting `ifb0`; -- container restart; -- Docker restart; -- host reboot; -- peer addition; -- peer deletion; -- status output in healthy and broken states; -- anti-torrent rule counter increments when a peer exceeds the configured concurrent-flow threshold; -- anti-torrent disable path does not flush unrelated nftables/Docker/Amnezia rules. +## 18. Тестирование + +Модульные тесты: + +- валидация конфига, включая форму `amnezia.wg_binaries`; +- разбор `config.example.toml` в актуальную схему; +- парсер `awg|wg show all allowed-ips`, включая несколько allowed ips через пробел; +- fallback-парсер `awg|wg show`; +- классификация peer/interface строк `awg|wg show all dump` против interface-строк AWG 3.0 (28 колонок), AWG 2.0 (14 колонок) и чистого WireGuard (5 колонок); +- порядок выбора бинаря wg и ошибка при отсутствии всех кандидатов; +- фильтрация по CIDR; +- детерминированное присвоение class id с контрактом лексикографической стабильности; +- рендеринг и разбор ruleset nftables для владеемых таблиц; +- парсер `tc` для снимков qdisc/class/filter; +- diff drift; +- расчёт окна перезагрузки; +- идемпотентность fstab и sysctl. + +Интеграционные тесты на реальном хосте: + +- первичное применение; +- повторное применение; +- восстановление частичного состояния после удаления только ingress qdisc; +- восстановление частичного состояния после удаления `ifb0`; +- рестарт контейнера; +- рестарт Docker; +- перезагрузка хоста; +- добавление peer; +- удаление peer; +- вывод status в здоровом и сломанном состояниях; +- рост counter anti-torrent при превышении peer настроенного порога одновременных flows; +- путь отключения anti-torrent не сбрасывает посторонние правила nftables/Docker/Amnezia. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index b976e3d..c829ce6 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -1,8 +1,8 @@ -# Installation +# Установка Этот документ описывает production-порядок установки AWG2 Harness. Подробный runtime-план с SSH hardening и Amnezia preflight находится в корневом [`README.md`](../README.md). -## Deployment contract +## Контракт развёртывания - Целевой production-хост: Debian 13 amd64. - Amnezia ставится до AWG2 Harness официальным self-hosted способом через приложение AmneziaVPN. @@ -13,9 +13,9 @@ - Пакет не устанавливает Docker/Amnezia и не включает services автоматически. - `sysctl_tuning` не применяется в `postinst`; применение только на старте daemon или вручную через CLI. -## A. Build machine +## A. Сборочная машина -### A1. Подготовить build environment +### A1. Подготовить сборочное окружение ```bash cd awg2_harness @@ -207,11 +207,11 @@ nsenter -t "$AWG_PID" -n ip route nsenter -t "$AWG_PID" -n dig @"$DNS_IP" example.com A +short ``` -## C. Transfer artifacts +## C. Передача артефактов Ниже два production-сценария установки. -### C1. Вариант A: config передаётся вместе с `.deb` +### C1. Вариант A: конфиг передаётся вместе с `.deb` ```bash scp -P 2323 dist/awg2-harness_*_amd64.deb root@:/root/ @@ -227,14 +227,14 @@ scp -P 2323 config.example.toml root@:/root/awg2-harness.config.toml - `[anti_torrent].peer_ipv4_cidr` обычно равен `[amnezia].peer_ipv4_cidr`; - `peer_limit` и `root_rate` отражают нужную traffic policy. -### C2. Вариант B (основной): config создаётся на production после установки +### C2. Вариант B (основной): конфиг создаётся на production после установки ```bash scp -P 2323 dist/awg2-harness_*_amd64.deb root@:/root/ scp -P 2323 dist/SHA256SUMS root@:/root/ ``` -## D. Production install +## D. Установка на production ### D1. Вариант A @@ -266,7 +266,7 @@ systemctl daemon-reload - `[amnezia].peer_ipv4_cidr` совпадает с peer-подсетью; - `[anti_torrent].peer_ipv4_cidr` обычно тот же CIDR. -## E. Preflight before enable +## E. Preflight до включения сервисов ```bash awg2-harness --config /etc/awg2-harness/config.toml validate-config @@ -277,7 +277,7 @@ awg2-harness --config /etc/awg2-harness/config.toml apply-limits awg2-harness --config /etc/awg2-harness/config.toml status ``` -Expected healthy result: +Ожидаемый здоровый результат: ```text Traffic actual: @@ -290,11 +290,11 @@ Swap: drift: no ``` -If there are no peers yet, `apply-limits` may print `no peers found, skip tc apply`. That is healthy for an empty server. +Если peers ещё нет, `apply-limits` напечатает `no peers found, skip tc apply`. Для пустого сервера это здоровое состояние. -When `swap.enabled=true`, AWG2 Harness intentionally makes swap exclusive: one managed swapfile, one fstab swap entry, configured swappiness. +При `swap.enabled = true` AWG2 Harness намеренно делает swap эксклюзивным: один управляемый swapfile, одна swap-запись в `/etc/fstab`, заданный `swappiness`. -## F. Enable services +## F. Включение сервисов ```bash systemctl enable --now awg2-harness.service @@ -306,19 +306,19 @@ journalctl -u awg2-harness.service --since '5 minutes ago' --no-pager journalctl -u awg2-reboot-scheduler.service --since '5 minutes ago' --no-pager ``` -After 30–60 seconds: +Через 30–60 секунд: ```bash awg2-harness --config /etc/awg2-harness/config.toml status ``` -## G. Reboot validation +## G. Проверка перезагрузкой ```bash reboot ``` -After reconnect: +После восстановления подключения: ```bash systemctl status docker --no-pager -l @@ -327,6 +327,6 @@ systemctl status awg2-reboot-scheduler.service --no-pager -l awg2-harness --config /etc/awg2-harness/config.toml status ``` -## H. Manual source-tree install +## H. Ручная установка из дерева исходников -`./tools/build.sh --install` and `./tools/build.sh --enable` exist for staging/debug. They are not the recommended production path. Use `.deb` on production. +`./tools/build.sh --install` и `./tools/build.sh --enable` существуют для staging и отладки. Это не рекомендуемый production-путь: на production используйте `.deb`. diff --git a/docs/PRODUCTION_EXAMPLE.md b/docs/PRODUCTION_EXAMPLE.md index fcc786a..7f77f4f 100644 --- a/docs/PRODUCTION_EXAMPLE.md +++ b/docs/PRODUCTION_EXAMPLE.md @@ -1,6 +1,6 @@ -# Production example (reference) +# Эталонный production-пример -Этот пример фиксирует эталонный production-state после успешной установки и preflight. Значения обезличены и предназначены как reference для smoke-проверки. +Документ фиксирует эталонное состояние production после успешной установки и preflight. Значения обезличены и предназначены как ориентир для smoke-проверки. ```text SSH: diff --git a/docs/README.md b/docs/README.md index 47ff3fd..1d31694 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,16 +2,22 @@ Рекомендуемый порядок чтения: -1. [`../README.md`](../README.md) — общий runtime-план: SSH, официальная Amnezia, установка пакета, preflight, reboot validation и CLI. -2. [`INSTALLATION.md`](INSTALLATION.md) — сжатый production install flow. +1. [`../README.md`](../README.md) — общий runtime-план: SSH, официальная Amnezia, установка пакета, preflight, проверка перезагрузкой и CLI. +2. [`INSTALLATION.md`](INSTALLATION.md) — сжатый порядок установки на production. 3. [`CONFIGURATION.md`](CONFIGURATION.md) — параметры `/etc/awg2-harness/config.toml`. -4. [`ACCEPTANCE_CHECKLIST.md`](ACCEPTANCE_CHECKLIST.md) — checklist перед production-ready статусом. -5. [`ANTI_TORRENT.md`](ANTI_TORRENT.md) — nftables connection-count guard. -6. [`IMPLEMENTATION_SPEC.md`](IMPLEMENTATION_SPEC.md) — архитектура и state reconciliation. +4. [`ACCEPTANCE_CHECKLIST.md`](ACCEPTANCE_CHECKLIST.md) — чек-лист приёмки перед статусом production-ready. +5. [`ANTI_TORRENT.md`](ANTI_TORRENT.md) — ограничение числа соединений через nftables. +6. [`IMPLEMENTATION_SPEC.md`](IMPLEMENTATION_SPEC.md) — архитектура и reconciliation состояния. Отдельно: - [`../tools/README.md`](../tools/README.md) — сборка и упаковка `.deb`. -- [`../reboot_module/README.md`](../reboot_module/README.md) — внешний reboot scheduler. +- [`../reboot_module/README.md`](../reboot_module/README.md) — внешний планировщик перезагрузки. -Практический порядок для оператора: сначала выполнить root README, затем свериться с acceptance checklist. Остальные документы нужны для понимания design intent и отладки. +Практический порядок для оператора: сначала выполнить корневой README, затем свериться с чек-листом приёмки. Остальные документы нужны для понимания замысла и отладки. + +## Язык документации + +Проза — на русском. Английский остаётся там, где он технически обязателен: команды, вывод утилит, ключи конфигурации, имена файлов, модулей, таблиц nftables и handles `tc`. Термины, у которых нет устоявшегося русского эквивалента в этом контексте (`drift`, `reconcile`, `peer`, `namespace`, `handshake`), не переводятся. + +`IMPLEMENTATION_SPEC.md` описывает фактическое состояние кода. Расхождение между ним и `traffic_core/src` считается дефектом документа, а не допустимым упрощением.