fix: harden firewall and traffic reconciliation
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
- [ ] Есть ingress qdisc `ffff:` на AWG/WG-интерфейсе.
|
||||
- [ ] Есть ingress redirect через `mirred ... redirect dev ifb0`.
|
||||
- [ ] `ifb0` существует и находится в UP state.
|
||||
- [ ] `ip -details link show ifb0` показывает alias `awg2-harness:traffic`; чужой IFB без этого alias не удаляется и не заменяется.
|
||||
- [ ] На `ifb0` есть root qdisc `2: htb`.
|
||||
- [ ] На `ifb0` есть root class `2:1` с `traffic.root_rate`.
|
||||
- [ ] Для каждого peer есть class `2:<id>` с `traffic.peer_limit`.
|
||||
@@ -49,6 +50,7 @@
|
||||
- [ ] Для каждого peer есть ingress filter `flower pref 10 src_ip peer_ip/32 -> 2:<id>`.
|
||||
- [ ] Все peer-фильтры на каждом parent используют один `pref 10`; `u32` остаётся только у catch-all ingress redirect.
|
||||
- [ ] Class ids начинаются с `traffic.class_start_id`, default `10`.
|
||||
- [ ] `class_start_id` и последний фактический peer class остаются в textual-диапазоне `10..=9999`; конфигурация/число peers, требующие `10000`, отвергаются до apply.
|
||||
- [ ] Peer IP → class id mapping использует стабильный лексикографический контракт: IP сортируются как строки, не numeric IPv4.
|
||||
|
||||
## D. Идемпотентность и восстановление после drift
|
||||
@@ -68,6 +70,8 @@
|
||||
- [ ] После добавления peer появляются новые class/filter.
|
||||
- [ ] Legacy peer-фильтры `u32` детектируются как migration drift и заменяются на `flower` через rebuild.
|
||||
- [ ] Ошибка в любой строке `tc -batch -` останавливает apply; после успеха проходит verify-after-apply.
|
||||
- [ ] Ошибка после частичного `tc -batch -` запускает cleanup только qdisc handles `1:`/`ffff:` и IFB с доказанным владением; в сообщении сохранены и primary, и cleanup error.
|
||||
- [ ] Чужой root qdisc, `clsact` или IFB с тем же именем вызывает явный ownership conflict и остаётся нетронутым.
|
||||
|
||||
## E. Самовосстановление в жизненном цикле
|
||||
|
||||
@@ -84,6 +88,8 @@
|
||||
- [ ] На старте daemon сбой `sysctl_tuning` или `swap` завершает процесс, а сбой traffic/firewall только логируется — сервис остаётся живым.
|
||||
- [ ] После обрыва потока `docker events` он перезапускается автоматически (проверяется, например, `systemctl restart docker`).
|
||||
- [ ] После восстановления потока `docker events` ошибка подсистемы снимается: в `events.log` появляется `docker_events=recovered`, а `last_action.state` не остаётся с этой ошибкой навсегда.
|
||||
- [ ] Readiness retry после start event повторяется только для typed `NotFound`; Docker permission/API/parse error немедленно отражается как ошибка, timeout не превращается в silent skip.
|
||||
- [ ] Start event lower-priority контейнера игнорируется, если уже выбран running container с более высоким приоритетом в `amnezia.container_names`.
|
||||
- [ ] Один startup/periodic/event-цикл использует один discovery `RuntimeTarget` для traffic и anti-torrent, а не повторяет `docker inspect` и interface discovery.
|
||||
- [ ] Успешный reconcile одной подсистемы не гасит незакрытую ошибку другой: при постоянно падающем `traffic` успешный `firewall` не очищает `last_error` и не пишет `firewall=recovered`.
|
||||
- [ ] Параллельный запуск `apply-limits` во время работы daemon не приводит к гонке: второй процесс ждёт блокировку до 10 секунд и затем сообщает `lock busy timeout`.
|
||||
@@ -97,8 +103,7 @@
|
||||
- [ ] `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`.
|
||||
- [ ] Flower counters и legacy u32 counters в `status` разбираются одним parser-ом и привязываются к правильному peer IP.
|
||||
- [ ] `status` показывает egress/ingress packet counters из `tc -s class show` по peer class или `n/a`; class-only flower action statistics не используются.
|
||||
- [ ] Логи доступны через `journalctl -u awg2-harness`.
|
||||
- [ ] Повтор одной и той же reconcile-ошибки не создаёт `error`-запись на каждом цикле; первая ошибка, изменение её текста и recovery видны как отдельные переходы.
|
||||
- [ ] Отсутствие обязательной host-утилиты (`tc`, `ip`, `docker`, `nsenter`) логируется явно с именем утилиты.
|
||||
@@ -194,12 +199,12 @@
|
||||
- [ ] При превышении порога новые 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` и `nf_conntrack_utilization`, если sysctl доступны.
|
||||
- [ ] `status` показывает `count_namespace`, `nf_conntrack_count`, глобальный `nf_conntrack_max_global` и utilization; при netns anti-torrent count читается внутри Amnezia, а max — на host.
|
||||
|
||||
## J. Откат
|
||||
|
||||
- [ ] Известно, как остановить и отключить `awg2-harness.service` и `awg2-reboot-scheduler.service`.
|
||||
- [ ] Известно, как удалить только AWG2-owned host firewall state (`inet awg2_host_firewall`).
|
||||
- [ ] Известно, как удалить только AWG2-owned host firewall state (comment jump `awg2-harness:host-firewall`, chains `AWG2H_A`/`AWG2H_B` в обеих IP-family).
|
||||
- [ ] Известно, как удалить только AWG2-owned anti-torrent state (`inet awg2_harness`) в выбранном namespace.
|
||||
- [ ] Известно, как удалить AWG2-owned `tc` topology (`root`, `ingress`, `ifb0`) в netns Amnezia.
|
||||
- [ ] При необходимости rollback включает удаление пакета `awg2-harness` после остановки сервисов.
|
||||
@@ -218,6 +223,7 @@
|
||||
- [ ] Пакет не включает/не стартует сервис автоматически.
|
||||
- [ ] Пакет не имеет `Depends`/`Recommends` на `docker.io`; установка Amnezia/Docker внешняя.
|
||||
- [ ] Пакет не имеет `Depends` на `wireguard-tools`: peer state читается внутри контейнера, host-side `wg` не используется.
|
||||
- [ ] Пакет имеет runtime `Depends` на `iptables` и `nftables`.
|
||||
- [ ] `apt install ./awg2-harness_<version>_amd64.deb` не устанавливает и не заменяет Docker.
|
||||
- [ ] `apt install ./awg2-harness_<version>_amd64.deb` проходит на clean Debian 13 Amnezia host.
|
||||
- [ ] После ручной установки config `awg2-harness validate-config` проходит.
|
||||
@@ -246,17 +252,20 @@
|
||||
|
||||
- [ ] `awg2-harness validate-config` падает, если отсутствует секция `[host_firewall]`.
|
||||
- [ ] `apply-firewall` не блокирует SSH на `host_firewall.ssh_port` (default `2323`).
|
||||
- [ ] После `apply-firewall` порт `host_firewall.amnezia_port` (default `443`) доступен по всем протоколам из `host_firewall.amnezia_protocols`.
|
||||
- [ ] Встроенная policy `INPUT` не меняется; managed jump с comment `awg2-harness:host-firewall` стоит первым и ведёт в одну из цепочек `AWG2H_A`/`AWG2H_B` для IPv4 и IPv6.
|
||||
- [ ] Managed chain разрешает loopback, established/related, configured SSH и optional ICMP/ICMPv6, затем завершает обработку configured `DROP`/`ACCEPT` verdict.
|
||||
- [ ] Host firewall не добавляет allow-rule для published Amnezia UDP port: bridge publish проходит через Docker-managed `FORWARD`/NAT.
|
||||
- [ ] Если эксплуатационно требуется закрыть legacy SSH `22`, это явно подтверждено проверкой доступа на `ssh_port` перед закрытием `22`.
|
||||
- [ ] При `host_firewall.enabled = false` удаляется только таблица `inet awg2_host_firewall`.
|
||||
- [ ] При `host_firewall.enabled = false` удаляются только помеченные jumps и доказанно owned `AWG2H_A`/`AWG2H_B`; чужие rules/chains остаются нетронутыми.
|
||||
- [ ] Повторный `apply-firewall` идемпотентен и не вносит лишних изменений в healthy state.
|
||||
- [ ] При невалидном `host_firewall.amnezia_protocols` (`icmp`, `foo`, дубли) `validate-config` завершается ошибкой.
|
||||
- [ ] Legacy fields `host_firewall.amnezia_port`/`amnezia_protocols` отвергаются как `unknown field`.
|
||||
- [ ] Новая конфигурация строится в inactive A/B chain до switch; искусственный сбой `ip6tables` после IPv4 switch возвращает IPv4 на прежнюю цепочку.
|
||||
- [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой.
|
||||
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
||||
- [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика.
|
||||
- [ ] `status` показывает блок `Docker/Amnezia port path` с полями `docker_userland_proxy`, `published_udp_port` и note про Docker-managed `FORWARD`.
|
||||
- [ ] `status` показывает блок `Docker/Amnezia port path` с полями `expected_host_udp_port`, `docker_userland_proxy`, `published_udp_port`, `awg_listen_udp_port`, `port_contract` и note про Docker-managed `FORWARD`.
|
||||
- [ ] `status` показывает поля `host_forward_policy_drop` и `docker_forward_rules_present` в блоке `Docker/Amnezia port path`.
|
||||
- [ ] Если `docker_userland_proxy=false` и `FORWARD policy DROP`, при отсутствии явных Docker accept-признаков `status` выводит warning, но `awg2-harness` не изменяет `FORWARD` автоматически.
|
||||
- [ ] Сверено соответствие: `config host_firewall.amnezia_port` ↔ опубликованный Docker UDP host port ↔ `docker exec amnezia-awg2 awg show` (`listening port`).
|
||||
- [ ] При нескольких UDP bindings без однозначного соответствия `host_firewall.amnezia_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт.
|
||||
- [ ] Сверено соответствие: `amnezia.expected_host_udp_port` ↔ опубликованный Docker UDP host port ↔ `docker exec <container> awg show <interface> listen-port`; `port_contract: ok`.
|
||||
- [ ] При нескольких UDP bindings без однозначного соответствия `amnezia.expected_host_udp_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт.
|
||||
- [ ] Для диагностики FORWARD используется best-effort проверка: `iptables -S FORWARD` и fallback через `nft list ruleset`; `status` не падает при недоступности одной из команд.
|
||||
|
||||
@@ -89,15 +89,17 @@ max_connections_per_peer = 500
|
||||
|
||||
`ct count` использует conntrack. Для примера 23 peers × 500 flows дают до 11 500 потенциальных flow slots перед ограничением каждого peer. Само по себе это не является доказательством достаточной ёмкости конкретного хоста, поэтому фактическое состояние conntrack обязательно наблюдается.
|
||||
|
||||
Что надо мониторить:
|
||||
Что надо мониторить (count — в namespace, где работает guard; max — глобально на host):
|
||||
|
||||
```bash
|
||||
sysctl net.netfilter.nf_conntrack_count
|
||||
C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)"
|
||||
AWG_PID="$(docker inspect -f '{{.State.Pid}}' "$C")"
|
||||
nsenter -t "$AWG_PID" -n -- sysctl net.netfilter.nf_conntrack_count
|
||||
sysctl net.netfilter.nf_conntrack_max
|
||||
nft list table inet awg2_harness
|
||||
nsenter -t "$AWG_PID" -n -- nft list table inet awg2_harness
|
||||
```
|
||||
|
||||
Если `nf_conntrack_count` приближается к `nf_conntrack_max`, проблема уже системная: надо разбираться с timeout, abuse или capacity. `awg2-harness status` показывает оба значения и вычисленный `nf_conntrack_utilization`, чтобы давление было видно без ручного деления. Не надо автоматически увеличивать `nf_conntrack_max` из orchestrator без отдельного решения.
|
||||
Если `nf_conntrack_count` приближается к `nf_conntrack_max`, проблема уже системная: надо разбираться с timeout, abuse или capacity. `awg2-harness status` показывает `count_namespace`, count, глобальный max и вычисленный utilization. При недоступном container namespace host count намеренно не подставляется. Не надо автоматически увеличивать `nf_conntrack_max` из orchestrator без отдельного решения.
|
||||
|
||||
## 7. Критерии приёмки
|
||||
|
||||
|
||||
+13
-18
@@ -4,7 +4,7 @@
|
||||
|
||||
## Создание runtime config
|
||||
|
||||
Пакет намеренно **не** создаёт `/etc/awg2-harness/config.toml` автоматически. Это снижает риск случайно применить firewall с неверным SSH-портом или неверным Amnezia-портом.
|
||||
Пакет намеренно **не** создаёт `/etc/awg2-harness/config.toml` автоматически. Это снижает риск случайно применить firewall с неверным SSH-портом или проверять не тот Amnezia UDP-порт.
|
||||
|
||||
После установки `.deb` создайте runtime config вручную:
|
||||
|
||||
@@ -17,7 +17,7 @@ nano /etc/awg2-harness/config.toml
|
||||
Перед первым `apply-firewall`/`apply-limits` оператор обязан сверить:
|
||||
|
||||
- `host_firewall.ssh_port` — должен совпадать с реально слушающим `sshd` портом;
|
||||
- `host_firewall.amnezia_port` — должен совпадать с published Docker/Amnezia UDP port;
|
||||
- `amnezia.expected_host_udp_port` — должен совпадать с published Docker/Amnezia UDP port;
|
||||
- `amnezia.peer_ipv4_cidr` — должен совпадать с реальной peer-подсетью;
|
||||
- `anti_torrent.peer_ipv4_cidr` — обычно тот же CIDR.
|
||||
|
||||
@@ -40,6 +40,7 @@ interface_candidates = ["awg0", "wg0", "amnezia0", "tun0"]
|
||||
interface_patterns = ["awg*", "wg*", "amnezia*", "tun*"]
|
||||
wg_binaries = ["awg", "wg"]
|
||||
peer_ipv4_cidr = "10.8.1.0/24"
|
||||
expected_host_udp_port = 443
|
||||
|
||||
[traffic]
|
||||
enabled = true
|
||||
@@ -66,8 +67,6 @@ udp_rmem_min = 8192
|
||||
[host_firewall]
|
||||
enabled = true
|
||||
ssh_port = 2323
|
||||
amnezia_port = 443
|
||||
amnezia_protocols = ["udp"]
|
||||
allow_icmp = true
|
||||
default_input_policy = "drop"
|
||||
|
||||
@@ -104,6 +103,8 @@ swappiness = 1
|
||||
|
||||
`amnezia.peer_ipv4_cidr` — подсеть VPN peers. Production default — `10.8.1.0/24`; значение конфигурируемое.
|
||||
|
||||
`amnezia.expected_host_udp_port` — ожидаемый published UDP host port Docker для AmneziaWG. Поле обязательное и используется диагностикой `status` для контракта `expected host port ↔ Docker binding ↔ awg/wg listen-port`. Оно не создаёт host INPUT allow-rule: трафик к published bridge container проходит через Docker `FORWARD`/NAT path, а не через host `INPUT`.
|
||||
|
||||
`traffic.peer_limit` — per-peer лимит в обе стороны. Текущий production default — `50mbit`.
|
||||
|
||||
`traffic.root_rate` — верхний HTB-класс, в текущем production default — `500mbit`.
|
||||
@@ -112,9 +113,9 @@ swappiness = 1
|
||||
|
||||
`traffic.container_ready_timeout_secs` — сколько ждать появления интерфейса после старта контейнера.
|
||||
|
||||
`traffic.class_start_id` — первый class id. Production default — `10`, что даёт классы `1:10`, `2:10` и дальше.
|
||||
`traffic.class_start_id` — первый class id. Production default — `10`, что даёт классы `1:10`, `2:10` и дальше. Допустимый диапазон `10..=9999`; каждый фактический peer class также обязан остаться не выше `9999`. Это граница текстового представления: `tc` разбирает class minor как hexadecimal, поэтому следующая десятичная строка `10000` уже вышла бы за 16-bit minor id.
|
||||
|
||||
`traffic.ifb_name` — имя IFB-интерфейса для ingress shaping. Production default — `ifb0`. Этот параметр можно оставить в конфиге, потому что бинарь реально создаёт/удаляет этот интерфейс.
|
||||
`traffic.ifb_name` — имя IFB-интерфейса для ingress shaping. Production default — `ifb0`. Harness ставит созданному IFB alias `awg2-harness:traffic` и удаляет его только при совпадении type+alias. Существующий чужой интерфейс с тем же именем вызывает ошибку и не заменяется.
|
||||
|
||||
`sysctl_tuning` — отдельный host-level модуль управляемого сетевого baseline через `sysctl`:
|
||||
|
||||
@@ -153,31 +154,25 @@ swappiness = 1
|
||||
|
||||
`anti_torrent.enabled` — включает nftables connection-count guard против high-peer-count P2P-нагрузки. Это отдельный AWG2 Harness module и часть текущего production-контракта.
|
||||
|
||||
`host_firewall.enabled` — включает host-level INPUT firewall в отдельной AWG2-owned таблице `inet awg2_host_firewall`.
|
||||
`host_firewall.enabled` — включает host-level INPUT firewall. Harness владеет только помеченным comment переходом из встроенного `INPUT` и изолированными цепочками `AWG2H_A`/`AWG2H_B` в `iptables` и `ip6tables`. Встроенная policy, Docker `FORWARD`/NAT и чужие правила не очищаются и не меняются.
|
||||
|
||||
`host_firewall.ssh_port` — SSH порт хоста, который обязательно должен быть открыт. Production default/example: `2323`. Значение обязано совпадать с реально слушающим sshd портом до применения `apply-firewall`.
|
||||
|
||||
`host_firewall.amnezia_port` — внешний UDP/TCP порт Amnezia, который AWG2 открывает в host `INPUT` (в зависимости от `host_firewall.amnezia_protocols`). В текущей целевой конфигурации default — `443`; значение должно совпадать с реально опубликованным Amnezia UDP/TCP port.
|
||||
Поля `host_firewall.amnezia_port` и `host_firewall.amnezia_protocols` удалены. Если они остались в старом runtime config, `validate-config` завершится ошибкой `unknown field`, чтобы оператор явно мигрировал значение в `amnezia.expected_host_udp_port`. Host firewall не должен открывать published Docker port в `INPUT`.
|
||||
|
||||
Важно: `host_firewall.amnezia_port` — это transport-порт AmneziaWG/AWG (например, `443/udp`). Это не порт AmneziaDNS.
|
||||
|
||||
- Не задавайте `amnezia_port = 53`.
|
||||
- Не добавляйте DNS/53 в `amnezia_protocols`.
|
||||
- Не открывайте `53/tcp` или `53/udp` в AWG2 host firewall для ожидаемого production-профиля.
|
||||
Не открывайте `53/tcp` или `53/udp` в AWG2 host firewall для ожидаемого production-профиля.
|
||||
|
||||
В expected production layout AmneziaDNS работает как внутренний Docker/VPN сервис и не публикуется на public host interface.
|
||||
|
||||
Для диагностического блока `Docker/Amnezia port path` в `status` приоритет сопоставления такой: сначала ищется published UDP binding с `HostPort == host_firewall.amnezia_port`, затем fallback на ключ `"<amnezia_port>/udp"` как container port, затем (если UDP binding ровно один) берётся он. Если UDP bindings несколько и однозначного совпадения нет, статус показывает `published_udp_port: unknown`.
|
||||
Для диагностического блока `Docker/Amnezia port path` в `status` приоритет сопоставления такой: сначала ищется published UDP binding с `HostPort == amnezia.expected_host_udp_port`, затем fallback на ключ `"<expected_host_udp_port>/udp"` как container port, затем (если UDP binding ровно один) берётся он. Внутри контейнера выполняется `awg show <interface> listen-port` с fallback на `wg`. `port_contract: ok` означает, что host port совпал с config, а container port Docker binding — с фактическим listen-port. При нескольких UDP bindings без однозначного соответствия или ошибке probe выводится `unknown`, а не произвольный первый порт.
|
||||
|
||||
Диагностика `FORWARD` в этом блоке является best-effort: сначала используется `iptables -S FORWARD`, затем fallback через `nft list ruleset`; результаты только отображаются в `status` и не изменяют host `FORWARD`/Docker NAT.
|
||||
|
||||
`host_firewall.amnezia_protocols` — протоколы для порта Amnezia (`udp`, `tcp` или оба).
|
||||
|
||||
Важно: `awg2-harness` не управляет host `FORWARD`/Docker NAT-цепочками. Проект сознательно не добавляет универсальные правила в `FORWARD` и не меняет Docker-managed forwarding path.
|
||||
|
||||
`host_firewall.allow_icmp` — разрешать ICMP/ICMPv6 на входе.
|
||||
|
||||
`host_firewall.default_input_policy` — политика INPUT chain (`drop` или `accept`). Для production рекомендуется `drop`.
|
||||
`host_firewall.default_input_policy` — конечный verdict собственной цепочки (`drop` или `accept`). Для production рекомендуется `drop`. Это поле не меняет policy встроенной цепочки `INPUT`.
|
||||
|
||||
`anti_torrent.max_connections_per_peer` — максимум одновременных conntrack flows на один VPN peer IP. Штатный production baseline — `500`, выбранный по эксплуатационным измерениям проекта так, чтобы connection guard оставался включённым и не вмешивался в нормальный повседневный профиль. Значение ниже `500` является сознательно более агрессивной политикой и требует проверки на реальных клиентах. Поле обязательное и не скрыто в Rust default.
|
||||
|
||||
@@ -190,7 +185,7 @@ swappiness = 1
|
||||
|
||||
Для production рекомендуется `true`, если peer source IP (`10.8.1.x`) видны именно внутри netns контейнера.
|
||||
|
||||
`apply-firewall` в текущей модели делает **оба** reconcile: host firewall (host namespace) и anti-torrent firewall (configured namespace).
|
||||
`apply-firewall` в текущей модели делает **оба** reconcile: iptables/ip6tables host firewall (host namespace) и nftables anti-torrent firewall (configured namespace). Host-цепочки обновляются через A/B staging: обе IP-family сначала строятся и проверяются, после чего jump переключается; при ошибке IPv6 выполненный IPv4 switch откатывается.
|
||||
|
||||
`reboot.enabled` — включает плановую перезагрузку.
|
||||
|
||||
|
||||
+28
-17
@@ -149,7 +149,7 @@ traffic_core/src/
|
||||
├── firewall/
|
||||
│ ├── mod.rs apply_firewall, выбор namespace, общие константы
|
||||
│ ├── nft.rs нормализация текста nftables и обёртки команд
|
||||
│ ├── host.rs таблица inet awg2_host_firewall (host namespace)
|
||||
│ ├── host.rs owned iptables/ip6tables INPUT chains (host namespace)
|
||||
│ └── antitorrent.rs таблица inet awg2_harness (namespace по конфигу)
|
||||
├── sysctl_tuning.rs управляемый sysctl baseline
|
||||
├── swap.rs управляемый swap
|
||||
@@ -163,7 +163,7 @@ reboot_module/src/
|
||||
|
||||
`traffic` разбит по ответственности: парсеры отдельно от инспекции, инспекция отдельно от apply. Чистые парсеры собраны в `parse_tc.rs` вместе со своими тестами — это тестируемая поверхность модуля, всё остальное требует живого контейнера.
|
||||
|
||||
`firewall` разбит по подсистемам, а не по фазам: host firewall и anti-torrent независимы, применяются в разных namespace и владеют разными таблицами. Общий у них только слой работы с `nft`.
|
||||
`firewall` разбит по подсистемам, а не по фазам: host firewall и anti-torrent независимы. Host firewall использует совместимый с Docker frontend `iptables`/`ip6tables`; anti-torrent владеет отдельной nftables-таблицей в configured namespace.
|
||||
|
||||
Приватные типы объявлены в соответствующем `mod.rs`. В Rust приватные элементы видны в модуле объявления и всех дочерних модулях, поэтому подмодули работают с ними без расширения видимости до `pub(crate)`.
|
||||
|
||||
@@ -218,6 +218,8 @@ struct PeerTrafficStatus {
|
||||
|
||||
Присвоение class id обязано быть детерминированным. Строки peer IP сортируются лексикографически, затем присваивается `class_start_id + index`. Это нельзя молча заменить на числовую сортировку IPv4: production-контракт class-id ставит `10.8.1.10` раньше `10.8.1.2`, потому что сортировка текстовая, а не числовая.
|
||||
|
||||
Рендеримое textual-значение minor id ограничено `9999`. `tc` интерпретирует class id как hexadecimal; переход от строки `9999` к `10000` вышел бы за 16-bit minor. Проверяются и стартовое значение, и последний id для фактического числа peers.
|
||||
|
||||
## 5. Стратегия namespace
|
||||
|
||||
Основной путь:
|
||||
@@ -321,6 +323,7 @@ Ingress через IFB:
|
||||
```text
|
||||
modprobe ifb
|
||||
ip link add name <ifb_name> type ifb
|
||||
ip link set dev <ifb_name> alias awg2-harness:traffic
|
||||
ip link set dev <ifb_name> up
|
||||
tc qdisc add dev <iface> handle ffff: ingress
|
||||
tc filter add dev <iface> parent ffff: protocol all u32 match u32 0 0 action mirred egress redirect dev <ifb_name>
|
||||
@@ -358,27 +361,27 @@ reconcile_traffic():
|
||||
```text
|
||||
apply_firewall():
|
||||
взять /run/awg2-harness/firewall.lock (timeout 10 c)
|
||||
apply_host_firewall(config) # host namespace, таблица inet awg2_host_firewall
|
||||
apply_host_firewall(config) # host namespace, iptables/ip6tables A/B chains
|
||||
apply_antitorrent_firewall(config) # namespace по anti_torrent.apply_in_amnezia_netns
|
||||
```
|
||||
|
||||
Каждый из firewall-путей сначала инспектирует фактическое состояние и выходит без изменений, если drift отсутствует. При наличии drift владеемая таблица удаляется и создаётся заново, после чего выполняется обязательная повторная инспекция.
|
||||
Каждый из firewall-путей сначала инспектирует фактическое состояние и выходит без изменений, если drift отсутствует. Anti-torrent таблица заменяется одной атомарной nft-транзакцией. Host firewall сначала строит и проверяет inactive chain отдельно для IPv4/IPv6, затем переключает помеченный jump; сбой IPv6 после IPv4 switch откатывает IPv4. После apply выполняется обязательная повторная инспекция.
|
||||
|
||||
Для production контролируемая полная пересборка при drift — единственный поддерживаемый путь.
|
||||
|
||||
Требования к устойчивости парсеров:
|
||||
|
||||
- `tc filter show` разбирается блоками (конечный автомат), а не наивным сканированием токенов по несвязанным строкам;
|
||||
- единый parser понимает production `flower` и legacy `u32`, включая counters; точный healthy-контракт требует `flower`, `pref 10`, правильное direction-field и `/32`, а legacy `u32` peer rules считаются migration drift;
|
||||
- единый parser понимает production `flower` и legacy `u32`; точный healthy-контракт требует `flower`, `pref 10`, правильное direction-field и `/32`, а legacy `u32` peer rules считаются migration drift;
|
||||
- нераспознанный или malformed detailed filter block не игнорируется, а делает инспекцию drifted;
|
||||
- отображение `peer_ip -> class_id` проверяется для обеих сторон: egress (`1:`) и ingress (`2:`);
|
||||
- проверка обязана падать, если классы и фильтры существуют, но отображение отличается от детерминированного присвоения.
|
||||
|
||||
Политика apply/verify:
|
||||
|
||||
- `modprobe ifb`, создание IFB и его перевод в UP выполняются строго и до `tc` apply;
|
||||
- `modprobe ifb`, создание IFB, установка ownership alias и перевод в UP выполняются строго и до `tc` apply;
|
||||
- после teardown вся `tc` topology подаётся одним fail-fast сценарием в stdin команды `tc -batch -`; `-force` не используется, поэтому первая ошибка останавливает batch и сохраняет номер строки в stderr;
|
||||
- ожидаемые конфликты идемпотентности допускаются выборочно, через отдельную обёртку, и по закрытому списку признаков: `file exists`, `eexist`, `object already exists`, `exclusive flag on`;
|
||||
- `tc -batch` не является транзакцией: любая ошибка после частичного применения запускает cleanup доказанно owned handles/IFB; если cleanup тоже падает, в итоговой ошибке сохраняются обе причины;
|
||||
- ошибки «объект не найден» (`no such`, `not found`, `cannot find`, `does not exist`, `handle of zero`) допускаются только на шагах удаления — в teardown и при отключении владеемых таблиц;
|
||||
- любая другая ошибка apply фатальна;
|
||||
- `verify-after-apply` обязателен и для traffic, и для anti-torrent.
|
||||
@@ -394,13 +397,15 @@ apply_firewall():
|
||||
|
||||
- root qdisc `1:` на выбранном интерфейсе AWG/WG;
|
||||
- ingress qdisc `ffff:` на выбранном интерфейсе AWG/WG, когда лимиты включены;
|
||||
- IFB-устройством с именем `traffic.ifb_name` (default `ifb0`) в целевом namespace;
|
||||
- IFB-устройством с именем `traffic.ifb_name` (default `ifb0`), type `ifb` и alias `awg2-harness:traffic` в целевом namespace;
|
||||
- root qdisc `2:` на IFB;
|
||||
- классами и фильтрами под этими handles.
|
||||
|
||||
Teardown может удалять и пересоздавать эти владеемые объекты. Поскольку сервер выделенный, это допустимо и предпочтительнее, чем оставлять устаревшие или частичные правила.
|
||||
До первой операции удаления teardown инспектирует все цели. На AWG/WG-интерфейсе owned считаются только root `htb 1:` и ingress `ffff:`; `clsact` и любой другой root — ownership conflict. IFB удаляется только при совпадении type+alias (кроме IFB, доказанно созданного текущей неуспешной попыткой). Чужие объекты не заменяются даже на выделенном сервере.
|
||||
|
||||
Бинарь не должен вслепую сбрасывать посторонние host firewall rules, Docker NAT rules или чужие интерфейсы. Владение firewall обязано быть явным и ограниченным таблицами AWG2 Harness: `inet awg2_harness` в целевом network namespace и `inet awg2_host_firewall` в host namespace.
|
||||
Бинарь не должен вслепую сбрасывать посторонние host firewall rules, Docker NAT rules или чужие интерфейсы. Владение firewall явно ограничено таблицей `inet awg2_harness`, comment jump `awg2-harness:host-firewall` и цепочками `AWG2H_A`/`AWG2H_B`. Встроенные `INPUT`/`FORWARD`, их policy и чужие chains не очищаются.
|
||||
|
||||
Host firewall не открывает published Amnezia UDP port в `INPUT`: Docker bridge publish проходит через `FORWARD`/NAT. Собственная chain разрешает loopback, established/related, configured SSH и optional ICMP/ICMPv6, затем завершает обработку configured `DROP`/`ACCEPT` verdict. Поле `amnezia.expected_host_udp_port` относится только к диагностике port path.
|
||||
|
||||
## 11. Anti-torrent firewall guard
|
||||
|
||||
@@ -444,7 +449,7 @@ table inet awg2_harness {
|
||||
- counter `antitorrent_dropped_v4`;
|
||||
- meter/set state, создаваемый правилом.
|
||||
|
||||
Status обязан показывать наличие таблицы/chain/правила, настроенный порог и drop counter. Также он один раз читает `net.netfilter.nf_conntrack_count` и `net.netfilter.nf_conntrack_max` и, когда оба значения доступны, печатает `nf_conntrack_utilization` в процентах.
|
||||
Status обязан показывать наличие таблицы/chain/правила, настроенный порог и drop counter. При anti-torrent в Amnezia netns `net.netfilter.nf_conntrack_count` читается в этом namespace, а глобальный `net.netfilter.nf_conntrack_max` — на host. Если container transport недоступен, host count не подставляется как ложный fallback.
|
||||
|
||||
При `anti_torrent.enabled = false` демон удаляет только владеемую anti-torrent таблицу. Глобальный ruleset nftables не сбрасывается никогда. Если контейнер в этот момент именно не найден, удаление в его namespace пропускается — это не ошибка. Ошибка Docker API/daemon/permissions не маскируется под «контейнер не найден» и возвращается оператору.
|
||||
|
||||
@@ -480,7 +485,7 @@ Status обязан показывать наличие таблицы/chain/п
|
||||
|
||||
Внутри одного startup/periodic/event-цикла traffic и anti-torrent получают один и тот же результат discovery `RuntimeTarget`; повторные `docker inspect`, PID/interface discovery и transport construction в этом же цикле не выполняются. Наличие host `nsenter` кэшируется на время жизни процесса.
|
||||
|
||||
Проверка готовности не должна хардкодить `awg0`. Она вызывает общий детектор интерфейса с интервалом 2 секунды до истечения `traffic.container_ready_timeout_secs`.
|
||||
Проверка готовности не должна хардкодить `awg0`. Она вызывает общий детектор интерфейса с интервалом до 2 секунд до истечения `traffic.container_ready_timeout_secs`. Retry разрешён только для typed `AppError::NotFound`; Docker/API/permission/parse errors возвращаются немедленно. Истечение timeout является ошибкой, а не silent skip. Если start event пришёл от lower-priority container, но discovery уже выбрал running container выше в `amnezia.container_names`, событие игнорируется без reconcile.
|
||||
|
||||
Порядок на старте демона и разница в критичности ошибок:
|
||||
|
||||
@@ -550,11 +555,12 @@ Traffic actual:
|
||||
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
|
||||
IP, Egress packets, Ingress packets
|
||||
Host firewall:
|
||||
enabled, ssh_port, amnezia_port, input_policy, drift[, reason]
|
||||
enabled, ssh_port, input_policy, drift[, reason]
|
||||
Docker/Amnezia port path:
|
||||
docker_userland_proxy, published_udp_port, host_forward_policy_drop,
|
||||
expected_host_udp_port, docker_userland_proxy, published_udp_port,
|
||||
awg_listen_udp_port, port_contract, host_forward_policy_drop,
|
||||
docker_forward_rules_present, note[, warning]
|
||||
Anti-torrent:
|
||||
enabled, max_connections_per_peer, drift[, reason], dropped_packets, dropped_bytes
|
||||
@@ -563,7 +569,7 @@ Swap:
|
||||
desired_active, exclusive, fstab_exclusive, fstab_swap_entries,
|
||||
swappiness_current, swappiness_persisted, drift[, drift_reason]
|
||||
Conntrack:
|
||||
nf_conntrack_count, nf_conntrack_max, nf_conntrack_utilization
|
||||
count_namespace, nf_conntrack_count, nf_conntrack_max_global, nf_conntrack_utilization
|
||||
Last actions:
|
||||
содержимое /run/awg2-harness/last_action.state
|
||||
Recent events:
|
||||
@@ -715,8 +721,13 @@ inode = 123456
|
||||
- порядок выбора бинаря wg и ошибка при отсутствии всех кандидатов;
|
||||
- фильтрация по CIDR;
|
||||
- детерминированное присвоение class id с контрактом лексикографической стабильности;
|
||||
- рендеринг и разбор ruleset nftables для владеемых таблиц;
|
||||
- textual boundary class id `9999`/`10000`;
|
||||
- рендеринг и разбор ruleset nftables для anti-torrent и ownership parser host iptables chains;
|
||||
- парсер `tc` для снимков qdisc/class/filter;
|
||||
- packet statistics из Debian-подобного `tc -s class`;
|
||||
- отказ удалять foreign IFB/root qdisc;
|
||||
- typed Docker readiness retry/timeout/lower-priority event;
|
||||
- conntrack count из Amnezia namespace и global max с host;
|
||||
- diff drift;
|
||||
- расчёт окна перезагрузки;
|
||||
- идемпотентность fstab и sysctl.
|
||||
|
||||
@@ -224,7 +224,7 @@ scp -P 2323 config.example.toml root@<server-ip>:/root/awg2-harness.config.toml
|
||||
Перед установкой отредактируйте `/root/awg2-harness.config.toml`. Минимально проверьте:
|
||||
|
||||
- `[host_firewall].ssh_port` совпадает с реальным SSH-портом (`2323` в production-примере);
|
||||
- `[host_firewall].amnezia_port` совпадает с published UDP port Amnezia;
|
||||
- `[amnezia].expected_host_udp_port` совпадает с published UDP port Amnezia;
|
||||
- `[amnezia].peer_ipv4_cidr` совпадает с подсетью clients;
|
||||
- `[anti_torrent].peer_ipv4_cidr` обычно равен `[amnezia].peer_ipv4_cidr`;
|
||||
- `peer_limit` и `root_rate` отражают нужную traffic policy.
|
||||
@@ -264,7 +264,7 @@ systemctl daemon-reload
|
||||
Перед первым `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` совпадает с peer-подсетью;
|
||||
- `[anti_torrent].peer_ipv4_cidr` обычно тот же CIDR.
|
||||
|
||||
@@ -286,6 +286,8 @@ Traffic actual:
|
||||
drift: no
|
||||
Host firewall:
|
||||
drift: no
|
||||
Docker/Amnezia port path:
|
||||
port_contract: ok
|
||||
Anti-torrent:
|
||||
drift: no
|
||||
Swap:
|
||||
|
||||
@@ -27,6 +27,7 @@ Expected status:
|
||||
network tuning drift: no
|
||||
traffic drift: no
|
||||
host firewall drift: no
|
||||
Docker/Amnezia port_contract: ok
|
||||
anti-torrent drift: no
|
||||
swap drift: no
|
||||
```
|
||||
@@ -46,9 +47,19 @@ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
|
||||
C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)"
|
||||
docker port "$C"
|
||||
docker exec "$C" awg show
|
||||
docker exec "$C" awg show awg0 listen-port
|
||||
docker inspect -f 'ports={{json .NetworkSettings.Ports}}' amnezia-dns
|
||||
```
|
||||
|
||||
Проверка host firewall выполняется через Docker-совместимый frontend. В `INPUT` ожидается первый jump с comment `awg2-harness:host-firewall`, но не allow-rule для `443/udp`:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Проверка AWG-контракта внутри контейнера (без раскрытия секретов):
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user