fix: harden firewall and traffic reconciliation
This commit is contained in:
+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.
|
||||
|
||||
Reference in New Issue
Block a user