fix: harden production topology and runtime state

This commit is contained in:
2026-08-28 15:19:12 +05:00
parent 9dd4732655
commit 0c32c9abbe
26 changed files with 3276 additions and 2815 deletions
+28 -11
View File
@@ -127,7 +127,7 @@ awg2-harness validate-runtime
При `swap.enabled = false` управляемый файл удаляется. **Остаются на месте**: сам swapfile, его активность в ядре и запись в `/etc/fstab` — то есть после перезагрузки swap снова включится. Удаляется только persistence `vm.swappiness`. Runtime-значение `vm.swappiness` при этом намеренно не меняется: назначать произвольное значение означало бы выдумать политику, о которой оператор не просил. После перезагрузки действует kernel default. Активный swap при отключении модуля тоже не выключается — деструктивное действие не должно запускаться самим фактом отключения функции.
`net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` попадают в целевое состояние только при активном BBR.
`net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` попадают в целевое состояние только когда BBR желателен по policy. Фактическая активность диагностируется отдельно по runtime `net.ipv4.tcp_congestion_control`.
## 3. Структура модулей
@@ -152,7 +152,15 @@ traffic_core/src/
│ ├── host.rs owned iptables/ip6tables INPUT chains (host namespace)
│ └── antitorrent.rs таблица inet awg2_harness (namespace по конфигу)
├── sysctl_tuning.rs управляемый sysctl baseline
├── swap.rs управляемый swap
├── swap/
│ ├── mod.rs facade и orchestration ensure_swap
│ ├── model.rs модели desired/runtime/status и replacement state
│ ├── path.rs syntax/path-chain safety и state directory
│ ├── ownership.rs ownership record и legacy sidecar
│ ├── runtime.rs swapon/swapoff/mkswap и parser runtime state
│ ├── replacement.rs создание, resize и rollback транзакции
│ ├── persistence.rs /etc/fstab, swappiness и legacy migration
│ └── status.rs inspect_swap и drift aggregation
├── status.rs сборка и печать статуса
├── lock.rs файловые блокировки
└── error.rs типы ошибок
@@ -273,11 +281,13 @@ struct PeerTrafficStatus {
awg show all allowed-ips
```
Правила разбора:
Правила разбора и общий `PeerTopology` contract:
- строки разбираются структурно на public key peer и его allowed IPs;
- в этой команде allowed ips одного peer разделены **пробелом**, в `show all dump` — запятой, в обычном `show` — `", "`; нужно потреблять все токены начиная с третьей колонки, иначе peers с несколькими allowed IP получат shaping только по первому адресу;
- оставляются только IPv4 `/32` внутри `amnezia.peer_ipv4_cidr`;
- в этой команде allowed ips разделены **пробелом**, в `show all dump` — запятой, в обычном `show` — `", "`; parser обязан проверить весь список, а не молча взять первый адрес;
- поддерживается ровно один IPv4 `/32` внутри `amnezia.peer_ipv4_cidr` на peer; `(none)` допустим и не создаёт peer class;
- IPv6-only, IPv4+IPv6, несколько allowed IPs, маска не `/32`, невалидный CIDR или IPv4 вне configured peer CIDR возвращают typed `UnsupportedTopology`;
- topology проверяется один раз на startup/periodic/event cycle и передаётся traffic и anti-torrent. При ошибке ни одна из этих подсистем не начинает mutation и не сообщает успешный reconcile;
- offline peers включаются: online-состояние не является требованием для shaping;
- перед присвоением class id строки peer IP сортируются лексикографически, чтобы сохранить действующий контракт class-id.
@@ -287,7 +297,7 @@ awg show all allowed-ips
awg show
```
Fallback-парсер разбирает текстовый вывод, извлекая строки `allowed ips:`. Он используется, если основная команда завершилась ошибкой или вернула пустой результат.
Fallback-парсер разбирает текстовый вывод, извлекая строки `allowed ips:`. Он используется, если основная команда завершилась ошибкой или вернула пустой результат, и применяет тот же строгий topology contract.
Классификация строк `show all dump`:
@@ -365,7 +375,7 @@ apply_firewall():
apply_antitorrent_firewall(config) # namespace по anti_torrent.apply_in_amnezia_netns
```
Каждый из firewall-путей сначала инспектирует фактическое состояние и выходит без изменений, если drift отсутствует. Anti-torrent таблица заменяется одной атомарной nft-транзакцией. Host firewall сначала строит и проверяет inactive chain отдельно для IPv4/IPv6, затем переключает помеченный jump; сбой IPv6 после IPv4 switch откатывает IPv4. После apply выполняется обязательная повторная инспекция.
Каждый из firewall-путей сначала инспектирует фактическое состояние и выходит без изменений, если drift отсутствует. Anti-torrent таблица заменяется одной атомарной nft-транзакцией и при включённом guard требует успешно проверенный `PeerTopology`; host firewall от server-side peer topology не зависит. Host firewall сначала строит и проверяет inactive chain отдельно для IPv4/IPv6. Новый jump всегда вставляется через `-I INPUT 1`; старый остаётся вторым до успешного переключения обеих IP-family и затем удаляется по полной rule specification. Numeric `-R/-D` для managed jumps не используется. Сбой IPv6 удаляет новый IPv4 jump по точной specification, поэтому старый IPv4 jump всё время остаётся рабочим. После apply выполняется обязательная повторная инспекция.
Для production контролируемая полная пересборка при drift — единственный поддерживаемый путь.
@@ -407,6 +417,8 @@ apply_firewall():
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.
Жизненный цикл managed jump использует только exact rule spec с comment `awg2-harness:host-firewall` и target `AWG2H_A`/`AWG2H_B`. Чужое правило, вставленное другим firewall manager между inspect и switch, не может быть заменено или удалено по изменившемуся номеру строки.
## 11. Anti-torrent firewall guard
Это часть production-контракта AWG2. Модуль вписывается в мульти-оркестраторную модель, потому что сервер выделенный и бинарю разрешено владеть host/network правилами, нужными VPN-продукту. Правило всё равно обязано быть узко ограниченным и наблюдаемым.
@@ -542,7 +554,8 @@ System:
interface: <iface>
Network tuning:
enabled, apply_on_daemon_start, tcp_bbr_mode, conf_path, modules_load_path,
значения управляемых ключей, bbr_available, bbr_active, drift[, reason][, warning]
значения управляемых ключей, bbr_available, bbr_desired,
bbr_runtime_active, drift[, reason][, warning]
Peers:
total: N
- <peer ip>
@@ -560,6 +573,7 @@ Host firewall:
enabled, ssh_port, input_policy, drift[, reason]
Docker/Amnezia port path:
expected_host_udp_port, docker_userland_proxy, published_udp_port,
host_bindings (все HostIp:HostPort, включая routed binding без HostPort),
awg_listen_udp_port, port_contract, host_forward_policy_drop,
docker_forward_rules_present, note[, warning]
Anti-torrent:
@@ -693,7 +707,7 @@ inode = 123456
### Перезапись `/etc/fstab`
Это самая разрушительная операция пакета, поэтому она транзакционная и вынесена в отдельный модуль `fstab.rs`. Контракт:
Это самая разрушительная операция пакета, поэтому она транзакционная и изолирована в `swap/persistence.rs`. Контракт:
- ошибка чтения `/etc/fstab` **никогда** не интерпретируется как пустой файл. `PermissionDenied`, ошибка ввода-вывода и отсутствие файла различаются, и все три прекращают операцию. Отсутствие `/etc/fstab` на Debian-хосте считается аномалией, а не основанием создать его с нуля;
- пустой или состоящий из пробелов кандидат отклоняется;
@@ -714,8 +728,8 @@ inode = 123456
- валидация конфига, включая форму `amnezia.wg_binaries`;
- разбор `config.example.toml` в актуальную схему с точным default-порядком контейнеров, `awg`/`wg` и anti-torrent baseline `500`;
- парсер `awg|wg show all allowed-ips`, включая несколько allowed ips через пробел;
- fallback-парсер `awg|wg show`;
- парсер `awg|wg show all allowed-ips`: один IPv4 `/32` проходит, IPv4+IPv6, IPv6-only, несколько маршрутов, маска не `/32` и адрес вне configured CIDR дают explicit `UnsupportedTopology`;
- fallback-парсер `awg|wg show` и dump-парсер применяют тот же topology contract; `(none)` остаётся допустимым;
- классификация peer/interface строк `awg|wg show all dump` против двух 30-полевых AWG 3.1 fixtures, AWG 3.0 с включённой и выключенной защитой заголовка, AWG 2.x, чистого WireGuard, а также конфигурации AWG 2.0, прочитанной tools AWG 3.0;
- разбор dump с несколькими интерфейсами, с интерфейсом без peers и с повреждёнными строками;
- порядок выбора бинаря wg и ошибка при отсутствии всех кандидатов;
@@ -727,6 +741,9 @@ inode = 123456
- packet statistics из Debian-подобного `tc -s class`;
- отказ удалять foreign IFB/root qdisc;
- typed Docker readiness retry/timeout/lower-priority event;
- Docker UDP bindings: dual-stack HostIp, routed binding с пустым HostPort и неоднозначные/divergent host ports;
- BBR decision: доступный BBR при runtime `cubic` означает `desired=yes`, `runtime_active=no`; mode `off` не маскирует уже активный runtime BBR;
- host firewall rollback удаляет новый jump по exact specification и не затрагивает чужое правило в начале `INPUT`;
- conntrack count из Amnezia namespace и global max с host;
- diff drift;
- расчёт окна перезагрузки;