feat: add declarative sing-box egress control plane

This commit is contained in:
2026-08-27 00:58:52 +05:00
commit b8d19b2c3e
54 changed files with 3337 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
# Архитектура
## Границы ответственности
Hysteria2 URI содержит только переносимые реквизиты VPN:
- authentication;
- hostname и port/port ranges;
- SNI и `insecure`;
- `salamander` и obfs password.
`policy.json` содержит локальную инфраструктурную политику:
- `eth0` — upstream;
- `eth1` — VPN LAN;
- `tun-sb0`, `172.19.0.1/30`, MTU 1400;
- TUN routes, nftables marks и rule/table indexes;
- bootstrap DNS и DoH через `hy2-out`;
- bandwidth hints и healthcheck.
Renderer не патчит существующий JSON. Полная конфигурация каждый раз строится
из typed model. Это устраняет config drift и исторические endpoint `/32`.
## Поток применения
```text
URI + policy
├─ strict parsing / schema validation
├─ exact sing-box version and build-tag gate
├─ deterministic render
├─ secure candidate in /etc/sing-box
└─ sing-box check -c candidate
redacted comparison
unchanged ─────── changed
│ │
▼ ├─ last-good backup
no restart ├─ atomic replace
├─ systemctl restart
└─ bounded healthcheck
fail ┴ success
│ │
▼ ▼
rollback state.json
```
Все операции изменения сериализованы `flock`-совместимой блокировкой. Поэтому
ручной `import` и запоздалый event от systemd.path не могут применять два
candidate одновременно.
## Почему endpoint IP не нужен
Outbound сохраняет DNS hostname и содержит `bind_interface=eth0`. В 1.13.19
`route.auto_detect_interface` не применяется к outbound с явным
`bind_interface`. Bootstrap DNS также привязан к `eth0`. Endpoint IP поэтому не
является частью TUN policy.
После смены A-записи следующая Hysteria2-сессия разрешает имя заново через
`bootstrap-dns`. `doctor` дополнительно проверяет, что текущие A-записи не
попали в exclusions.
## Файловая модель
```text
/etc/vpn-egress/
├── policy.json root:root 0600
└── hysteria2.uri root:root 0600
/var/lib/vpn-egress/
├── state.json без секретов, 0600
├── last-good.json содержит secrets, 0600
└── backups/ ограниченная история, 0700/0600
/etc/sing-box/
└── config.json root:root 0600
```
Service sing-box уже имеет `CAP_DAC_READ_SEARCH`, поэтому сохраняется текущая
рабочая модель `root:root 0600`.
Guard читает имена интерфейсов из того же policy и устанавливает всю nftables
таблицу одной batch-транзакцией. Если новый ruleset некорректен, nft не оставляет
систему с частично заменённой таблицей.
## Версионная граница
В коде существует только `renderer_1_13_19.py`. Renderer для 1.14 не является
пустой заготовкой: он появится только в отдельной миграции после аудита схемы,
маршрутизации и полных интеграционных тестов.