fix: harden firewall and traffic reconciliation

This commit is contained in:
2026-08-28 05:56:40 +05:00
parent 9787cb2d23
commit 9dd4732655
23 changed files with 1681 additions and 524 deletions
+20 -15
View File
@@ -1,6 +1,6 @@
# AWG2 Harness
`AWG2 Harness` — host-side orchestrator для выделенного Debian-хоста с self-hosted AmneziaWG. Основной target релиза 0.3.1 — AmneziaWG 3.1; AWG 3.0/2.x сохраняются как regression compatibility. Пакет управляет инфраструктурным состоянием вокруг Amnezia: per-peer traffic shaping, IFB ingress shaping, nftables host firewall, anti-torrent connection guard, managed swap, диагностикой drift и плановым reboot.
`AWG2 Harness` — host-side orchestrator для выделенного Debian-хоста с self-hosted AmneziaWG. Основной target релиза 0.3.1 — AmneziaWG 3.1; AWG 3.0/2.x сохраняются как regression compatibility. Пакет управляет инфраструктурным состоянием вокруг Amnezia: per-peer traffic shaping, IFB ingress shaping, Docker-совместимый host firewall через `iptables`/`ip6tables`, nftables anti-torrent guard, managed swap, диагностикой drift и плановым reboot.
Релиз source-validated по официальной документации и исходникам AWG 3.1; фактическая live-приёмка выполняется после production deployment. Harness не заменяет AmneziaVPN, не генерирует ключи клиентов и не мигрирует AWG 2.0 на 3.1. Amnezia сначала ставится официальным способом, после этого устанавливается AWG2 Harness. Точная upstream-база зафиксирована в [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md).
@@ -209,7 +209,7 @@ scp -P 2323 dist/SHA256SUMS root@<server-ip>:/root/
scp -P 2323 config.example.toml root@<server-ip>:/root/awg2-harness.config.toml
```
Перед установкой отредактируйте `/root/awg2-harness.config.toml`: SSH-порт должен совпадать с реальным sshd, `amnezia_port` — с опубликованным UDP-портом контейнера, `peer_ipv4_cidr` — с подсетью клиентов Amnezia.
Перед установкой отредактируйте `/root/awg2-harness.config.toml`: SSH-порт должен совпадать с реальным sshd, `amnezia.expected_host_udp_port` — с опубликованным Docker UDP-портом, `peer_ipv4_cidr` — с подсетью клиентов Amnezia. Это поле используется для диагностики и не создаёт host INPUT allow-rule: published bridge traffic проходит через Docker `FORWARD`/NAT path.
#### Вариант B (основной): config создаётся после установки пакета
@@ -248,7 +248,7 @@ nano /etc/awg2-harness/config.toml
В варианте B после `cp` обязательно проверьте перед первым `apply-firewall`:
- `host_firewall.ssh_port` совпадает с реальным sshd-портом;
- `host_firewall.amnezia_port` совпадает с published Docker UDP port;
- `amnezia.expected_host_udp_port` совпадает с published Docker UDP port;
- `amnezia.peer_ipv4_cidr` и `anti_torrent.peer_ipv4_cidr` совпадают с реальной peer-подсетью.
### 7. Ручная preflight-проверка
@@ -271,6 +271,8 @@ Traffic actual:
drift: no
Host firewall:
drift: no
Docker/Amnezia port path:
port_contract: ok
Anti-torrent:
drift: no
Swap:
@@ -321,13 +323,16 @@ awg2-harness --config /etc/awg2-harness/config.toml status
C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)"
PID="$(docker inspect -f '{{.State.Pid}}' "$C")"
nsenter -t "$PID" -n -- tc class show dev awg0
nsenter -t "$PID" -n -- tc class show dev ifb0
nsenter -t "$PID" -n -- tc -s class show dev awg0
nsenter -t "$PID" -n -- tc -s class show dev ifb0
nsenter -t "$PID" -n -- nft list table inet awg2_harness
nft list table inet awg2_host_firewall
iptables -S INPUT
iptables -S AWG2H_A 2>/dev/null || iptables -S AWG2H_B
ip6tables -S INPUT
ip6tables -S AWG2H_A 2>/dev/null || ip6tables -S AWG2H_B
```
Для default config на одном peer ожидаются классы `1:10` и `2:10` с `rate 50Mbit ceil 50Mbit`, root classes `1:1` и `2:1` с `500Mbit`, host firewall table `inet awg2_host_firewall` и anti-torrent table `inet awg2_harness` внутри Amnezia netns.
Для default config на одном peer ожидаются классы `1:10` и `2:10` с `rate 50Mbit ceil 50Mbit`, root classes `1:1` и `2:1` с `500Mbit`, помеченный переход из host `INPUT` в одну из цепочек `AWG2H_A`/`AWG2H_B` и anti-torrent table `inet awg2_harness` внутри Amnezia netns.
## CLI команды
@@ -337,15 +342,15 @@ nft list table inet awg2_host_firewall
awg2-harness --config /etc/awg2-harness/config.toml <command>
```
`validate-config` проверяет TOML и смысловые ограничения: пустые списки, формат rate (`50mbit`, `500mbit`), CIDR, reboot window, swap path, SSH/Amnezia ports, protocol list.
`validate-config` проверяет TOML и смысловые ограничения: пустые списки, формат rate (`50mbit`, `500mbit`), CIDR, reboot window, swap path, SSH/ожидаемый Amnezia UDP port и диапазон textual class id `10..=9999`. Удалённые поля `host_firewall.amnezia_port`/`amnezia_protocols` считаются ошибкой, а не молча игнорируются.
`validate-runtime` проверяет host tools, которые реально нужны выбранным модулям: `tc`, `ip`, `docker`, `modprobe`, `nsenter`, а также `nft`, `systemctl`, swap tooling при включённых firewall/reboot/swap. Хостовый `wg` в этот список не входит: peer state читается внутри контейнера через `docker exec` (`amnezia.wg_binaries`).
`validate-runtime` проверяет host tools, которые реально нужны выбранным модулям: `tc`, `ip`, `docker`, `modprobe`, `nsenter`, а также `iptables`, `ip6tables`, `nft`, `systemctl` и swap tooling при включённых firewall/reboot/swap. Хостовый `wg` в этот список не входит: peer state читается внутри контейнера через `docker exec` (`amnezia.wg_binaries`).
`ensure-swap` приводит swap к desired state: создаёт/проверяет swapfile, включает его с заданным priority, применяет `vm.swappiness`, делает swap state exclusive и переписывает swap entries в `/etc/fstab` под управляемый swapfile.
`apply-firewall` делает два reconcile: host INPUT firewall в таблице `inet awg2_host_firewall` и anti-torrent guard в выбранном namespace. Команда не управляет Docker NAT и host `FORWARD`.
`apply-firewall` делает два reconcile: host INPUT firewall в изолированных `iptables`/`ip6tables` A/B-цепочках и nftables anti-torrent guard в выбранном namespace. Команда не меняет policy встроенного `INPUT`, Docker NAT или host `FORWARD`.
`apply-limits` находит Amnezia container, заходит в его network namespace, читает peers через container-side `awg`/`wg`, строит per-peer HTB/fq_codel классы на AWG/WG интерфейсе и IFB ingress path. Peer filters создаются classifier-ом `flower` с единым `pref 10`; legacy peer-`фильтры `u32` считаются drift и мигрируют через controlled rebuild. Healthy state не печатает сообщение на каждом цикле daemon.
`apply-limits` находит Amnezia container, заходит в его network namespace, читает peers через container-side `awg`/`wg`, строит per-peer HTB/fq_codel классы на AWG/WG интерфейсе и IFB ingress path. Peer filters создаются classifier-ом `flower` с единым `pref 10`; legacy peer-фильтры `u32` считаются drift и мигрируют через controlled rebuild. Перед удалением проверяются qdisc handles и alias IFB `awg2-harness:traffic`; чужой объект с тем же именем является ошибкой. Healthy state не печатает сообщение на каждом цикле daemon.
`status` печатает фактическую диагностику: сервис, контейнер, netns, интерфейс, peers, WireGuard runtime, traffic desired/actual, host firewall drift, Docker/Amnezia port path, anti-torrent counters, swap state, conntrack, recent events и planned reboot.
@@ -355,11 +360,11 @@ awg2-harness --config /etc/awg2-harness/config.toml <command>
## Что происходит под капотом
Traffic shaping реализован через `tc` в network namespace контейнера Amnezia. На egress используется root qdisc `htb 1:` на AWG/WG интерфейсе, root class `1:1`, per-peer classes `1:<id>` и `fq_codel` под каждым peer class. Egress peer filter сопоставляет `dst_ip <peer>/32`, ingress peer filter — `src_ip <peer>/32`; оба используют `flower pref 10`. На ingress ставится `ingress ffff:` на AWG/WG интерфейс, входящие пакеты перенаправляются через `mirred` в `ifb0`, а там строится отдельная HTB topology `2:` с такими же per-peer лимитами. Только catch-all redirect в IFB остаётся `u32`. Вся `tc` topology после teardown передаётся одним fail-fast batch через `tc -batch -`, затем обязательно проверяется.
Traffic shaping реализован через `tc` в network namespace контейнера Amnezia. На egress используется root qdisc `htb 1:` на AWG/WG интерфейсе, root class `1:1`, per-peer classes `1:<id>` и `fq_codel` под каждым peer class. Egress peer filter сопоставляет `dst_ip <peer>/32`, ingress peer filter — `src_ip <peer>/32`; оба используют `flower pref 10`. На ingress ставится `ingress ffff:` на AWG/WG интерфейс, входящие пакеты перенаправляются через `mirred` в configured IFB, а там строится отдельная HTB topology `2:` с такими же per-peer лимитами. Только catch-all redirect в IFB остаётся `u32`. `tc -batch -` является fail-fast, но не транзакционным: при любой ошибке Harness удаляет только доказанно свои частичные объекты, сохраняет первичную и cleanup-ошибку и затем обязательно выполняет verify. Счётчики peers берутся из `tc -s class`, потому что class-only `flower` не гарантирует action statistics.
Peer discovery берётся из WireGuard/AmneziaWG allowed IPs, а не из handshake state. Поэтому offline peer с `allowed ips: 10.8.1.1/32` тоже получает лимит заранее.
Host firewall — отдельная AWG2-owned таблица `inet awg2_host_firewall`. Она открывает loopback, established/related, SSH-порт из конфига, Amnezia-порт по указанным протоколам и optionally ICMP/ICMPv6. Docker-managed `FORWARD` и NAT не трогаются.
Host firewall владеет только помеченным переходом из встроенного `INPUT` и двумя A/B-цепочками `AWG2H_A`/`AWG2H_B` отдельно для IPv4 и IPv6. Цепочка разрешает loopback, established/related, SSH-порт из конфига и optionally ICMP/ICMPv6, затем применяет configured verdict. Новая цепочка полностью проверяется до переключения; ошибка IPv6 после переключения IPv4 запускает rollback. Published Amnezia UDP port здесь намеренно не открывается: Docker bridge publish обслуживается через Docker-managed `FORWARD`/NAT path.
Anti-torrent guard — отдельная AWG2-owned таблица `inet awg2_harness` обычно внутри Amnezia netns. Правило ограничивает число новых conntrack flows per source VPN IP. Это не DPI и не распознавание BitTorrent-протокола; это грубый, дешёвый guard против сотен одновременных P2P-соединений.
@@ -369,8 +374,8 @@ Swap module намеренно строгий: если `swap.enabled=true`, des
AWG2 Harness владеет только собственным state:
- `tc` topology на Amnezia interface и configured `ifb_name`;
- nft table `inet awg2_host_firewall` в host namespace;
- `tc` topology с handles `1:`/`ffff:` на Amnezia interface и IFB с alias `awg2-harness:traffic`;
- помеченный host `INPUT` jump и цепочки `AWG2H_A`/`AWG2H_B` в `iptables`/`ip6tables`;
- nft table `inet awg2_harness` для anti-torrent;
- `/run/awg2-harness/*` runtime state;
- managed swapfile и swap entries, если `swap.enabled=true`.