# Архитектура ## Границы ответственности 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 local connectivity 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/ root:root 0700 ├── policy.json root:root 0600 └── hysteria2.uri root:root 0600 /var/lib/vpn-egress/ root:root 0700 ├── 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 не оставляет систему с частично заменённой таблицей. Package-managed drop-in добавляет для `sing-box.service` зависимости `Requires=` и `After=` от guard. Поэтому при совместном запуске ошибка guard блокирует старт sing-box. Уже активный oneshot не является watchdog: ручное удаление nftables-таблицы обнаруживает `doctor`, но не systemd dependency. ## Версионная граница В коде существует только `renderer_1_13_19.py`. Renderer для 1.14 не является пустой заготовкой: он появится только в отдельной миграции после аудита схемы, маршрутизации и полных интеграционных тестов.