platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

1166
docs/EXECUTION_TRACKER.md Normal file

File diff suppressed because it is too large Load Diff

54
docs/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Selective VPN Dashboard — Документация
## О проекте
- Go-ядро (`selective-vpn-api/`) управляет nftables, policy routing, DNS, SmartDNS, VPN и trace. Всё взаимодействие идет через локальный REST/SSE сервер (`127.0.0.1:8080`).
- GUI (`selective-vpn-gui/`) использует `api_client.py` и `dashboard_controller.py`, не дублируя бизнес-логику.
- Задача текущего этапа — проверить ядро, задокументировать API и подготовить почву для будущего веб-прототипа.
- Архитектурный ориентир: один API-контракт для всех клиентов (`web + iOS + Android`) без развилки бизнес-логики в ядре.
- Принятое направление web prototype: `Vite + React + TypeScript` (SPA); `Next.js` не используется на MVP-этапе.
- Фокус текущего desktop-этапа: `SingBox` вкладка + `SingBox` Go API (`profiles/validate/render/apply`); `DNSTT/Phoenix` остаются backend-ready треком без UI-расширения на этом шаге.
## Структура фаз
- Phase A — аудит API, реестр маршрутов и подтверждение, что все операции раскрыты в Go.
- Phase B — проверка внешних зависимостей (nftables, systemd, cgroup, SmartDNS) и фиксация требований к привилегиям/состоянию.
- Phase C — анализ готовности к веб-доступу (binding, SSE, авторизация, CORS, long-running команды).
- Phase D — документирование результатов (матрицы, чеклистов, критериев готовности) и запуск multi-client подхода.
- Phase E — дизайн multi-client PBR: удобное управление несколькими transport-клиентами, изоляция маршрутов и anti-conflict защита.
- Integration Track — подключение внешних transport-клиентов (`sing-box`, `dnstt-client`, `phoenix->slipstream`) через единый API-контракт ядра.
## Быстрый доступ к артефактам
- `docs/phase-a/A1_API_AUDIT.md` — цели и критерии фазы A.
- `docs/phase-b/B1_CORE_VERIFICATION.md` — набор задач по ядру и зависимостям.
- `docs/phase-b/B2_RESOLVER_DIFF_AND_IMPROVEMENT_PLAN.md` — различия `system resolver` vs `sing-box DNS` и roadmap улучшений нашего resolver.
- `docs/phase-b/B4_RUNTIME_DEPENDENCIES_AND_PREFLIGHT.md` — разделение `go.mod` зависимостей и runtime/bin/service зависимостей + preflight-check скрипт.
- `docs/phase-b/B5_SINGBOX_TEMPLATE_MIGRATION_ROLLBACK_RUNBOOK.md` — runbook миграции `legacy singbox-*.service` -> `singbox@<id>.service` и аварийного rollback-плана.
- `docs/phase-c/C1_WEB_READINESS.md` — возможности и ограничения REST/SSE доступа.
- `docs/phase-c/C2_WEB_STACK_DECISION.md` — зафиксированное решение по веб-стеку (`Vite + React + TypeScript`) и условия для возможного перехода на `Next.js`.
- `docs/phase-d/D1_GO_READINESS_DOCS.md` — матрицы, чеклисты и тревоги перед веб-прототипом.
- `docs/phase-d/D4_PLATFORM_COMPATIBILITY_MATRIX.md` — совместимость transport-контракта для `web + iOS + Android` и платформенные ограничения runtime.
- `docs/phase-d/D5_NETNS_RUNTIME_CASE.md` — готовый netns-case для `SingBox` (runtime-check, exec-mode, refactor map GUI/API).
- `docs/phase-e/E1_MULTI_CLIENT_PBR_DESIGN.md` — целевая архитектура multi-client маршрутизации, mark/table allocator, conflict-guardrails и UX-поток.
- `docs/phase-e/E2_TRANSPORT_API_CONTRACT.md` — контракт `/api/v1/transport/*` с DTO, примерами запросов/ответов и workflow `validate -> apply`.
- `docs/phase-e/E4_VALIDATE_CONFIRM_APPLY_UX.md` — UX-сценарии предупреждений и подтверждения для конфликтного apply.
- `docs/phase-e/E5_SINGBOX_PROTOCOLS_REQUIREMENTS.md` — требования к вкладке протоколов `SingBox` и target Go API для `singbox profiles`.
- `docs/phase-e/E5_2_SINGBOX_DESKTOP_DASHBOARD_SPEC.md` — зафиксированный desktop-дизайн вкладки `SingBox`: runtime card + profile settings + global defaults.
- `docs/phase-f/F1_REFACTOR_MODULARITY_PLAN.md` — план декомпозиции крупных файлов GUI/API/Go без изменения поведения.
- `docs/EXECUTION_TRACKER.md` — статус фаз и текущие шаги.
- `tests/` — smoke-скрипты для API (sanity, SSE, VPN login, trace append) + общий запуск через `tests/run_all.sh`.
- `selective-vpn-api/cmd/` — явные Go entrypoints (`selective-vpn-api`, `selective-vpn-routes-update`, `selective-vpn-routes-clear`, `selective-vpn-autoloop`), legacy root `main.go` сохранён для совместимости.
- `selective-vpn-api/app/cli/` и `selective-vpn-api/app/bootstrap/` — вынесенные runtime-раннеры (CLI и HTTP bootstrap) при сохранении фасадов `Run*` в `app`.
- `selective-vpn-api/app/transporttoken/` — вынесенный confirm-token store для transport policy apply/force-override lifecycle.
- `selective-vpn-gui/api/` — пакетизированный API-клиент GUI (base `client.py` + domain mixin-модули `status/routes/traffic/dns/domains/vpn/trace/transport_*`) с сохранением legacy-фасада `api_client.py`.
- `selective-vpn-gui/controllers/` — пакетизированный слой `DashboardController` (domain mixin-модули + `views.py`), при этом `dashboard_controller.py` сохранён как facade для совместимости импорта в UI.
- `selective-vpn-gui/main_window/` — модульные части GUI-окна (`constants`, `workers`, `ui_shell_mixin`, `ui_tabs_*`, `ui_tabs_singbox_{layout,editor}`, `runtime_actions_mixin`, `runtime_{state,refresh,auth,ops}`) и подпакет `main_window/singbox/*` (`editor/cards/links/runtime` + split `links_*`/`runtime_*`) при сохранении `vpn_dashboard_qt.py` как thin-bootstrap/wiring слоя.
- `scripts/transport_runbook.py` — операционный helper для lifecycle transport-клиентов через API (`create/provision/start/health/metrics/restart/stop/delete`).
- `scripts/transport_recovery_runbook.py` — runbook восстановления transport-клиента (`health -> restart -> provision/start fallback -> diagnostics`).
- `scripts/check_runtime_dependencies.sh` — preflight-check runtime зависимостей среды (`required/optional`, `--strict` режим).
- `scripts/transport-packaging/` — manual+pinned updater (`update.sh`), opt-in scheduler (`auto_update.sh`) и rollback (`rollback.sh`) для companion-бинарей (`runtime_mode=exec`), включая `manifest.production.json` и `source_policy.production.json`.
- `selective-vpn-web/` — web foundation (`Vite + React + TypeScript`) для будущего control-plane UI.
## Как использовать план
1. Следите за статусом фаз в `docs/EXECUTION_TRACKER.md`.
2. Обновляйте соответствующие `phase-*` документы результатами проверок (замены `[~]` на `[x]`).
3. После окончания фаз AD приступить к прототипу веб-интерфейса, опираясь на собранную матрицу endpoint → handler.
4. Интеграционный трек выполнять параллельно с завершением ядра: новые клиенты подключаются через backend-адаптеры, UI остаётся тонким слоем вызовов API.

View File

@@ -0,0 +1,144 @@
# A1 Аудит API и HTTP-маршрутов
Дата: 2026-02-27
Статус: draft
Владелец: Engineering
## 1) Цель
- Подтвердить, что все пользовательские сценарии (статусы, смена режимов, управление маршрутами, DNS, VPN, SmartDNS, trace) реализованы в Go-ядре и доступны через единую REST-SSE оболочку.
- Зафиксировать текущую структуру `/api/v1/*`, SSE `/api/v1/events/stream` и CLI-режимов (`routes-update`, `routes-clear`, `autoloop`) как основу для будущего веб-интерфейса.
## 1.1) Факт по коду (точный baseline)
- Источник: `selective-vpn-api/app/server.go`.
- Зарегистрировано `61` `mux.HandleFunc(...)`.
- Из них:
- `60` endpoint под `/api/v1/*`.
- `1` сервисный endpoint `/healthz`.
- CLI режимы в `Run()`: `routes-update`, `routes-clear`, `autoloop`.
- Изменение от 2026-03-07: в transport добавлены `8` base route (`/api/v1/transport/*`) и action-subpath `GET /api/v1/transport/clients/{id}/metrics`.
## 2) Критерии завершения
- Таблица endpoint → handler-файл / CLI mode составлена и вынесена в документ (см. Phase A).
- Подтверждено, что GUI не содержит альтернативных реализаций бизнес-логики (всё идет через Go API).
- Описаны ответы (CmdResult, dataclasses) и форматы, которые нужно сохранять для веба.
## 3) Задачи
- Выписать все маршруты из `selective-vpn-api/app/server.go` и сопоставить их с файлами-обработчиками (`routes_handlers.go`, `traffic_mode.go`, `dns_settings.go`, `vpn_handlers.go`, `smartdns_runtime.go`, `trace_handlers.go`, `traffic_appmarks.go` и др.).
- Отметить CLI-флаги и специальные пути (`/routes/update`, `/routes/timer`, `/routes/rollback`, trace, SmartDNS, VPN login) и их привязку к Go-механикам.
- Проверить `selective-vpn-gui/api_client.py` на факт наличия всех URL/методов/форматов и зафиксировать зависимости.
- Подготовить краткое описание (для фазы A) о том, какие маршруты уже готовы к прямому вызову чере веб (без правок).
## 4) Состояние (черновик таблицы)
| Endpoint | HTTP | Handler file | Краткая логика | Требования |
| --- | --- | --- | --- | --- |
| `/api/v1/status`, `/api/v1/routes/status` | GET | `routes_handlers.go` | Чтение `status.json`, вычисление policy-route через nftables | Файл `status.json`, доступ к nftables |
| `/api/v1/routes/service[*]` | POST | `routes_handlers.go` | `systemctl start|stop|restart` service unit | systemd unit name, root или capability |
| `/api/v1/routes/update` | POST | `routes_handlers.go``routes_update.go` | Сообщает о ручном апдейте через Go-реализацию вместо bash → обновляет set и policy | NFT/state dirs, `stateDir`, `preferredIface` |
| `/api/v1/routes/timer`, `/routes/timer/toggle` | GET/POST | `routes_handlers.go` | Чтение/запись enable flag для таймера, использует `runRoutesTimerSet` | Файл таймера, systemd |
| `/api/v1/traffic/mode`, `/traffic/mode/test` | GET/POST | `traffic_mode.go` | Переключает режимы Selective/Full/Direct, включает auto bypass, проверка `TrafficModeState` | Состояние в `stateDir`, доступ к nftables/systemd |
| `/api/v1/dns-upstreams`, `/api/v1/dns/mode`, `/api/v1/dns/benchmark` | GET/POST | `dns_settings.go` | Управление DNS upstreams, режимом и benchmark (проверка резольвера) | `resolv.conf`, `dnsmaestro`, `SmartDNS` |
| `/api/v1/vpn/status`, `/api/v1/vpn/autoloop*` | GET/POST | `vpn_handlers.go` | Статусы VPN, управление autoloop/autoconnect (через AdGuard VPN API) | Доступ к AdGuard VPN API, PTY |
| `/api/v1/events/stream` | GET SSE | `events_handlers.go` / `events_bus.go` | SSE-поток статусов и логов для GUI | Доступ к event bus, `ctx` |
| `/api/v1/trace`, `/api/v1/trace-json`, `/api/v1/trace/append` | GET/POST | `trace_handlers.go` | tail/JSON/append лог-файла `trace.log` | Доступ к `trace.log`, root |
| `/api/v1/traffic/advanced/reset` | POST | `traffic_mode.go` | Сброс расширенных bypass-опций (auto-local, ingress) | Доступ к traffic state |
| `/api/v1/traffic/interfaces` | GET | `traffic_mode.go` | Список интерфейсов и предпочитаемый iface | Просмотр `iproute2` interfaces |
| `/api/v1/traffic/candidates` | GET | `traffic_candidates.go` | Выборка потенциальных подсетей для bypass | `nft`/routing discovery |
| `/api/v1/traffic/appmarks` | POST | `traffic_appmarks.go` | Управление cgroup mark (vpn/direct) | `cgroup v2`, systemd scopes |
| `/api/v1/traffic/appmarks/items` | GET | `traffic_appmarks.go` | Список runtime marks для UI | `cgroup v2` info |
| `/api/v1/traffic/app-profiles` | GET/POST/DELETE | `traffic_app_profiles.go` | CRUD профилей запуска приложений | Хранение в `stateDir` |
| `/api/v1/traffic/audit` | GET | `traffic_audit.go` | Проверки консистентности nft/route | Доступ к nft\logs |
| `/api/v1/vpn/autoloop-status` | GET | `vpn_handlers.go` | Статус autoloop watcher | AdGuard VPN API |
| `/api/v1/vpn/status` | GET | `vpn_handlers.go` | Общий статус VPN/traffic | AdGuard VPN API |
| `/api/v1/vpn/autoconnect` | POST | `vpn_handlers.go` | Переключение autoconnect | AdGuard VPN API |
| `/api/v1/vpn/locations` | GET | `vpn_handlers.go` | Список стран/регионов | AdGuard VPN API (locations) |
| `/api/v1/vpn/location` | POST | `vpn_handlers.go` | Установка нового location | AdGuard VPN API |
| `/api/v1/vpn/login/session/*` | POST/GET | `vpn_login_session.go` | Установка интерактивной PTY сессии, проверки, действия | PTY, systemd user session |
| `/api/v1/vpn/logout` | POST | `vpn_handlers.go` | Выход VPN | AdGuard VPN API |
| `/api/v1/dns/upstream-pool` | GET/POST | `dns_settings.go` | pool upstreams (итоги) | `dns_upstreams` state |
| `/api/v1/dns/status` | GET | `dns_settings.go` | Текущий upstreams и режим | `resolver` состояние |
| `/api/v1/dns/smartdns-service` | POST | `dns_settings.go` | Старт/стоп SmartDNS сервис | `smartdns` бинарь, systemd unit |
| `/api/v1/smartdns/service` | POST | `smartdns_runtime.go` | Управление SmartDNS runtime | systemd + config |
| `/api/v1/smartdns/runtime` | GET | `smartdns_runtime.go` | Получение runtime stats | СмартДНС статус |
| `/api/v1/smartdns/prewarm` | POST | `smartdns_runtime.go` | Прогрев wildcard-базы | smartdns runtime |
| `/api/v1/domains/table` | GET/POST | `domains_handlers.go` | Загрузка/сохранение таблицы доменов | Файл доменов |
| `/api/v1/domains/file` | GET | `domains_handlers.go` | Скачать raw файл доменов | Доступ к `domains.json` |
| `/api/v1/smartdns/wildcards` | GET/POST | `smartdns_wildcards_store.go` | CRUD wildcard-списка | `wildcards.json` |
Это рабочая матрица для ключевых сценариев. Полный реестр endpointов из `server.go` приведён ниже.
## 5) GUI → API
- `selective-vpn-gui/api_client.py` инкапсулирует все URL/методы/JSON-форматы (например, `TrafficModeStatus`, `TrafficAppMarkItem`, `CmdResult`), поэтому при замене фронта достаточно реализовать аналогичные dataclasses в новой веб-части.
- `dashboard_controller.py` использует эти модели, подписывается на SSE и отображает текущий статус/trace. Нужно подтвердить, что в GUI нет logic-branch (например, расчёт маршрутов), которая должна перейти в веб отдельно.
- Задача анализа: пробежать по `api_client.py` и `dashboard_controller.py` и отметить все вызовы; в Phase D перенести их в колонку “UI usage” для проверки совместимости.
## 6) CLI-режимы и API parity
- `routes-update` / `selective-vpn-api routes-update` — запускает ту же логику, что и `POST /api/v1/routes/update`, но без HTTP-запросов; используется для cron/автообновления.
- `routes-clear` / `/api/v1/routes/rollback` — очищает nftables/statestatus; веб должен предоставить rollback-кнопку, чтобы совпадал функционал CLI.
- `autoloop` — опрашивает AdGuard VPN API и пишет `autoloop.log`; веб потребляет статус через `/api/v1/vpn/autoloop-status` и SSE события watchers.
## 7) Полный реестр endpoint (из `server.go`)
- `/healthz`
- `/api/v1/events/stream`
- `/api/v1/status`
- `/api/v1/routes/status`
- `/api/v1/vpn/login-state`
- `/api/v1/systemd/state`
- `/api/v1/routes/service/start`
- `/api/v1/routes/service/stop`
- `/api/v1/routes/service/restart`
- `/api/v1/routes/service`
- `/api/v1/routes/update`
- `/api/v1/routes/timer`
- `/api/v1/routes/timer/toggle`
- `/api/v1/routes/rollback`
- `/api/v1/routes/clear`
- `/api/v1/routes/cache/restore`
- `/api/v1/routes/precheck/debug`
- `/api/v1/routes/fix-policy-route`
- `/api/v1/routes/fix-policy`
- `/api/v1/traffic/mode`
- `/api/v1/traffic/mode/test`
- `/api/v1/traffic/advanced/reset`
- `/api/v1/traffic/interfaces`
- `/api/v1/traffic/candidates`
- `/api/v1/traffic/appmarks`
- `/api/v1/traffic/appmarks/items`
- `/api/v1/traffic/app-profiles`
- `/api/v1/traffic/audit`
- `/api/v1/trace`
- `/api/v1/trace-json`
- `/api/v1/trace/append`
- `/api/v1/dns-upstreams`
- `/api/v1/dns/upstream-pool`
- `/api/v1/dns/status`
- `/api/v1/dns/mode`
- `/api/v1/dns/benchmark`
- `/api/v1/dns/smartdns-service`
- `/api/v1/smartdns/service`
- `/api/v1/smartdns/runtime`
- `/api/v1/smartdns/prewarm`
- `/api/v1/domains/table`
- `/api/v1/domains/file`
- `/api/v1/smartdns/wildcards`
- `/api/v1/vpn/autoloop-status`
- `/api/v1/vpn/status`
- `/api/v1/vpn/autoconnect`
- `/api/v1/vpn/locations`
- `/api/v1/vpn/location`
- `/api/v1/vpn/login/session/start`
- `/api/v1/vpn/login/session/state`
- `/api/v1/vpn/login/session/action`
- `/api/v1/vpn/login/session/stop`
- `/api/v1/vpn/logout`
- `/api/v1/transport/clients`
- `/api/v1/transport/clients/{id}`
- `/api/v1/transport/clients/{id}/start`
- `/api/v1/transport/clients/{id}/stop`
- `/api/v1/transport/clients/{id}/restart`
- `/api/v1/transport/clients/{id}/health`
- `/api/v1/transport/policies`
- `/api/v1/transport/policies/validate`
- `/api/v1/transport/policies/apply`
- `/api/v1/transport/policies/rollback`
- `/api/v1/transport/conflicts`
- `/api/v1/transport/capabilities`

View File

@@ -0,0 +1,100 @@
# B1 Проверка ядра и внешних зависимостей
Дата: 2026-02-27
Статус: draft
Владелец: Engineering
## 1) Цель
- Убедиться, что Go-ядро имеет централизованную логику для работы с nftables, policy routing, systemd, cgroup и SmartDNS, и что эти зависимости отражены в конфигурациях/документации.
- Выявить потенциальные отверстия (например, части логики, которые напрямую обращаются к shell-скриптам `routes_update`, `autoloop`, `trace`) перед тем, как стартовать задачи для веба.
## 2) Критерии завершения
- Описание работы ключевых модулей ({`routes_update.go`, `traffic_mode.go`, `events_bus.go`, `watchers.go`, `resolver.go`, `smartdns_runtime.go`, `vpn_handlers.go`}) подготовлено, включая внешние зависимости (nftables, SmartDNS, systemd, AdGuard VPN API).
- Сформулирован список требований (root, обязательные сервисы, долгие операции, возможные блокировки) на случай запуска через веб-консоль.
- Уточнено, какие данные уже сохраняются в `stateDir`, `appmark` и `routes` файлах, чтобы веб мог отображать информацию без дополнительного API.
## 3) Задачи
- Пройти `routes_update.go`, `routes_units.go`, `routes_cache.go`, `nft_update.go` и описать, как обновляются маршруты и кэшируются состояния.
- Проанализировать `traffic_mode.go`, `traffic_appmarks.go`, `traffic_app_profiles.go`, `traffic_audit.go`: что именно меняют (fwmark, cgroup, nft sets) и какие проверки выполняются.
- Прописать взаимодействие с DNS/SmartDNS (`dns_settings.go`, `smartdns_runtime.go`, `smartdns_wildcards_store.go`, `resolver.go`), включая init/restore/caching жизненный цикл.
- Проверить `watchers.go`, `events_bus.go`, `trace_handlers.go`: какие события стримятся, какие веб-интерфейсы они смогут потреблять, и нужны ли дополнительные фильтры или буферизация.
- Отметить, какие части кода требуют root-доступ и как это влияет на веб (например, API будет работать под сервис-аккаунтом и выполнять nft/update через internal API, но сама служба должна запускаться с нужными привилегиями).
- Собрать стартовый список фактов по каждому модулю: с какими `systemd` unit'ами, `nft` set'ами, SmartDNS/AdGuard VPN API и `stateDir` работает, и какие ограничения это накладывает.
- Задокументировать потенциальные точки отказа (недоступность nftables/systemd, SmartDNS/AdGuard API) и предложить стратегию контроля ошибок для веб-интерфейса.
## 4) Модули и зависимости
### `routes_update.go`
- Основной контроллер обновления маршрутов: строит policy routes, nftables-цепочки `agvpn`/`agvpn4`/`agvpn_dyn4`, запускает `runResolverJob`, пишет список доменов/IP в `stateDir` и сохраняет `status.json`.
- Поддерживает прогресс через `events.push("routes_nft_progress", ...)`, пишет heartbeat-файлы и требует доступа к `iproute2`, `nft`, `stateDir`, `trace.log` и воркеру резольвера.
- Обрабатывает `force`/`auto` конфигурации из `TrafficModeState` и фиксирует `trafficEval` в статусе, что важно для отображения в вебе.
### `routes_units.go`, `routes_cache.go`, `nft_update.go`
- `routes_units.go` разрешает `systemd` имя (`routesServiceUnitName`, `routesTimerUnitName`) и используется в `/routes/service`/`timer`.
- `routes_cache.go` кеширует `domains`, `ips`, `domains` state; можно использовать для расчёта прогресса и восстановления.
- `nft_update.go` держит “умный” апдейтер, который управляет правилами и sets; важно документировать, какие команды выполняются и как проверяется успех (`runNFTUpdate`, `progressCb`) для веба.
### `traffic_mode.go` + `traffic_appmarks*`
- Управляют состоянием (`stateDir/state-traffic-mode.json`), применяют флаги fwmark и policy rules (`applyTrafficMode`), читают/пишут список принудительных субнетов/UID/cgroup.
- `traffic_appmarks.go` работает с cgroup v2, создаёт маркеры `MARK_APP`/`MARK_DIRECT` и задаёт TTL (через systemd scopes) для runtime управления per-app traffic.
- `traffic_app_profiles.go` хранит профили в `stateDir` и предоставляет CRUD, что пригодится вебу для создания shortcut-профилей.
- `traffic_audit.go` проверяет состояние nft/route и формирует `TrafficAudit` issues, полезные для мониторинга.
### `dns_settings.go` + `resolver.go`
- Хранит конфигурацию `dns-upstreams.conf`, режим (`dns_mode.json`) и pool upstreams; отвечает за benchmark, SmartDNS control (`smartdns` service start/stop) и режим `ViaSmartDNS`.
- `resolver.go` запускает Go-резольвер, читает `domains.txt`, `meta-special.txt`, `static-ips.txt`, использует кеши (`domain-cache.json`, `ptr-cache.json`) и пишет результат в `stateDir`.
- Сервисы SmartDNS (`smartdns_runtime.go`, `smartdns_wildcards_store.go`) управляют дополнительными wildcard-базами, runtime stats и prewarm, что потребует отображения статуса SmartDNS в веб.
### `vpn_handlers.go` + `vpn_login_session.go`
- Интеграция с AdGuard VPN API: `autoloop`, `autoconnect`, `locations`, `set location`, `logout` и status založený na HTTP-запросах к локальному `adguardvpn` сервису.
- `vpn_login_session.go` создаёт PTY-сессию (через `runCommand` + `systemd-run --user`?), сохраняет состояния в `loginStatePath` и выпускает события (`events.push`) для SSE; вебу потребуется поддерживать эти пользователи.
### `events_bus.go`, `watchers.go`, `trace_handlers.go`
- `startWatchers` запускает наблюдение за `status.json`, `loginState`, autoloop логом, `trace.log`, state traffic appmarks TTL и systemd unitами (routes service/timer, VPN unit, SmartDNS).
- Все watcher-изменения отправляются в `events` и поступают клиенту через `handleEventsStream`, что даёт вебу источник realtime данных.
- `trace_handlers.go` читает `trace.log`, `trace-json` и принимает append, что позволяет вебу показывать live trace и записывать дополнительные строки.
### `routes_handlers.go`
- Управление systemd-unit'ами (`routes_service`, `routes_timer`), ручной rollback/clear, fixing policy route, переключение режимов/advanced config.
- Служит фасадом для CLI (как `routes-update`, `routes-clear`, `autoloop`), следовательно API может использоваться как `POST /api/v1/routes/service` и `POST /api/v1/routes/update`.
### Корневые ограничения и привилегии
- `routes_update`/`routes_handlers` запускаются как root (nft, ip, systemctl); веб-интерфейс должен вызывать API, а не повторять команды, сохраняя сервис privileged.
- SmartDNS требует запуска `smartdns-local.service`; VPN команды ждут доступ к AdGuard VPN (возможно, `adguardvpn.service`).
- `stateDir` и `domains` файлы должны быть доступны API, а вебу важно понять, какие endpoints кешируют/читают эти файлы. Например, `routes timer enable` хранится в `routes_timer_state.json`.
- Потенциальные точки отказа:
- `nft` команды падут, если kernel не поддерживает nftables или service не запущен — API должен возвращать `CmdResult` с `stderr` и `exitCode`.
- `systemd` unit может быть недоступна (не установлен `routes-service`), мониторинг через watchers должен отправлять `unit_state_changed`.
- SmartDNS runtime может не стартовать: нужно отразить ошибку через `events.push("smartdns_error"... )` и вернуть `CmdResult.ok=false`.
- AdGuard VPN API или PTY могут быть недоступны; API должен возвращать ошибки и веб должен приступать к повторной попытке.
## 5) Матрица зависимостей
| Модуль | http/cli | Зависимости | Состояния/файлы | Потенциальные ошибки |
| --- | --- | --- | --- | --- |
| `events_bus.go`, `watchers.go`, `events_handlers.go` | `/api/v1/events/stream` | in-memory queue, watcher goroutines | `status.json`, `login_state.json`, `trace.log`, systemd unit polling, `autoloop.log` | SSE disconnected, poll errors, buffer overflow |
| `routes_update.go` | `POST /api/v1/routes/update`, CLI `routes-update` | `iproute2`, `nft`, `runResolverJob`, `SmartDNS` | `stateDir/{domains,ips}*`, `status.json`, `trace.log`, `heartbeat` | Нет VPN-интерфейса, резольвер падает, `nft` пишет ошибки |
| `routes_units.go` | `/api/v1/routes/service`, `/routes/timer`, CLI `routes-clear` | `systemd` (service/timer names) | `routes_timer_state.json`, unit name env | Unit не установлена, systemctl возвращает error |
| `routes_cache.go`, `nft_update.go` | внутренняя логика routes update | `nft`, temp files | `/var/run/selective-vpn` temp files | `nft` прогресс в stderr, temp-файлы не создаются |
| `traffic_mode.go` + `traffic_appmarks*` | `/api/v1/traffic/*` | `nft`, policy routing, `cgroup v2`, `systemd` | `stateDir/traffic-mode.json`, `appmarks.json`, cgroup scopes | Некорректная конфигурация, `nft` не применяет правила |
| `traffic_app_profiles.go` | `/api/v1/traffic/app-profiles` | файловые операции | `stateDir/app-profiles.json` | Файл повреждён, не сохраняется |
| `dns_settings.go` + `resolver.go` | `/api/v1/dns-*` | `resolv.conf`, SmartDNS binary | `dns-upstreams.conf`, `dns_mode.json`, `domain-cache.json`, `ptr-cache.json` | Нет upstream, benchmark таймаут |
| `smartdns_runtime.go`, `smartdns_wildcards_store.go` | `/api/v1/smartdns/*` | `smartdns-local.service`, wildcard files | `smartdns.conf`, `wildcards.json` | Service fail, wildcard file read-only |
| `vpn_handlers.go`, `vpn_login_session.go` | `/api/v1/vpn/*` | AdGuard VPN API, PTY, systemd-user | `login_state.json`, `autoloop.log` | API недоступен, PTY не стартует |
| `trace_handlers.go` | `/api/v1/trace*` | доступ к `trace.log` | `trace.log` | Файл недоступен, append write error |
Эта матрица показывает, какие компоненты требуют привилегий и как ошибка отражается на UI. В Phase D можно перенести эти строки в таблицу `web-ready` и отметить `status` (готово/требует/blocked).
### Технические state-файлы и кеши
- `stateDir` (по умолчанию `/var/lib/selective-vpn`) содержит:
- `domains.txt`, `ips-*.txt`, `ipmap-*.txt` — сериализация выходных данных resolver, используются trace/expand logic.
- `state-traffic-mode.json`, `appmarks.json`, `app-profiles.json` — основные состояния traffic mode/appmarks/profiles, читаются `traffic_mode.go`, `traffic_appmarks.go`, `traffic_app_profiles.go`.
- `domain-cache.json`, `ptr-cache.json` — кеш resolver, обновляется из `runResolverJob` и влияет на `routes_update` (limit/ttl).
- `status.json`, `heartbeat` — пишутся `routes_update`. `watchStatusFile` читает file, `events.push("status_changed")` доставляет web info (`iface`, `table`, `healthy`).
- `login_state.json` — создаётся `vpn_login_session.go`, SSE `login_state_changed` отражает current state; при `CmdResult.ok=false` пишется `error` в file/event.
- `trace.log` — задействован `trace_handlers.go` и `watchFileChange`, SSE `trace_changed` парсит tail. `trace_append` endpoint пишет новые строки (CmdResult). Web должна ограничить size (<=1<<20) и показывать stderr/exitCode.
### Watchers и события
- `watchStatusFile`, `watchLoginFile`, `watchAutoloop`, `watchTrafficAppMarksTTL`, `watchSystemdUnit*` запускаются на `startWatchers` и публикуют события (`status_changed`, `login_state_changed`, `autoloop_status_changed`, `unit_state_changed`, `appmarks_ttl`).
- `events_bus` буферизует последние `SVPN_EVENTS_CAP` событий (config via env). SSE `events/stream` читает их `since`/`Last-Event-ID`. При ошибок (blocked mutex, dropped events) нужно логировать `selective-vpn-api` и отображать `status_error`.
- `events.push` используется для `routes_nft_progress`, `smartdns`, `trace_changed`. Вебу важно знать ключи, чтобы фильтровать события по статус/trace/health.

View File

@@ -0,0 +1,76 @@
# B2 Resolver: разница ролей и план улучшения
Дата: 2026-03-07
Статус: planned
Владелец: Engineering
## 1) Цель
- Зафиксировать, чем отличается текущий системный resolver selective-vpn от DNS-модуля `sing-box`.
- Зафиксировать roadmap доработки нашего resolver, чтобы он оставался основой для `selective`-маршрутизации.
## 2) Роли компонентов
- `System resolver` (Go + SmartDNS + nftset):
- источник истины для PBR (`routes/update`, nft sets, wildcard/static fallback),
- системный контроль резолва для selective-режима.
- `SingBox DNS`:
- DNS-часть transport-профиля `sing-box`,
- управляет резолвом внутри конкретного engine/profile.
Правило слоя:
- В текущей архитектуре authoritative для маршрутизации остаётся системный resolver.
- `SingBox DNS` используется как transport-level capability и не заменяет системный PBR resolver.
## 3) Матрица различий
| Область | System resolver (текущий) | SingBox DNS |
| --- | --- | --- |
| Основная роль | Системная selective-маршрутизация | DNS внутри transport engine |
| Связь с nft/PBR | Прямая (`agvpn4/agvpn_dyn4`, `routes/update`) | Непрямая, через поведение engine |
| Wildcard runtime | Да (SmartDNS nftset + merge в `routes/update`) | Нет прямого контроля системных nft sets |
| Static fallback | Да (`static-ips.txt`, policy merge) | Через raw/typed config профиля |
| Единая картина для всех клиентов | Да, через Go state/API | Нет, scoped к конкретному профилю |
| Риск рассинхрона | Низкий при single source of truth | Высокий, если использовать как отдельный source of truth |
## 4) Основные gap-ы текущего resolver
- Недостаточная наблюдаемость по качеству резолва (latency/error-rate/NX/servfail per upstream).
- Нет отдельного endpoint'а с полным health snapshot resolver pipeline.
- Нужна более строгая TTL/refresh-стратегия для mixed источников (cache + wildcard runtime + static).
- Нужен формализованный negative caching и rate-limit на проблемные домены при деградации upstream.
- Нужна формализация owner-map для доменов в multi-client сценариях (без дублей и гонок).
## 5) План улучшения resolver (приоритеты)
### R1 Надёжность и консистентность
- Ввести unified resolver state snapshot (`updated_at`, `stale`, `refresh_in_progress`, `last_error`, `next_retry_at`, counters).
- Усилить single-flight на expensive refresh-path и добавить доменный backoff.
- Нормализовать merge-порядок источников: `static fallback -> wildcard runtime -> resolver cache`.
### R2 Качество резолва
- Добавить adaptive upstream scoring (latency/success/NX/timeout/servfail).
- Добавить sticky-preferred upstream с безопасным failover.
- Ввести отдельную политику negative cache TTL для NXDOMAIN/SERVFAIL.
### R3 Наблюдаемость и API
- Добавить API snapshot для resolver health:
- `GET /api/v1/resolver/status` (target),
- `POST /api/v1/resolver/refresh` (target trigger),
- `GET /api/v1/resolver/upstreams` (target metrics).
- Добавить SSE-события:
- `resolver_status_changed`,
- `resolver_upstream_degraded`,
- `resolver_refresh_completed`.
### R4 Multi-client безопасность
- Ввести `domain_owner_map` и conflict-detection до применения policy.
- Гарантировать, что один domain-selector не получает двух владельцев без explicit override.
- Синхронизировать resolver ownership с transport policy revision.
## 6) Критерии готовности B2
- Есть документированная и реализуемая разница ролей `system resolver` vs `sing-box DNS`.
- Resolver имеет прозрачный health/status API и наблюдаемость по upstream.
- При деградации upstream UI получает stale-safe состояние, а не "пустой провал".
- В multi-client режиме домены не уходят в двойное владение без подтверждённого override.
## 7) Ограничения этапа
- В этом этапе не переводим управление маршрутизацией на `SingBox DNS`.
- `DNSTT/Phoenix` DNS-аспекты не расширяются в UI до завершения `SingBox` API/GUI трека.

View File

@@ -0,0 +1,50 @@
# B4 Runtime Dependencies And Preflight
## Важно
- `go.mod` хранит только Go-модули (библиотеки, которые импортируются в коде).
- Внешние сервисы/бинарники (`systemd`, `nft`, `ip`, `sing-box`, `dnstt-client`, `phoenix-client`) не являются Go-модулями и не должны фиксироваться в `go.mod`.
## Текущие Go-зависимости (по `go.mod`)
- `github.com/cenkalti/backoff/v4`
- `github.com/creack/pty`
## Runtime-зависимости ядра (вне go.mod)
### Required (core path)
- `systemctl`
- `nft`
- `ip`
- `curl`
- `/usr/local/bin/adguardvpn-cli-root`
### Required service units (current production path)
- `singbox@.service`
### Recommended / optional
- `nsenter` (предпочтительный exec-mode для netns)
- `wget` (fallback для части egress probe)
- `ps`
- `ipset`
### Optional by enabled transport kind
- `sing-box` (`/usr/local/bin/sing-box` или `/usr/bin/sing-box`)
- `dnstt-client` (`/usr/local/bin/dnstt-client` или `/usr/bin/dnstt-client`)
- `phoenix-client` (`/usr/local/bin/phoenix-client` или `/usr/bin/phoenix-client`)
### Optional service units (зависят от deployment-профиля)
- `adguardvpn-autoconnect.service`
- `smartdns-local.service`
- `selective-vpn2@.service`
- `sing-box.service` (legacy/compat)
- `dnstt-client.service`
- `phoenix-client.service`
## Preflight-check
- Скрипт: `scripts/check_runtime_dependencies.sh`
- Режимы:
- `scripts/check_runtime_dependencies.sh` — проверка required + warning по optional.
- `scripts/check_runtime_dependencies.sh --strict` — fail если есть missing/warning.
## Зачем это перед рефакторингом
- Убираем смешение понятий: Go-зависимости отдельно, runtime/system-зависимости отдельно.
- Перед декомпозицией (`F1.*`) быстро проверяем среду и снижаем ложные регрессии.

View File

@@ -0,0 +1,61 @@
# B5 SingBox Template Migration And Rollback Runbook
## Цель
Операционный runbook для миграции `legacy singbox-<id>.service` к template-модели `singbox@<id>.service` и для аварийного отката при инциденте деплоя.
## Контекст текущей модели
- Целевой runtime: `singbox@.service` + per-instance drop-in `singbox@<id>.service.d/10-selective-vpn.conf`.
- One-shot миграция legacy unit выполняется автоматически в pre-action (`start/restart`) для `kind=singbox`.
- Safety guard: legacy unit обрабатывается только при ownership marker `Environment=SVPN_TRANSPORT_ID=<client_id>`.
## Preflight перед деплоем
1. Проверить runtime-зависимости:
- `scripts/check_runtime_dependencies.sh`
2. Проверить strict-режим (опционально для CI/релиза):
- `scripts/check_runtime_dependencies.sh --strict`
3. Проверить наличие template unit:
- `systemctl list-unit-files 'singbox@.service' --no-legend`
## Управление миграцией
- Отключить авто-миграцию для профиля:
- `config.singbox_legacy_unit_migrate=false`
- Включить dry-run без изменений unit-файлов:
- `config.singbox_legacy_unit_migrate_dry_run=true`
- Поведение dry-run:
- backend пишет trace/stdout о планируемом `legacy -> template` переходе,
- `stop/disable/remove` не выполняются.
## Нормальный migrate-путь
1. Обновить API-бинарь и перезапустить `selective-vpn-api.service`.
2. На `start/restart` конкретного SingBox-клиента backend:
- валидирует ownership legacy unit,
- выполняет `stop + disable + remove` legacy unit,
- выполняет `daemon-reload + reset-failed` для legacy unit,
- запускает template instance `singbox@<id>.service`.
3. Проверить:
- `systemctl status 'singbox@<id>.service'`
- `systemctl list-unit-files 'singbox-*' --no-legend` (legacy не должен появляться как managed unit)
## Аварийный rollback (template -> legacy)
Важно: текущий прод-код нормализует SingBox clients к `config.unit=singbox@.service`, поэтому rollback делается на уровне release rollback (предыдущий API build + восстановление unit/state).
1. Зафиксировать инцидент и остановить изменения policy:
- временно не выполнять `start/restart/switch` через GUI/API.
2. Откатить API до предыдущего релиза (где legacy path поддерживался штатно).
3. Восстановить `transport-clients` state из backup/снапшота релиза (если в инциденте state был изменён).
4. Восстановить legacy unit-файлы `singbox-*.service` из backup среды (если удалены).
5. Выполнить:
- `systemctl daemon-reload`
- `systemctl reset-failed`
6. Поднять нужные legacy unit и проверить egress/health.
## Проверка после rollback
- `systemctl is-active selective-vpn-api.service`
- `systemctl is-active 'singbox-<id>.service'` (для rollback-релиза)
- API smoke:
- `curl -fsS http://127.0.0.1:8080/healthz`
- `curl -fsS http://127.0.0.1:8080/api/v1/transport/clients`
## Примечание
- Legacy `sing-box.service` остаётся только как compat-артефакт окружения.
- Production path для новых профилей: только `singbox@.service`.

View File

@@ -0,0 +1,72 @@
# C1 Подготовка ядра к веб-совместимости
Дата: 2026-02-27
Статус: draft
Владелец: Engineering
## 1) Цель
- Зафиксировать, какие компоненты Go-ядра уже готовы к внешнему доступу (SSE, REST, локальный API) и что нужно изменить/документировать для будущего веб-прототипа.
- Убедиться, что веб-интерфейс сможет работать с ядром через ожидаемые договоренности (binding, CORS, авторизация, long-polling/SSE).
- Зафиксировать архитектурный принцип повторного использования одного API-контракта для `web + iOS + Android`, без форков бизнес-логики по платформам.
- Выбор UI-стека вынесен в `docs/phase-c/C2_WEB_STACK_DECISION.md` (принято: `Vite + React + TypeScript` для MVP).
## 2) Критерии завершения
- Описан binding (`127.0.0.1:8080`), механизм SSE (`/api/v1/events/stream`), стандартные ответы (e.g. `CmdResult`, dataclasses) и авторизация через `vpn_login_session`.
- Зафиксированы необходимые изменения инфраструктуры (proxy/unix socket, CORS/CSRF, дополнительная аутентификация или токены).
- Собраны критические endpointы для веба (routes service/update/rollback, traffic mode/appmarks/profiles, DNS/SmartDNS, VPN, trace) и их характеристики (async операции, payload, ответы).
- Для мобильных клиентов описаны условия transport/auth/retry (TLS, токены, refresh, polling fallback к SSE, device session control).
## 3) Задачи
- Подтвердить, что SSE поток (`events/stream`) предоставляет status/login/trace/autoloop updates и определить, какие события нужно визуализировать (policy route, unit state, autoloop, SmartDNS, routes progress).
- Задокументировать payload/методы/JSON форматы для ключевых endpointов (`routes/service`, `routes/update`, `traffic/mode`, `dns/benchmark`, `smartdns/prewarm`, `vpn/login/session`), чтобы новый фронт мог повторить вызовы.
- Определить ограничения текущего binding (`127.0.0.1:8080`) и описать безопасный путь для проксирования (unix socket + nginx/systemd socket/reverse proxy).
- Проработать механизм авторизации: как web UI вызывает `/api/v1/vpn/login/session/start`, получает `login_state`, делает action, и как `/api/v1/auth/status`/`login/session/state` поддерживают health.
- Уточнить, какие операции требуют async-статуса (например, `routes update`, `routes/service restart`) и как отображать прогресс (SSE event `routes_nft_progress`, `status_changed`).
- Задокументировать headers/CORS (`allowed origin`, `Authorization`, `Content-Type`, `X-Requested-With`, `GET/POST`) и сценарии CSRF/preflight при использовании cookie.
- Утвердить единый API-versioning подход (`/api/v1`) и правила обратной совместимости для всех клиентов (веб и мобильных).
- Зафиксировать контракт ошибок (`ok`, `message`, `exitCode`, `stderr`) и политику idempotency/retry для мобильных сетевых условий.
## 4) SSE, watchers, trace
- `events_bus.go` собирает события `{ID, Kind, Ts, Data}` и поддерживает replay по `Last-Event-ID`/`since`. Web должен подписаться на `status_changed`, `login_state_changed`, `routes_nft_progress`, `unit_state_changed`, `autoloop_status_changed` и восстанавливать соединение при reconnect.
- `watchers.go` poll-ит `status.json`, `login_state.json`, `trace.log`, `autoloop.log`, state traffic appmarks TTL и systemd unitы (`routes_service`, `routes_timer`, `vpn_unit`, `smartdns_unit`). Эти события идут в SSE и дают realtime-статус на UI (health, trace, unit status).
- `trace_handlers.go` предоставляет `GET /api/v1/trace` (tail), `/trace-json` (structured), `/trace/append`. Убедиться, что web ограничивает JSON size (1<<20) и отображает `CmdResult` (stdout/stderr) при append.
- Нужно описать, какие watcher-события и trace-последствия критичны для UI (policy route check, SmartDNS runtime, traffic appmarks TTL) и как SSE clients обрабатывают disconnect/replay.
## 5) Проксирование и безопасность
- API должен быть доступен через безопасный proxy (unix socket + nginx, systemd socket или reverse proxy) и/или на изолированном интерфейсе, чтобы не экспонировать `127.0.0.1:8080` наружу.
- Proxy configuration:
- Systemd socket unit (`selective-vpn-api.socket`) слушает `/run/selective-vpn.sock`; nginx проксирует `location /api/``proxy_pass http://unix:/run/selective-vpn.sock:`.
- В nginx включаете `proxy_buffering off`, `proxy_http_version 1.1`, `proxy_set_header Connection keep-alive`, `proxy_set_header Host $host`, `proxy_set_header X-Real-IP $remote_addr`, чтобы SSE `/api/v1/events/stream` работал без задержек.
- Альтернатива — nginx на localhost:8080 с firewall, разрешающий соединения только от веб-интерфейса; при этом добавляется `allow 127.0.0.1` и `deny all`.
- CORS/CSRF:
- `Access-Control-Allow-Origin: https://selective-ui.local` (или переменная окружения). `Access-Control-Allow-Credentials: true` при использовании cookie.
- `Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With`, `Access-Control-Allow-Methods: GET, POST`, `Access-Control-Expose-Headers: X-Request-Id`.
- Preflight (`OPTIONS`) обрабатывается proxy (nginx) или Go кодом, возвращая нужные заголовки и `204`.
- Авторизация:
- Web UI вызывает `POST /api/v1/vpn/login/session/start`, backend пишет `login_state.json` и возвращает `LoginState` (включая `state`/`msg`/`email`).
- Web сохраняет `login_state_id`, прикладывает его как `Authorization: Bearer $login_state_id` либо использует session cookie от proxy. Go middleware проверяет токен перед вызовом критичных endpointов.
- `/api/v1/auth/status` (или расширение `/api/v1/vpn/login/session/state`) сообщают, активна ли сессия; если нет, UI перенаправляет пользователя к login modal.
- `CmdResult` с `ok=false`, SSE `login_state_changed` или события `status_error` должна отображаться как alert и давать кнопку retry.
- Async операции и прогресс:
- `routes update` и `routes/service restart` занимают до 60 секунд. Web запускает POST `/api/v1/routes/update`, включает бесперебойный SSE и показывает progress bar, пока не придёт `CmdResult.ok=true` + event `routes_nft_progress`/`status_changed`.
- `dns/benchmark`, `smartdns/prewarm` и `traffic/mode/test` тоже могут длиться минуты — UI ожидает SSE `progress`/`status` и отключает повторные вызовы до завершения.
- Trace append (`/api/v1/trace/append`) возвращает `CmdResult`; UI показывает `stderr`/`exitCode` и ограничивает длину payload (<=1<<20).
## 6) Кросс-платформенная переиспользуемость (Web + iOS + Android)
- Backend-слой остается единым: бизнес-логика только в Go-ядре, клиенты используют одинаковые endpointы и payload-контракты.
- DTO/модели (`Status`, `CmdResult`, `TrafficModeStatus`, `LoginState`, `TrafficAppMarkItem`) считаются каноничными; изменение полей только с backward-compatible стратегией.
- Реaltime:
- Web: SSE как основной канал.
- Mobile: SSE при активном приложении + fallback polling для background/нестабильной сети.
- Авторизация:
- Веб-клиент может использовать cookie/token через proxy.
- Мобильные клиенты используют bearer access token + refresh token, хранят секреты в secure storage (iOS Keychain / Android Keystore).
- Надежность:
- Для mutating POST операций вводить `Idempotency-Key`, чтобы избежать дублей при ретраях на мобильной сети.
- Добавить request correlation (`X-Request-Id`) в ответы/логи для расследования инцидентов на всех клиентах.
- Транспорт:
- Только HTTPS, поддержка certificate pinning для мобильных приложений.
- Никаких прямых подключений к `127.0.0.1:8080` с клиентов; только через контролируемый gateway/proxy.
- Transport backends:
- Поддержать модель pluggable backend-клиентов (`sing-box`, `dnstt-client`, `phoenix->slipstream`) с единым API-контрактом ядра.
- UI-слой (web/iOS/Android) не должен зависеть от конкретного transport backend; различия покрываются capability endpoint/матрицей в Go.

View File

@@ -0,0 +1,37 @@
# C2 Решение по стеку web prototype
Дата: 2026-03-07
Статус: approved
Владелец: Engineering
## 1) Принятое решение
- Для web prototype используется `Vite + React + TypeScript` (SPA).
- Бэкенд-ядро остаётся в Go (`/api/v1` + SSE `/api/v1/events/stream`).
- `Next.js` на MVP-этапе не используется.
## 2) Почему так
- В проекте уже есть готовый Go API-контракт; отдельный Node/SSR-слой не обязателен.
- Цель текущего этапа: рабочая control-plane панель, а не SEO-публичный сайт.
- `Vite` даёт быстрый старт, простой билд и минимальные операционные риски.
## 3) Базовый frontend stack
- `react` + `typescript`
- `vite`
- `react-router`
- `@tanstack/react-query` для REST
- `EventSource` (SSE) для realtime
- lightweight UI state (например, `zustand`) только для локального состояния интерфейса
## 4) Когда рассматривать переход на Next.js
- Появляется требование SSR/SEO.
- Нужен встроенный BFF/edge middleware в самом фронтенд-проекте.
- Требуется server-side session orchestration, которую нецелесообразно держать в Go gateway.
## 5) Архитектурный инвариант
- Независимо от UI-фреймворка, источник истины остаётся в Go-ядре.
- UI не дублирует бизнес-логику маршрутизации/transport/policy.
- Контракт для web/iOS/Android остаётся единым: `/api/v1`.
## 6) Статус внедрения
- Создан модуль `selective-vpn-web/` (foundation level).
- На текущем этапе включены read-only проверки и SSE connectivity; mutating controls будут подключаться по фазам P1/E4.

View File

@@ -0,0 +1,184 @@
# D1 Документирование готовности ядра
Дата: 2026-03-02
Статус: in-progress
Владелец: Engineering
## 1) Цель
- Дать точный endpoint-level срез: что уже можно подключать в веб, что требует инфраструктурных условий, и где нужен отдельный UX-поток.
- Зафиксировать критерии перехода к веб-прототипу на базе фактического кода `selective-vpn-api`.
## 2) Легенда статусов
- `ready-read` — готово для веба как read endpoint.
- `ready-write` — готово для веба как write endpoint, но обязательно через auth/rbac/audit/proxy.
- `ready-async` — write endpoint готов, но требуется async UX (SSE progress/polling).
- `ready-interactive` — endpoint готов, но требует интерактивного flow (session state/action).
## 3) Глобальные условия для веба
- API должен ходить через proxy (unix socket + nginx/systemd socket) и не быть публично открыт напрямую на `127.0.0.1:8080`.
- Для всех mutating endpoints обязательны auth/rbac и аудит вызовов.
- Для SSE (`/api/v1/events/stream`) нужен корректный proxy режим (`proxy_buffering off`).
- Для async операций (`routes update`, `smartdns prewarm`, `dns benchmark`) веб должен слушать SSE/статусы и блокировать повторные конкурирующие действия.
- Архитектурный инвариант: один контракт `/api/v1` для `web + iOS + Android`, без платформенных форков backend-логики.
## 4) Матрица endpoint -> web-ready
| Endpoint | Method | Handler | Статус | Комментарий для веба |
| --- | --- | --- | --- | --- |
| `/healthz` | GET | `handleHealthz` | `ready-read` | Базовый liveness probe. |
| `/api/v1/events/stream` | GET | `handleEventsStream` | `ready-read` | SSE source для realtime UI. |
| `/api/v1/status` | GET | `handleGetStatus` | `ready-read` | Карточка статуса маршрутов. |
| `/api/v1/routes/status` | GET | `handleGetStatus` | `ready-read` | Алиас статуса. |
| `/api/v1/vpn/login-state` | GET | `handleVPNLoginState` | `ready-read` | Текущий state логина VPN. |
| `/api/v1/systemd/state` | GET | `handleSystemdState` | `ready-read` | Проверка unit state по query `unit`. |
| `/api/v1/routes/service/start` | POST | `makeRoutesServiceActionHandler("start")` | `ready-write` | Чувствительная операция systemd. |
| `/api/v1/routes/service/stop` | POST | `makeRoutesServiceActionHandler("stop")` | `ready-write` | Чувствительная операция systemd. |
| `/api/v1/routes/service/restart` | POST | `makeRoutesServiceActionHandler("restart")` | `ready-write` | Чувствительная операция systemd. |
| `/api/v1/routes/service` | POST | `handleRoutesService` | `ready-write` | Универсальный action endpoint. |
| `/api/v1/routes/update` | POST | `handleRoutesUpdate` | `ready-async` | Запуск async job; прогресс через SSE/events. |
| `/api/v1/routes/timer` | GET/POST | `handleRoutesTimer` | `ready-write` | GET состояние, POST переключение. |
| `/api/v1/routes/timer/toggle` | POST | `handleRoutesTimerToggle` | `ready-write` | Legacy toggle для совместимости. |
| `/api/v1/routes/rollback` | POST | `handleRoutesClear` | `ready-write` | Очистка nft/routes с риском влияния на трафик. |
| `/api/v1/routes/clear` | POST | `handleRoutesClear` | `ready-write` | Алиас rollback. |
| `/api/v1/routes/cache/restore` | POST | `handleRoutesCacheRestore` | `ready-write` | Восстановление из clear-cache. |
| `/api/v1/routes/precheck/debug` | POST | `handleRoutesPrecheckDebug` | `ready-async` | Debug flow + опциональный async restart. |
| `/api/v1/routes/fix-policy-route` | POST | `handleFixPolicyRoute` | `ready-write` | Ремонт policy route по status.json. |
| `/api/v1/routes/fix-policy` | POST | `handleFixPolicyRoute` | `ready-write` | Алиас fix-policy-route. |
| `/api/v1/traffic/mode` | GET/POST | `handleTrafficMode` | `ready-write` | Ключевой control-plane endpoint. |
| `/api/v1/traffic/mode/test` | GET/POST | `handleTrafficModeTest` | `ready-read` | Проверка health/probe для UI. |
| `/api/v1/traffic/advanced/reset` | POST | `handleTrafficAdvancedReset` | `ready-write` | Сброс advanced bypass state. |
| `/api/v1/traffic/interfaces` | GET | `handleTrafficInterfaces` | `ready-read` | Список интерфейсов и active iface. |
| `/api/v1/traffic/candidates` | GET | `handleTrafficCandidates` | `ready-read` | Подсказки subnet/unit/uid. |
| `/api/v1/traffic/appmarks` | GET/POST | `handleTrafficAppMarks` | `ready-write` | Runtime app marking, требует RBAC. |
| `/api/v1/traffic/appmarks/items` | GET | `handleTrafficAppMarksItems` | `ready-read` | Список текущих runtime marks. |
| `/api/v1/traffic/app-profiles` | GET/POST/DELETE | `handleTrafficAppProfiles` | `ready-write` | CRUD профилей приложений. |
| `/api/v1/traffic/audit` | GET | `handleTrafficAudit` | `ready-read` | Sanity-check и issues report. |
| `/api/v1/trace` | GET | `handleTraceTailPlain` | `ready-read` | Plain tail trace. |
| `/api/v1/trace-json` | GET | `handleTraceJSON` | `ready-read` | Structured trace view. |
| `/api/v1/trace/append` | POST | `handleTraceAppend` | `ready-write` | Write в trace; ограничить доступ и размер body. |
| `/api/v1/dns-upstreams` | GET/POST | `handleDNSUpstreams` | `ready-write` | Конфиг upstreams. |
| `/api/v1/dns/upstream-pool` | GET/POST | `handleDNSUpstreamPool` | `ready-write` | Конфиг pool. |
| `/api/v1/dns/status` | GET | `handleDNSStatus` | `ready-read` | Состояние DNS mode/runtime. |
| `/api/v1/dns/mode` | POST | `handleDNSModeSet` | `ready-write` | Переключение DNS resolver mode. |
| `/api/v1/dns/benchmark` | POST | `handleDNSBenchmark` | `ready-async` | Долгий бенчмарк; нужен UX ожидания. |
| `/api/v1/dns/smartdns-service` | POST | `handleDNSSmartdnsService` | `ready-write` | Start/stop/restart smartdns unit. |
| `/api/v1/smartdns/service` | GET/POST | `handleSmartdnsService` | `ready-write` | GET state + POST action. |
| `/api/v1/smartdns/runtime` | GET/POST | `handleSmartdnsRuntime` | `ready-write` | Runtime nftset hook toggle. |
| `/api/v1/smartdns/prewarm` | POST | `handleSmartdnsPrewarm` | `ready-async` | Потенциально долгий prewarm. |
| `/api/v1/domains/table` | GET | `handleDomainsTable` | `ready-read` | Таблица nft/ipset dump. |
| `/api/v1/domains/file` | GET/POST | `handleDomainsFile` | `ready-write` | File-backed editor; нужен ACL по name. |
| `/api/v1/smartdns/wildcards` | GET/POST | `handleSmartdnsWildcards` | `ready-write` | CRUD wildcard list. |
| `/api/v1/vpn/autoloop-status` | GET | `handleVPNAutoloopStatus` | `ready-read` | Статус autoloop parser. |
| `/api/v1/vpn/status` | GET | `handleVPNStatus` | `ready-read` | VPN state + unit state. |
| `/api/v1/vpn/autoconnect` | POST | `handleVPNAutoconnect` | `ready-write` | Управление adgvpn unit. |
| `/api/v1/vpn/locations` | GET | `handleVPNListLocations` | `ready-read` | Список location options. |
| `/api/v1/vpn/location` | POST | `handleVPNSetLocation` | `ready-write` | Смена desired location + unit restart. |
| `/api/v1/vpn/login/session/start` | POST | `handleVPNLoginSessionStart` | `ready-interactive` | Старт интерактивной PTY login session. |
| `/api/v1/vpn/login/session/state` | GET | `handleVPNLoginSessionState` | `ready-interactive` | Poll состояния и линий с `since`. |
| `/api/v1/vpn/login/session/action` | POST | `handleVPNLoginSessionAction` | `ready-interactive` | Команды open/check/cancel. |
| `/api/v1/vpn/login/session/stop` | POST | `handleVPNLoginSessionStop` | `ready-interactive` | Cleanup/stop interactive session. |
| `/api/v1/vpn/logout` | POST | `handleVPNLogout` | `ready-write` | Logout + refresh login state. |
## 5) Скриптовые smoke-тесты
- `tests/api_sanity.sh` — read endpoints + method guard + SSE headers.
- `tests/trace_append.sh` — append + readback (`/trace`, `/trace-json`).
- `tests/events_stream.py` — SSE stream + активный триггер `trace_append`.
- `tests/vpn_login_flow.py` — start/state/action/stop для login session.
- `tests/transport_flow_smoke.py` — API-flow `draft/validate/confirm/apply/rollback` для transport policy.
- `tests/transport_platform_compatibility_smoke.py` — проверка кроссплатформенного transport-контракта (`web/iOS/Android`) через capabilities + policy endpoints.
- `tests/transport_runbook_cli_smoke.sh` — smoke операционного runbook helper (`scripts/transport_runbook.py`) по lifecycle transport-клиента.
- `tests/transport_recovery_runbook_smoke.sh` — smoke recovery runbook (`scripts/transport_recovery_runbook.py`) для сценария `health->restart->fallback->diagnostics`.
- `tests/transport_systemd_real_e2e.py` — real-systemd e2e для `singbox/dnstt(+ssh)/phoenix` + проверка cleanup unit artifacts.
- `tests/transport_production_like_e2e.py` — production-like e2e для template-команд и `packaging_profile=bundled` на `systemd` backend.
- `tests/transport_singbox_e2e.py` — backend-first e2e для `singbox` lifecycle/guards.
- `tests/transport_dnstt_e2e.py` — backend-first e2e для `dnstt` lifecycle/guards (`ssh overlay`, template checks).
- `tests/transport_phoenix_e2e.py` — backend-first e2e для `phoenix` lifecycle/guards.
- `tests/run_all.sh` — общий запуск набора.
## 6) Факт прогона (2026-03-02)
- Команда: `API_URL=http://127.0.0.1:8080 ./tests/run_all.sh`.
- Результат: `api_sanity`, `trace_append`, `events_stream`, `vpn_login_flow``passed`.
- Дополнение (2026-03-07):
- Расширенный `run_all` с transport smoke/e2e и packaging smoke проходит `passed`;
- включён и проходит `tests/transport_platform_compatibility_smoke.py` (capabilities + policy contract);
- при запуске старого backend-процесса `transport_*_e2e` могут завершаться `SKIP` (без `provision/metrics`);
- после пересборки и перезапуска `selective-vpn-api` из текущего кода `transport_singbox_e2e`, `transport_dnstt_e2e`, `transport_phoenix_e2e` прошли без `SKIP`.
- в `run_all` включены и проходят `transport_systemd_real_e2e` + `transport_production_like_e2e` (template-commands + packaging profile checks на `runner=systemd`).
## 7) Вывод для веб-прототипа
- Срез по legacy-матрице (53 endpoint из Phase D):
- `ready-read`: 18
- `ready-write`: 27
- `ready-async`: 4
- `ready-interactive`: 4
- Дополнение 2026-03-07:
- В ядро добавлены `8` базовых mux-route `/api/v1/transport/*` + action-subpath `GET /api/v1/transport/clients/{id}/metrics` (контракт D4.1).
- Их контракт и rollout описаны в `docs/phase-e/E2_TRANSPORT_API_CONTRACT.md`.
- По API-ядру нет endpoint, который блокирует веб-прототип на уровне контракта.
- Ограничения не в ручках, а в обвязке: auth/rbac, proxy/CORS/CSRF, и UX для async/interactive сценариев.
- Практический старт веба можно делать сразу: read панели + write действия под admin scope + SSE realtime слой.
## 8) Условия переиспользования для смартфонов (iOS/Android)
- Серверная обвязка:
- gateway/proxy с TLS и токен-авторизацией;
- refresh/access токены и управление device sessions;
- аудит и rate-limit для mutating endpoints.
- Контракт:
- стабильные DTO и error contract (`ok`, `message`, `exitCode`, `stderr`);
- versioning через `/api/v1` и backward-compatible изменения.
- Realtime и фоновые ограничения:
- веб использует SSE постоянно;
- mobile использует SSE в foreground и polling fallback в background.
- Надежность мобильной сети:
- idempotency key для POST операций;
- retry policy с экспоненциальной паузой;
- correlation id для трассировки запросов.
- Безопасность клиента:
- iOS Keychain / Android Keystore для токенов;
- certificate pinning для production приложений.
## 9) D3: Последовательность запуска multi-client
- D3.1: Завершить gateway/auth слой (TLS, токены, RBAC, audit).
- D3.2: Подключить web-клиент к уже готовой матрице endpoint и SSE.
- D3.3: Ввести mobile transport profile (polling fallback + retry/idempotency).
- D3.4: Запустить iOS/Android клиенты на том же `/api/v1` без изменения бизнес-логики ядра.
## 10) D4: Transport Integration Backlog
- D4.1 `sing-box client`:
- Роль: универсальный клиент-транспорт для сценариев proxy/tun.
- Требование к ядру: запуск/остановка/health через backend-адаптер, без переноса логики в UI.
- Статус: `in-progress` (2026-03-07: в API введён минимальный общий backend-контракт lifecycle/health/metrics/errors, добавлен `GET /api/v1/transport/clients/{id}/metrics`).
- E2E шаг (backend-first): `tests/transport_singbox_e2e.py`:
- проверяет успешный lifecycle на `runner=mock` (`provision/start/health/restart/stop/metrics`);
- проверяет guard `runtime_mode=embedded` -> `TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED`;
- проверяет fail-fast `require_binary=true` для отсутствующего `singbox_bin`.
- Примечание: если в рантайме запущен старый процесс API без `provision/metrics`, тест завершится `SKIP`; для полного прогона нужен перезапуск backend из актуального кода.
- D4.2 `dnstt-client`:
- Роль: отдельный клиент для DNSTT-протокола (как самостоятельный backend).
- Требование к ядру: управление lifecycle + состояние/ошибки через общий API слой.
- Уточнение: поддержать режим `dnstt + ssh overlay` (туннель поверх SSH как единая система, аналогично phoenix UX-модели).
- Статус: `in-progress` (2026-03-07: в Go добавлены adapter foundation, `POST /api/v1/transport/clients/{id}/provision`, template-команды запуска, systemd restart/watchdog tuning, unit hardening-профили, `runtime_mode` foundation `exec|embedded|sidecar`, packaging profiles (`system|bundled`) и manual pinned updater/rollback scripts; `config.exec_start` работает как optional override).
- E2E шаг (backend-first): `tests/transport_dnstt_e2e.py`:
- проверяет успешный lifecycle на `runner=mock`;
- проверяет guard `ssh_overlay` конфигурации (`ssh_host` обязателен, `ssh_unit` валидируется);
- проверяет template-валидацию DNSTT при неполном config.
- Примечание: как и для singbox, при старом API-процессе без `provision` тест завершится `SKIP`; нужен перезапуск backend из текущего кода.
- D4.3 `phoenix -> slipstream`:
- Роль: отдельный backend-клиент для подключения к удаленному `slipstream` серверу.
- Требование к ядру: конфиг/статус/управление сессией через backend-адаптер и общий контракт.
- Статус: `in-progress` (2026-03-07: добавлен backend-first e2e тест `tests/transport_phoenix_e2e.py` для lifecycle/guards на API-контракте).
- E2E шаг (backend-first): `tests/transport_phoenix_e2e.py`:
- проверяет успешный lifecycle на `runner=mock` (`provision/start/health/restart/stop/metrics`);
- проверяет guard `runtime_mode=embedded` -> `TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED`;
- проверяет fail-fast `require_binary=true` для отсутствующего `phoenix_bin`.
- Примечание: при старом API-процессе без `provision/metrics` тест завершится `SKIP`; для полного прогона нужен перезапуск backend из актуального кода.
- D4.4 Общий инвариант:
- Для `web + iOS + Android` UI не знает о внутренней реализации backend-клиента; он работает с единым API ядра.
- Все transport-backend различия прячутся в Go-слое (feature flags, capability matrix, unified error contract).
## 11) D4.3: Матрица совместимости web + iOS + Android
- Отдельный артефакт: `docs/phase-d/D4_PLATFORM_COMPATIBILITY_MATRIX.md`.
- Зафиксировано:
- единый control-plane контракт `/api/v1` для всех платформ;
- transport-runtime работает только в backend (`exec`), UI работает через capabilities/policies flow;
- mobile использует тот же API, с fallback `polling` при недоступном SSE в background.

View File

@@ -0,0 +1,59 @@
# D4.3 Матрица совместимости (Web + iOS + Android)
Дата: 2026-03-07
Статус: done
Владелец: Engineering
## 1) Цель
- Зафиксировать единый контракт, чтобы `web`, `iOS`, `Android` переиспользовали одно Go-ядро (`/api/v1`) без платформенных форков бизнес-логики.
- Явно обозначить ограничения transport-runtime и ожидания к клиентам.
## 2) Инвариант архитектуры
- Все transport-клиенты (`singbox`, `dnstt`, `phoenix`) запускаются на стороне хоста с `selective-vpn-api` через backend-адаптеры.
- `web/iOS/Android` являются control-plane клиентами: вызывают API, читают состояние, запускают validate/apply flow.
- Платформы не должны управлять локальными `systemd`/бинарями напрямую.
## 3) Платформенная матрица API-контракта
| Контур | Web (`Vite + React + TS`) | iOS | Android |
| --- | --- | --- | --- |
| Базовый API | `GET/POST /api/v1/*` | `GET/POST /api/v1/*` | `GET/POST /api/v1/*` |
| Realtime | SSE (`/api/v1/events/stream`) в foreground | SSE в foreground, polling fallback в background | SSE в foreground, polling fallback в background |
| Transport capabilities | `GET /api/v1/transport/capabilities` | тот же endpoint | тот же endpoint |
| Policy flow | `GET /policies`, `POST /validate`, `POST /apply`, `POST /rollback` | тот же flow | тот же flow |
| Auth/TLS | gateway/proxy + token | gateway/proxy + token + secure storage | gateway/proxy + token + secure storage |
| Error contract | `ok/code/message/exitCode/stderr` | идентично | идентично |
## 4) Матрица transport backend vs платформы
| Transport backend (в Go-ядре) | Web UI | iOS UI | Android UI | Ограничения |
| --- | --- | --- | --- | --- |
| `singbox` | Поддержан через API | Поддержан через API | Поддержан через API | runtime на сервере, не в мобильном UI |
| `dnstt` (+ `ssh_overlay`) | Поддержан через API | Поддержан через API | Поддержан через API | SSH overlay оркестрируется backend-ом |
| `phoenix` -> `slipstream` | Поддержан через API | Поддержан через API | Поддержан через API | подключение/health/metrics только через backend |
## 5) Runtime/packaging ограничения
- Поддерживаемый runtime mode на текущем этапе: `exec=true`.
- `embedded=false`, `sidecar=false` (зарезервировано, без silent fallback).
- Packaging profiles: `system=true`, `bundled=true`.
- Практический вывод для платформ:
- UI показывает capabilities и ограничения из backend.
- UI не должен предполагать поддержку `embedded/sidecar` до явного включения в capabilities.
## 6) Проверка совместимости (smoke)
- Скрипт: `tests/transport_platform_compatibility_smoke.py`.
- Проверяет:
- наличие `singbox/dnstt/phoenix` в `/transport/capabilities`,
- `runtime_modes.exec=true`,
- `packaging_profiles.system=true`, `packaging_profiles.bundled=true`,
- рабочий policy-контракт (`/transport/policies`, `/validate`, `/conflicts`).
Команда запуска:
```bash
API_URL=http://127.0.0.1:8080 ./tests/transport_platform_compatibility_smoke.py
```
## 7) Итог D4.3
- Совместимость `web + iOS + Android` зафиксирована как единый API-контракт control-plane.
- Все transport-runtime различия остаются в Go-ядре.
- Это позволяет продолжать backend-first реализацию и затем переиспользовать ту же логику в desktop/web/mobile UI.

View File

@@ -0,0 +1,34 @@
# D5: NetNS Runtime Case (Ready)
Дата: 2026-03-09
## Что зафиксировано
- `SingBox` может работать в отдельном `netns` без влияния на основной VPN-контур.
- Для запуска внутри namespace используется адаптивный exec-режим:
- default: `nsenter --net=/var/run/netns/<name> -- ...`
- fallback: `ip netns exec <name> ...`
- Подготовка `netns` (veth/route/NAT) и запуск runtime используют единый механизм выбора exec-mode.
- GUI debug toggle (`Debug netns`) изменяет `netns_enabled` через API, делает `provision` и `restart` для активных клиентов.
## Рефакторинг модулей
- Go:
- `selective-vpn-api/app/transport_netns.go` — setup/cleanup/NAT/spec.
- `selective-vpn-api/app/transport_netns_exec.go` — exec-mode selection (`nsenter|ip`) и wrappers.
- GUI:
- `selective-vpn-gui/netns_debug.py` — вычисление состояния netns-кнопки и общий toggle pipeline (`patch -> provision -> restart`).
- `selective-vpn-gui/vpn_dashboard_qt.py` — только UI-обвязка и вызов helper-модуля.
## Минимальный runtime-check
1. `POST /api/v1/transport/clients/<id>/restart` -> `ok=true`.
2. `GET /api/v1/transport/clients/<id>/health` -> `status=up`.
3. `systemctl status <unit>` -> `active (running)`.
4. `systemctl show <unit> -p ExecStart` -> команда через `nsenter` (или `ip netns exec` при fallback).
## Полезные конфиг-ключи клиента
- `netns_enabled: bool`
- `netns_name: string`
- `netns_exec_mode: auto|nsenter|ip` (optional)
- `netns_nsenter_bin: /usr/bin/nsenter` (optional)
- `netns_ip_bin: /sbin/ip` (optional)
- `netns_setup_strict: bool` (optional)
- `netns_auto_cleanup: bool` (optional)

View File

@@ -0,0 +1,188 @@
# E1 Дизайн multi-client маршрутизации (PBR)
Дата: 2026-03-04
Статус: draft
Владелец: Engineering
## 1) Цель
- Спроектировать расширение текущего ядра так, чтобы несколько transport-клиентов (`sing-box`, `dnstt-client`, `phoenix->slipstream`) работали под единым control-plane API.
- Сохранить один источник истины для маршрутизации: только Go-ядро управляет PBR/nft/ip rule, UI только вызывает API.
- Обеспечить безопасную многоклиентную схему без конфликтов: один трафик/сайт не должен одновременно идти через два интерфейса.
## 2) Текущий baseline (по коду)
- Сейчас модель маршрутизации бинарная: `vpn|direct` + глобальные `MARK`/`MARK_APP`/`MARK_DIRECT`/`MARK_INGRESS`.
- Runtime app routing хранится в `traffic-appmarks.json`, persistent launcher-профили в `traffic-app-profiles.json`.
- Центр оркестрации: `traffic_mode.go`, `traffic_appmarks.go`, `routes_update.go`.
- Ограничение: нет сущности "клиент-транспорт" как объекта с собственным iface/table/mark.
## 3) Архитектурный инвариант
- `PBR Engine` в Go-ядре остается единственным writer для:
- `ip rule`,
- `ip route table`,
- `nft chains/sets/rules`,
- `conntrack mark policy`.
- Transport backends (sing-box/dnstt/phoenix) подключаются через backend-адаптеры и не пишут маршруты напрямую.
- UI (desktop/web/iOS/android) оперирует одинаковым API-контрактом, не знает о внутреннем устройстве backend-клиентов.
## 4) Целевая модель данных
### 4.1 TransportClient
- `id`: стабильный ключ (`sb-main`, `dnstt-home`, `phoenix-eu`).
- `kind`: `singbox | dnstt | phoenix`.
- `enabled`: bool.
- `status`: `starting | up | degraded | down`.
- `iface`: фактический интерфейс/туннель.
- `routing_table`: имя таблицы (`agvpn_<id>`).
- `mark_hex`: выделенная fwmark клиента.
- `priority_base`: базовый диапазон `ip rule pref` для клиента.
- `capabilities`: `tcp`, `udp`, `dns_tunnel`, `ssh_tunnel`.
- `health`: last_check, latency, last_error.
### 4.2 RouteIntent
- Нормализованная запись назначения трафика к клиенту:
- `selector_type`: `domain | cidr | app_key | cgroup | uid`.
- `selector_value`: значение селектора.
- `client_id`: целевой клиент.
- `priority`: порядок применения.
- `mode`: `strict | fallback`.
### 4.3 ConflictRecord
- Запись о конфликте маршрутизации:
- `key`: нормализованный ключ пересечения.
- `owners`: список `client_id`.
- `severity`: `warn | block`.
- `reason`: человеко-читаемая причина.
- `suggested_resolution`: автоматическая подсказка.
## 5) Схема марков и таблиц (без конфликтов)
### 5.1 Mark allocator
- Ввести менеджер выделения марков из пула, например:
- `0x100-0x1FF` для client-specific route marks,
- `0x66/0x67/0x68/0x69` оставить для legacy/системных сценариев.
- Для каждого `client_id` выделяется:
- `client_mark`,
- `client_reply_mark` (если нужен отдельный ingress stickiness).
### 5.2 Table allocator
- Для каждого клиента заводится отдельная routing table:
- `agvpn_sb_main`, `agvpn_dnstt_home`, `agvpn_phoenix_eu`.
- В таблице только default route через iface клиента + локальные bypass-правила.
### 5.3 Rule priority allocator
- Для каждого клиента выделяется непересекаемый диапазон `pref`:
- пример: `13000-13049` клиент A, `13050-13099` клиент B.
- Это исключает перетирание правил между клиентами при apply/reconcile.
## 6) Защита от "мешанины" трафика
### 6.1 Destination ownership lock
- Один домен/cidr/app_key в активной конфигурации может иметь только одного владельца (`client_id`).
- При пересечении:
- по умолчанию `block` (HTTP `409` на apply),
- опционально `force_override` с явным подтверждением пользователя.
### 6.2 Flow stickiness (conntrack)
- Для первого пакета потока проставляется `ct mark = client_mark`.
- Для последующих пакетов mark восстанавливается из `ct mark`, чтобы один и тот же flow не перескакивал между интерфейсами.
- Правило действует в `output` и `prerouting`, аналогично текущему ingress-reply подходу.
### 6.3 DNS/IP coherence
- Для domain-based маршрутизации вводится owner-cache:
- `domain -> client_id -> ip set` с TTL.
- Один и тот же домен в активной политике не может одновременно резолвиться в разные клиентские set-цепочки.
### 6.4 Audit/guardrail
- Расширить `traffic_audit` на multi-client проверки:
- duplicate destination ownership,
- overlap CIDR между клиентами,
- app_key на двух клиентах одновременно,
- nft/rule drift по client chains.
## 7) UX дизайн (удобное добавление/переключение)
### 7.1 Экран "Клиенты"
- Список клиентов: имя, тип, статус, интерфейс, health.
- Действия: `Добавить`, `Включить/Выключить`, `Перезапустить`, `Удалить`.
- Мастер добавления:
- Шаг 1: тип клиента (`sing-box`, `dnstt`, `phoenix`),
- Шаг 2: параметры подключения,
- Шаг 3: health-check,
- Шаг 4: назначение default policy.
- Подпункты UX для engine:
- единый селектор `Active engine` с вариантами `singbox|dnstt|phoenix`;
- быстрые действия `Connect`, `Disconnect`, `Switch to ...`;
- явное отображение `desired_engine` vs `active_engine`;
- при деградации показывать `last_error` и action `Rollback to previous engine`.
### 7.2 Экран "Маршрутизация"
- Матрица `Селектор -> Клиент`.
- Массовое назначение списков IP/CIDR/доменов.
- Быстрый переключатель "перенести селектор на другой клиент" с dry-run проверкой конфликтов.
### 7.3 UX предупреждения
- Перед apply показывать diff:
- какие селекторы сменят владельца,
- какие потоки могут быть прерваны,
- какие конфликты заблокируют применение.
- При `force_override` обязательное подтверждение пользователя с явным риском:
- "Один и тот же сайт может потерять стабильность при частой смене интерфейса".
- При switch/connect engine:
- показывать предупреждение о кратковременном разрыве активных сессий;
- запрещать параллельные mutating-операции до завершения текущего switch;
- при failed switch предлагать rollback на предыдущий engine.
## 8) API-контракт (новые ручки, проект)
- `GET /api/v1/transport/clients`
- `POST /api/v1/transport/clients`
- `POST /api/v1/transport/clients/{id}/start`
- `POST /api/v1/transport/clients/{id}/stop`
- `GET /api/v1/transport/clients/{id}/health`
- `GET /api/v1/transport/policies`
- `POST /api/v1/transport/policies/validate`
- `POST /api/v1/transport/policies/apply`
- `GET /api/v1/transport/conflicts`
- `GET /api/v1/transport/capabilities`
Принцип:
- Все операции изменения policy идут через `validate -> apply`.
- `apply` атомарный: либо вся новая политика применена, либо rollback на предыдущую snapshot-конфигурацию.
## 9) Реализация по шагам
### E1.1 Контракты и состояние
- Ввести state-файлы:
- `transport-clients.json`,
- `transport-policies.json`,
- `transport-conflicts.json`.
- Добавить DTO и минимальные read endpoints.
### E1.2 PBR compiler v2
- Реализовать компиляцию RouteIntent в:
- nft sets/chains per client,
- ip rule pref ranges per client,
- table route entries per client.
### E1.3 Guardrails
- Валидация ownership/overlap до apply.
- Conntrack stickiness rules для стабильности flow.
### E1.4 UX-ready слой
- API предупреждений + dry-run diff.
- SSE события:
- `transport_client_state_changed`,
- `transport_policy_applied`,
- `transport_conflict_detected`.
## 10) Критерии готовности дизайна
- Можно добавить 2+ клиентов и поднять 2+ интерфейса без перезаписи чужих rule/table.
- Нельзя назначить один и тот же selector двум клиентам без explicit override.
- `traffic_audit` показывает целостную картину конфликтов и drift.
- UI получает понятные предупреждения до применения рискованной конфигурации.
## 11) Обратная совместимость
- Текущие `/api/v1/traffic/*` продолжают работать в legacy-режиме.
- При отсутствии multi-client политики ядро использует текущий single-client pipeline.
- Миграция: legacy `vpn|direct` может быть автоматически представлена как:
- `client_id=legacy-vpn`,
- `client_id=legacy-direct`.

View File

@@ -0,0 +1,602 @@
# E2 API-контракт transport control-plane
Дата: 2026-03-05
Статус: in-progress
Владелец: Engineering
## 1) Цель
- Зафиксировать стабильный `/api/v1/transport/*` контракт для управления несколькими transport-клиентами через единый Go control-plane.
- Обеспечить одинаковый API для desktop/web/iOS/android.
- Встроить anti-conflict workflow: `validate -> confirm -> apply`.
## 2) Область и ограничения
- Контракт описывает API-слой и DTO; низкоуровневый backend-runner (systemd/process supervisor) реализуется отдельно.
- Все изменения policy должны идти только через `validate` и `apply`.
- Для совместимости с текущим API проект использует паттерн:
- HTTP `200` + `"ok": false` для операционных ошибок;
- HTTP `4xx` для ошибки запроса (bad json, invalid id, missing fields).
## 3) Общие правила
### 3.1 Базовые поля ответа
```json
{
"ok": true,
"message": "ok",
"request_id": "req-01JABC...",
"data": {}
}
```
### 3.2 Ошибка доменной валидации
```json
{
"ok": false,
"message": "policy has blocking conflicts",
"code": "POLICY_CONFLICT_BLOCK",
"issues": []
}
```
### 3.3 Идемпотентность
- Для mutating POST/PATCH/DELETE клиент передаёт `Idempotency-Key`.
- Для `apply` дополнительно используется `policy_revision` (optimistic lock).
- Для `POST /api/v1/transport/policies/apply` и `POST /api/v1/transport/policies/rollback` backend хранит persisted replay-state:
- одинаковые `(scope, Idempotency-Key, request payload)` возвращают один и тот же сохранённый response без повторного runtime apply;
- повторное использование того же `Idempotency-Key` с другим payload возвращает `IDEMPOTENCY_KEY_REUSED`.
## 4) Модели данных
### 4.1 TransportClient
```json
{
"id": "phoenix-eu",
"name": "Phoenix EU",
"kind": "phoenix",
"enabled": true,
"status": "up",
"iface": "phx0",
"routing_table": "agvpn_phoenix_eu",
"mark_hex": "0x110",
"priority_base": 13050,
"capabilities": ["tcp", "udp", "ssh_tunnel"],
"health": {
"last_check": "2026-03-05T10:11:12Z",
"latency_ms": 83,
"last_error": ""
},
"config": {
"runtime_mode": "exec",
"runner": "systemd",
"endpoint": "eu.example.net:443",
"profile": "default"
},
"updated_at": "2026-03-05T10:11:12Z"
}
```
### 4.2 RouteIntent
```json
{
"selector_type": "domain",
"selector_value": "youtube.com",
"client_id": "phoenix-eu",
"priority": 100,
"mode": "strict"
}
```
### 4.3 ConflictRecord
```json
{
"key": "domain:youtube.com",
"type": "ownership",
"severity": "block",
"owners": ["phoenix-eu", "dnstt-home"],
"reason": "one selector is assigned to multiple clients",
"suggested_resolution": "keep only one owner or use force_override"
}
```
## 5) Endpoints: clients
### 5.1 `GET /api/v1/transport/clients`
- Назначение: список клиентов.
- Query:
- `enabled_only=true|false` (optional)
- `kind=singbox|dnstt|phoenix` (optional)
Response:
```json
{
"ok": true,
"message": "ok",
"items": [],
"count": 3
}
```
### 5.2 `POST /api/v1/transport/clients`
- Назначение: создать клиента.
Request:
```json
{
"id": "dnstt-home",
"name": "DNSTT Home",
"kind": "dnstt",
"enabled": true,
"config": {
"runtime_mode": "exec",
"runner": "systemd",
"packaging_profile": "bundled",
"bin_root": "/opt/selective-vpn/bin",
"server": "1.2.3.4:443",
"domain": "tunnel.example.org",
"pubkey": "base64..."
}
}
```
Response:
```json
{
"ok": true,
"message": "client created",
"item": {}
}
```
### 5.3 `GET /api/v1/transport/clients/{id}`
- Назначение: получить детальную карточку клиента.
### 5.4 `PATCH /api/v1/transport/clients/{id}`
- Назначение: частичное обновление метаданных/конфига.
- Поддерживаемые поля: `name`, `enabled`, `config`.
### 5.5 `DELETE /api/v1/transport/clients/{id}`
- Назначение: удалить клиента.
- Правило: удаление запрещено, если есть активные policy-ссылки без `force=true`.
### 5.6 `POST /api/v1/transport/clients/{id}/start`
### 5.7 `POST /api/v1/transport/clients/{id}/stop`
### 5.8 `POST /api/v1/transport/clients/{id}/restart`
- Назначение: lifecycle операции backend-клиента.
- Ответ: унифицированный `cmdResult`-совместимый формат + backend runtime поля (`status_before/status_after`, `runtime.metrics`, `runtime.last_error`).
Пример:
```json
{
"ok": true,
"message": "start done",
"exitCode": 0,
"client_id": "phoenix-eu",
"kind": "phoenix",
"action": "start",
"status_before": "down",
"status_after": "up",
"health": { "last_check": "2026-03-07T10:11:12Z", "latency_ms": 83, "last_error": "" },
"runtime": {
"backend": "phoenix",
"allowed_actions": ["start", "stop", "restart"],
"metrics": { "restarts": 1, "state_changes": 2, "uptime_sec": 17 }
}
}
```
### 5.9 `GET /api/v1/transport/clients/{id}/health`
- Назначение: быстрый probe статуса и деградации.
Response:
```json
{
"ok": true,
"message": "ok",
"code": "TRANSPORT_CLIENT_DEGRADED",
"client_id": "phoenix-eu",
"kind": "phoenix",
"status": "degraded",
"latency_ms": 480,
"last_error": "upstream timeout",
"health": {
"last_check": "2026-03-07T10:11:12Z",
"latency_ms": 480,
"last_error": "upstream timeout"
},
"runtime": {
"backend": "phoenix",
"metrics": { "restarts": 1, "state_changes": 4, "uptime_sec": 0 },
"last_error": {
"code": "BACKEND_RUNTIME_ERROR",
"message": "upstream timeout",
"retryable": true
}
}
}
```
### 5.10 `GET /api/v1/transport/clients/{id}/metrics`
- Назначение: read-only срез lifecycle metrics для UI (desktop/web/iOS/android) без знания backend-внутренностей.
Response:
```json
{
"ok": true,
"message": "ok",
"client_id": "phoenix-eu",
"kind": "phoenix",
"status": "up",
"metrics": {
"restarts": 2,
"state_changes": 8,
"uptime_sec": 341,
"last_transition_at": "2026-03-07T10:11:12Z"
},
"runtime": {
"backend": "phoenix",
"last_action": "restart",
"last_action_at": "2026-03-07T10:11:12Z"
}
}
```
### 5.11 `POST /api/v1/transport/clients/{id}/provision`
- Назначение: backend-side provision (создание/обновление unit/runner-конфигурации) перед lifecycle-операциями.
- Для `runner=systemd` пишет unit-файлы и делает `systemctl daemon-reload`.
### 5.12 `GET /api/v1/transport/runtime/observability`
- Назначение: unified multi-interface runtime snapshot для карточек/дашбордов без ручной склейки `interfaces + clients + egress + policy`.
- Источники:
- `transport-interfaces`,
- `transport-clients`,
- compile-plan policy,
- `egress identity` для active client на интерфейсе.
Response:
```json
{
"ok": true,
"message": "ok",
"generated_at": "2026-03-16T12:10:00Z",
"count": 2,
"items": [
{
"iface_id": "edge-a",
"name": "Edge A",
"mode": "dedicated",
"runtime_iface": "tun-edge",
"active_iface": "tun-edge0",
"netns_name": "svpn-edge-a",
"routing_table": "agvpn_if_edge_a",
"client_id": "sb-main",
"client_ids": ["sb-main", "dnstt-fallback"],
"status": "degraded",
"latency_ms": 81,
"last_error": "fallback probe failed",
"last_check": "2026-03-16T12:09:30Z",
"egress": {
"scope": "transport:sb-main",
"source": "transport",
"source_id": "sb-main",
"ip": "203.0.113.10",
"country_code": "SG",
"country_name": "Singapore",
"stale": false
},
"counters": {
"client_count": 2,
"enabled_count": 2,
"up_count": 1,
"degraded_count": 1,
"rule_count": 4
},
"engine_counts": [
{ "kind": "dnstt", "count": 1, "degraded_count": 1 },
{ "kind": "singbox", "count": 1, "up_count": 1 }
]
}
]
}
```
## 6) Endpoints: policies
### 6.1 `GET /api/v1/transport/policies`
- Назначение: получить текущую политику и ревизию.
Response:
```json
{
"ok": true,
"message": "ok",
"policy_revision": 12,
"intents": []
}
```
### 6.2 `POST /api/v1/transport/policies/validate`
- Назначение: dry-run валидация без применения.
Request:
```json
{
"base_revision": 12,
"intents": [],
"options": {
"allow_warnings": true,
"force_override": false
}
}
```
Response:
```json
{
"ok": true,
"message": "validation complete",
"valid": false,
"summary": {
"block_count": 1,
"warn_count": 2
},
"conflicts": [],
"diff": {
"added": 10,
"changed": 3,
"removed": 1
}
}
```
### 6.3 `POST /api/v1/transport/policies/apply`
- Назначение: атомарное применение новой policy.
- Обязательные условия:
- `base_revision` совпадает с текущей ревизией,
- нет blocking-конфликтов или задан `force_override=true` с подтверждением.
Request:
```json
{
"base_revision": 12,
"intents": [],
"options": {
"force_override": true,
"confirm_token": "cnf-01JABC..."
}
}
```
Response:
```json
{
"ok": true,
"message": "policy applied",
"policy_revision": 13,
"apply_id": "apl-01JABC...",
"rollback_available": true
}
```
Ошибка конкурентного изменения:
```json
{
"ok": false,
"message": "stale policy revision",
"code": "POLICY_REVISION_MISMATCH",
"current_revision": 13
}
```
### 6.4 `POST /api/v1/transport/policies/rollback`
- Назначение: откатить policy к предыдущему snapshot.
- Условия:
- snapshot должен существовать,
- `base_revision` (если задан) должен совпадать с текущей ревизией,
- snapshot проходит текущую валидацию конфликтов.
Request:
```json
{
"base_revision": 13
}
```
Response:
```json
{
"ok": true,
"message": "policy rollback applied",
"policy_revision": 14,
"apply_id": "rbk-01JABC...",
"rollback_available": true
}
```
### 6.5 `GET /api/v1/transport/conflicts`
- Назначение: получить актуальные конфликты активной конфигурации.
Response:
```json
{
"ok": true,
"message": "ok",
"items": [],
"has_blocking": true
}
```
### 6.6 `GET /api/v1/transport/capabilities`
- Назначение: матрица возможностей backend-клиентов и текущей платформы.
Response:
```json
{
"ok": true,
"message": "ok",
"clients": {
"singbox": { "tcp": true, "udp": true, "dns_tunnel": true, "ssh_tunnel": false },
"dnstt": { "tcp": true, "udp": false, "dns_tunnel": true, "ssh_tunnel": true },
"phoenix": { "tcp": true, "udp": true, "dns_tunnel": false, "ssh_tunnel": true }
},
"runtime_modes": {
"exec": true,
"embedded": false,
"sidecar": false
},
"packaging_profiles": {
"system": true,
"bundled": true
},
"lifecycle": ["provision", "start", "stop", "restart"],
"health_fields": ["status", "latency_ms", "last_error", "health.last_check"],
"metrics_fields": ["restarts", "state_changes", "uptime_sec", "last_transition_at"],
"error_codes": [
"TRANSPORT_CLIENT_NOT_FOUND",
"TRANSPORT_CLIENT_SAVE_FAILED",
"TRANSPORT_CLIENT_DEGRADED",
"BACKEND_RUNTIME_ERROR",
"TRANSPORT_BACKEND_UNIT_REQUIRED",
"TRANSPORT_BACKEND_ACTION_FAILED",
"TRANSPORT_BACKEND_HEALTH_FAILED",
"TRANSPORT_BACKEND_PROVISION_CONFIG_REQUIRED",
"TRANSPORT_BACKEND_PROVISION_FAILED",
"TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED"
]
}
```
Примечание по `config.runtime_mode`:
- `exec` — текущий production режим (внешний companion-бинарь под управлением backend-адаптера);
- `embedded`, `sidecar` — зарезервированы для следующих фаз; при попытке lifecycle/provision сейчас возвращается `TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED`;
- alias `external|companion` нормализуются в `exec`.
Примечание по packaging для `runtime_mode=exec`:
- `packaging_profile=system` (default): поиск бинарей в системных путях (`/usr/bin`, `/usr/local/bin`, `$PATH`);
- `packaging_profile=bundled`: поиск в `bin_root` (default `/opt/selective-vpn/bin`) с опциональным fallback в system (`packaging_system_fallback=true`);
- `require_binary=true`: fail-fast на этапе `provision`/template build, если целевой бинарь не найден;
- для ручного override (`singbox_bin`, `dnstt_bin`, `phoenix_bin`) `require_binary=true` также валидирует существование.
- manual updater/rollback MVP:
- `scripts/transport-packaging/update.sh` читает pinned manifest, проверяет `sha256`, устанавливает release и атомарно переключает symlink;
- `update.sh` поддерживает trusted-source policy (`--source-policy`), optional/required detached signature verify (`signature.type=openssl-sha256`) и staged rollout (`rollout.stage/percent`, `--rollout-stage`, `--cohort-id`);
- `scripts/transport-packaging/auto_update.sh` — opt-in scheduler-wrapper (`enabled=true`) с interval gate/lock/jitter для безопасного фонового запуска;
- `scripts/transport-packaging/rollback.sh` откатывает компонент на предыдущую запись в `BIN_ROOT/.packaging/*.history`.
Примечание для `dnstt`:
- При `runner=systemd` допускается единая оркестрация `dnstt + ssh overlay`:
- `unit`: systemd unit DNSTT-клиента;
- `exec_start`: явный override команды запуска DNSTT-клиента (опционально);
- если `exec_start` не задан, Go-ядро строит команду по шаблону из полей:
- resolver: `resolver_mode=doh|dot|udp` + `doh_url|dot_addr|udp_addr|resolver_addr`,
- ключ: `pubkey` или `pubkey_file`,
- endpoint: `domain` + `local_addr` (default `127.0.0.1:7000`);
- `ssh_tunnel` или `ssh_overlay`: `true`;
- `ssh_unit`: systemd unit SSH-туннеля;
- `ssh_exec_start` (или `ssh_host` + `ssh_user` + `ssh_port` + `socks_port`): команда запуска SSH overlay.
Примечание для `singbox` и `phoenix`:
- при `runner=systemd` `exec_start` также опционален;
- при отсутствии `exec_start` команда строится шаблонами ядра:
- `singbox`: `<bin> run -c <config_path>`;
- `phoenix`: `<bin> -config <config_path>`.
Примечание для `runner=systemd` (общий tuning):
- `restart_policy`: `no|on-success|on-failure|on-abnormal|on-watchdog|on-abort|always` (default `always`);
- `restart_sec`: задержка перезапуска в секундах (default `2`);
- `start_limit_interval_sec`, `start_limit_burst`: анти-flap лимиты unit (defaults `300`, `30`);
- `timeout_start_sec`, `timeout_stop_sec`: таймауты старта/остановки (defaults `90`, `20`);
- `watchdog_sec`: опциональный systemd watchdog (default `0`, отключён);
- для `dnstt + ssh overlay` поддержаны `ssh_*` overrides тех же ключей (`ssh_restart_sec`, `ssh_watchdog_sec` и т.д.) для отдельного tuning SSH unit.
Примечание для `runner=systemd` (unit hardening):
- `hardening_profile`: `baseline|strict|off` (default `baseline`);
- `hardening_enabled`: `true|false` (может принудительно включить/выключить hardening);
- baseline-профиль включает:
- `NoNewPrivileges=yes`, `PrivateTmp=yes`,
- `ProtectSystem=full`, `ProtectHome=read-only`,
- `ProtectControlGroups=yes`, `ProtectKernelModules=yes`, `ProtectKernelTunables=yes`,
- `RestrictSUIDSGID=yes`, `LockPersonality=yes`, `UMask=0077`;
- strict-профиль дополнительно включает `ProtectSystem=strict` и `PrivateDevices=yes`;
- тонкие override-ключи:
- `no_new_privileges`, `private_tmp`, `protect_system`, `protect_home`,
- `protect_control_groups`, `protect_kernel_modules`, `protect_kernel_tunables`,
- `restrict_suid_sgid`, `lock_personality`, `private_devices`, `umask`;
- для overlay-пары поддержаны `ssh_*` overrides этих же hardening-ключей (например `ssh_hardening_enabled`, `ssh_protect_system`, `ssh_umask`).
## 7) События SSE (проект)
- `transport_client_state_changed`
- `{"id":"phoenix-eu","from":"starting","to":"up"}`
- `transport_client_provisioned`
- `{"id":"dnstt-home","ok":true,"msg":"provision done"}`
- `transport_policy_validated`
- `{"valid":false,"block_count":1,"warn_count":2}`
- `transport_policy_applied`
- `{"apply_id":"apl-...","policy_revision":13}`
- `transport_runtime_snapshot_changed`
- `{"reason":"transport_client_state_changed","generated_at":"2026-03-16T12:10:00Z","client_ids":["sb-main"],"iface_ids":["edge-a"],"items":[...]}`
- payload переиспользует тот же DTO, что и `GET /api/v1/transport/runtime/observability`, чтобы UI мог либо сделать re-fetch, либо обновиться напрямую без ручной агрегации.
- `transport_conflict_detected`
- `{"key":"domain:youtube.com","severity":"block"}`
## 8) Правила anti-conflict
- Ownership lock:
- один `selector_type + selector_value` принадлежит только одному `client_id`.
- По умолчанию конфликты `severity=block` блокируют `apply`.
- `force_override` разрешен только с `confirm_token`, полученным на этапе `validate`.
- Для UX предупреждений backend возвращает:
- список конфликтов,
- потенциальный impact (`flows_rebind_required`, `session_drop_risk`),
- diff по изменениям политики.
## 9) Безопасность и аудит
- Все mutating endpoints требуют `Authorization` + RBAC scope `transport:write`.
- Для операций `apply`, `delete`, `force_override` обязателен audit record:
- user id,
- request id,
- previous revision,
- new revision,
- short diff summary.
## 10) Минимальный план внедрения
- E2.1: ввести DTO и read-only endpoints (`GET clients`, `GET policies`, `GET capabilities`).
- E2.2: добавить `validate` с ownership/overlap анализом.
- E2.3: добавить `apply` с optimistic lock + rollback snapshot.
- E2.4: подключить SSE события и UI flow подтверждения.
## 11) Статус реализации в коде (2026-03-07)
- Реализовано в `selective-vpn-api/app/transport_handlers.go`:
- `GET/POST /api/v1/transport/clients`
- `GET/PATCH/DELETE /api/v1/transport/clients/{id}`
- `POST /api/v1/transport/clients/{id}/provision`
- `POST /api/v1/transport/clients/{id}/{start|stop|restart}`
- `GET /api/v1/transport/clients/{id}/health`
- `GET /api/v1/transport/clients/{id}/metrics`
- `GET /api/v1/transport/policies`
- `POST /api/v1/transport/policies/validate`
- `POST /api/v1/transport/policies/apply`
- `POST /api/v1/transport/policies/rollback`
- `GET /api/v1/transport/conflicts`
- `GET /api/v1/transport/capabilities`
- D4.1-контракт в Go:
- унифицированные DTO для lifecycle/health/metrics/errors,
- runtime-срез в `TransportClient` (`backend`, `allowed_actions`, counters, `last_error`),
- method-level ответы с кодами ошибок (`TRANSPORT_CLIENT_*`, `BACKEND_RUNTIME_ERROR`).
- D4.2 foundation в Go:
- backend-адаптеры `mock/systemd` с выбором по `client.config.runner`,
- для `dnstt` поддержан режим dual-unit orchestration (`ssh overlay`) в `provision/lifecycle/health`,
- шаблонный build `exec_start` в Go для `singbox|dnstt|phoenix` (с manual override через `config.exec_start`),
- systemd tuning для restart/start-limit/timeout/watchdog с отдельными `ssh_*` override для overlay unit,
- unit hardening профили (`baseline/strict/off`) и `ssh_*` hardening overrides для overlay unit.
- Валидация конфликтов:
- ownership conflict (`selector` на несколько клиентов),
- overlap CIDR между разными клиентами,
- unknown client / invalid selector.
- Apply flow:
- `base_revision` lock,
- `confirm_token` при `force_override`,
- snapshot предыдущей policy (`transport-policies.prev.json`),
- SSE события `transport_policy_validated`, `transport_policy_applied`, `transport_conflict_detected`.
- Allocator policy v2:
- резервные диапазоны для `mark_hex` и `priority_base`,
- детерминированное восстановление слотов при загрузке state,
- auto re-balance при коллизиях/битых слотах в `transport-clients.json`,
- детерминированная генерация уникальных `routing_table` (с защитой от коллизий длинных ID).

View File

@@ -0,0 +1,91 @@
# E3 План реализации мультиинтерфейса (execution roadmap)
Дата: 2026-03-15
Статус: in-progress
Владелец: Engineering
## 1) Цель
- Реализовать мультиинтерфейсную архитектуру для transport-клиентов (`singbox`, далее `dnstt`, `phoenix`) без конфликтов маршрутизации.
- Сохранить инвариант: вся логика маршрутизации живёт в Go-ядре, GUI/Web/Mobile остаются тонкими клиентами API.
- Добавить безопасный path миграции без поломки текущего single-interface контура.
## 2) Инварианты реализации
- Никаких прямых мутаций `ip rule`/`ip route`/`nft` из UI.
- Один destination/intent может иметь только одного owner (до явного override).
- Сначала foundation/state/contract, потом orchestration data-plane.
- Каждый этап обратим (rollback), каждый этап проверяется `go test ./...`.
## 3) Фазы
### M1. Foundation интерфейсов (без изменения data-plane)
- Добавить логический `iface_id` в `TransportClient` (default: `shared`).
- Добавить state-файл интерфейсов (`transport-interfaces.json`) и нормализацию.
- Добавить read-only endpoint `GET /api/v1/transport/interfaces`.
- Обновить трекер и тесты миграции state.
- Критерий: поведение runtime не меняется, старые профили продолжают работать.
### M2. Interface Orchestrator core (E3.3)
- Ввести оркестратор `create/bind/start/stop/cleanup` по `iface_id`.
- Разделить "логический интерфейс" (`iface_id`) и "runtime iface" (`tunX/dev`) с явным mapping.
- Добавить lock-стратегию на уровне `iface_id`, чтобы исключить race между клиентами.
- Критерий: один API-path для оркестрации всех движков, без дублирования per-client логики.
### M3. Policy compiler per-interface
- Компилировать intents в наборы правил per `iface_id`: table/mark/pref/nft sets.
- Гарантировать непересекаемые allocator-пулы для разных интерфейсов.
- Подготовить атомарный apply-plan для группы интерфейсов.
- Критерий: отдельные интерфейсы не перетирают таблицы/правила друг друга.
### M4. Anti-mixing и ownership guardrails (E3.4/E3.5)
- Strict ownership registry (`domain/cidr/app`) с явным conflict reason.
- Destination stickiness (`conntrack mark` + owner lock).
- Predictable override-flow с подтверждением.
- Критерий: один destination не может "гулять" между двумя интерфейсами без явного switch.
### M5. Transaction pipeline (E3.6)
- Расширить apply до `validate -> plan -> confirm -> apply -> health-check -> commit`.
- На любой ошибке health-check выполнять auto-rollback на previous snapshot.
- Добавить idempotency/optimistic lock для multi-interface apply.
- Критерий: частично применённой политики не остаётся.
### M6. Unified observability API (E6.6)
- Добавить runtime endpoint для карточек/дашбордов:
- `active_iface`,
- `egress` (ip/country),
- `latency`,
- `last_error`,
- counters per engine/policy.
- Вынести метрики в единый DTO для GUI/Web/Mobile.
- Критерий: UI не склеивает статус из нескольких endpoint-ов вручную.
### M7. UI/Web адаптация после backend-ready
- Desktop: переключение iface/client через новый orchestration API.
- Web/Mobile: reuse того же backend-контракта без новой бизнес-логики.
- Добавить feature-flag/compat-mode для плавной миграции.
- Критерий: backend-контракт единый для всех фронтов.
- Текущий дизайн desktop-first зафиксирован: `docs/phase-e/E4_2_MULTI_INTERFACE_GUI_DESIGN.md`.
## 4) Что делаем прямо сейчас
- M1 завершён:
- `iface_id` + `transport-interfaces` state + `GET /transport/interfaces` + тесты.
- M2 завершён:
- добавлен per-`iface_id` lock manager для mutating lifecycle/provision (`start/stop/restart/provision`);
- добавлен mapping-layer `iface_id -> runtime_iface/netns/routing_table` (dedicated iface defaults + interface hints + apply на create/patch/lifecycle/netns-toggle/provision);
- закрыт owner-scope compile этап: `nft_set` генерируется в scope `iface+client+selector` (без shared-set mixing на одном `iface_id`).
- M3 завершён (foundation):
- добавлен compile-plan `iface_id -> table/mark/pref/nft sets` с persisted state (`transport-policies.plan.json`) и возвратом в `validate/apply/rollback/get-policy`;
- добавлен atomic apply executor foundation (`transport-policies.runtime.json` + runtime snapshot/restore) и врезан в `apply/rollback` до commit policy revision;
- подключён kernel stage в executor: per-interface CIDR nft sets apply/cleanup + optional `ip rule` stage под feature-flag;
- ownership foundation (M4-start): добавлен persisted registry `transport-ownership.json` + `GET /api/v1/transport/owners`;
- apply guardrails усилены: `force_override` допускается только для `owner_switch`, hard blocks не bypass-ятся override-флагом;
- anti-mixing foundation (M4-start): owner switch теперь блокируется runtime owner-lock (если previous owner в статусе `up|starting|degraded`);
- ownership observability: `GET /api/v1/transport/owners` аннотирует записи `owner_status/lock_active`, возвращает агрегат `lock_count` для UI/Web;
- conntrack stickiness foundation:
- kernel-stage (feature-flag `SVPN_TRANSPORT_POLICY_CONNTRACK_STICKY=1`) собирает destination-lock state из `conntrack -L -f ipv4` по `mark -> owner`;
- persisted state: `transport-owner-locks.json`;
- read-only endpoint: `GET /api/v1/transport/owner-locks`;
- validate/apply добавляют `destination_lock` block для `cidr` owner-switch, если destination ещё sticky-locked на предыдущего owner.
- owner-lock recovery:
- endpoint `POST /api/v1/transport/owner-locks/clear` (point clear by `client_id` and/or `destination_ip(s)`);
- двухшаговый confirm flow (`confirm_token`) для предотвращения случайного lock-loss.
- следующий шаг: hardening kernel stage (расширение selector coverage, guardrails/observability) + M4 stickiness (`conntrack owner lock`).

View File

@@ -0,0 +1,101 @@
# E4.4 Multi-Interface GUI Design (Desktop-first)
Дата: 2026-03-15
Статус: planned (design approved)
Владелец: Engineering
## 1) Ответ на ключевой вопрос
- Да, это общий мультиинтерфейсный контур на всё приложение.
- Источник истины: Go-ядро (`transport interfaces/policies/owners/owner-locks`).
- GUI/Web/Mobile только рисуют состояние и вызывают API.
## 2) Границы
- `AdGuardVPN` остаётся отдельным engine-контуром (autoloop/tun0) и не смешивается с transport policy ownership.
- Мультиинтерфейс (`iface_id`, `routing_table`, owner-locks) относится к transport-движкам (`singbox`, далее `dnstt`, `phoenix`).
- Дизайн делаем универсальным, без жёсткой привязки к одному протоколу.
## 3) UI-композиция (вкладка/модуль Transport)
### A. Interface Summary (верхний блок)
- Карточки по `iface_id`:
- `iface_id`,
- `routing_table`,
- количество клиентов на интерфейсе,
- количество rule/intents.
- Источники:
- `GET /api/v1/transport/interfaces`,
- `GET /api/v1/transport/policies`.
### B. Clients Grid (карточки подключений)
- Карточки клиентов остаются, но группируются по `iface_id`.
- На карточке:
- `client name/id`,
- `protocol/transport/security`,
- `status/latency`,
- `egress ip/country` (если доступно).
- Источник:
- `GET /api/v1/transport/clients`,
- `GET /api/v1/egress/identity?scope=transport:<client_id>`.
### C. Ownership & Locks Panel (новый блок)
- Вкладки внутри панели:
- `Ownership`:
- данные из `GET /api/v1/transport/owners`,
- поля: selector, owner client, iface, `owner_status`, `lock_active`.
- `Destination locks`:
- данные из `GET /api/v1/transport/owner-locks`,
- поля: destination ip, owner client, mark, proto, updated_at.
- Назначение:
- быстро понять, почему блокируется owner-switch.
### D. Safe Lock Recovery (точечная очистка lock)
- Кнопка: `Clear selected lock(s)` в `Destination locks`.
- Только точечный режим:
- по `client_id`,
- по `destination_ip`/`destination_ips`.
- Полный clear без фильтра запрещён.
## 4) UX-flow clear owner-lock (двухшаговый)
1. Пользователь выбирает фильтры и нажимает `Clear`.
2. GUI вызывает `POST /api/v1/transport/owner-locks/clear` без `confirm_token`.
3. Backend отвечает:
- `OWNER_LOCK_CLEAR_CONFIRM_REQUIRED`,
- `confirm_token`,
- список matched lock.
4. GUI показывает confirm-диалог с последствиями (что удалится).
5. При подтверждении GUI повторяет запрос с `confirm_token`.
6. Успех:
- перечитать `owner-locks`,
- обновить `owners`,
- показать `cleared_count`.
## 5) Правила безопасности UI
- Нельзя отправить clear-запрос без фильтра.
- Нельзя кешировать `confirm_token` между сессиями.
- При `*_REVISION_MISMATCH` GUI обязан перечитать `owner-locks` и повторить выбор.
- Все mutating-кнопки блокируются на время in-flight запроса.
## 6) События/обновление данных
- В приоритете SSE refresh от уже существующих transport-событий.
- На `transport_policy_applied`:
- перечитать `owners`,
- если включён sticky-режим, перечитать `owner-locks`.
- После clear:
- локально optimistic update запрещён, только re-fetch из API.
## 7) Контракт API для GUI (фикс)
- `GET /api/v1/transport/interfaces`
- `GET /api/v1/transport/clients`
- `GET /api/v1/transport/policies`
- `GET /api/v1/transport/owners`
- `GET /api/v1/transport/owner-locks`
- `POST /api/v1/transport/owner-locks/clear`
- `POST /api/v1/transport/policies/validate`
- `POST /api/v1/transport/policies/apply`
## 8) Порядок внедрения GUI
1. Добавить read-only `Ownership & Locks Panel`.
2. Подключить фильтры и таблицу `Destination locks`.
3. Подключить двухшаговый clear-flow с confirm-диалогом.
4. Встроить обновление после validate/apply и transport refresh.
5. После desktop-стабилизации переиспользовать UI-контракт в web/mobile.

View File

@@ -0,0 +1,126 @@
# E4 UX-поток предупреждений: validate -> confirm -> apply
Дата: 2026-03-05
Статус: in-progress (E4.2 foundation реализован в GUI controller)
Владелец: Engineering
## 1) Цель
- Зафиксировать единый UX-флоу для безопасного применения multi-client policy.
- Исключить "тихие" конфликтные применения.
- Дать пользователю прозрачный diff, риски и явное подтверждение.
## 2) Базовый сценарий
1. Пользователь редактирует routing policy.
2. UI вызывает `POST /api/v1/transport/policies/validate`.
3. UI показывает результат валидации:
- `valid=true` и `block_count=0` -> можно применять.
- `valid=false` или `block_count>0` -> блокируем apply до подтверждения/исправления.
4. Если пользователь выбирает принудительное применение:
- UI показывает модал подтверждения риска,
- использует `confirm_token` из validate.
5. UI вызывает `POST /api/v1/transport/policies/apply`.
### 2.1 Сценарий Engine Switch / Connect
1. Пользователь выбирает целевой engine (`singbox|dnstt|phoenix`) или нажимает `Connect`.
2. UI формирует draft policy, где default ownership переходит к выбранному `client_id`.
3. Дальше используется тот же pipeline:
- `validate` -> (safe|risky) -> `confirm` -> `apply`.
4. После apply UI проверяет:
- `GET /api/v1/transport/clients/{id}/health`,
- расхождение `desired_engine` vs `active_engine`.
5. Если engine не поднялся, UI предлагает `rollback`.
## 3) Состояния UI
### 3.1 Draft
- Политика редактируется, но не проверена.
- Кнопка Apply отключена.
- Доступна кнопка Validate.
### 3.2 Validated (safe)
- `block_count=0`.
- Показываем diff (`added/changed/removed`).
- Apply активен без force режима.
### 3.3 Validated (risky)
- Есть блокирующие конфликты (`ownership`, `cidr_overlap`, `unknown_client`).
- Показываем список конфликтов и конкретные селекторы.
- Обычный Apply отключен.
- Доступен `Force apply` только через отдельный confirm-step.
### 3.4 Confirm required
- Модал с явным предупреждением:
- что будет перезаписано,
- какие flow могут быть прерваны,
- какие сайты/сети сменят client owner.
- Кнопка подтверждения вызывает `apply` с `force_override=true` + `confirm_token`.
### 3.5 Applied
- Показываем `policy_revision` и `apply_id`.
- Обновляем текущую policy в UI.
- Слушаем SSE `transport_policy_applied`.
### 3.6 Switching engine
- Идёт переключение активного engine.
- Кнопки mutating-действий блокируются до завершения.
- Отображается прогресс: `Validating`, `Applying`, `Waiting for health`.
### 3.7 Switch failed
- `apply` или `health` завершились ошибкой.
- Показываем `last_error` активного клиента и причину валидации/применения.
- Предлагаем быстрые действия:
- `Rollback`,
- `Switch back to previous engine`.
## 4) Тексты предупреждений (шаблоны)
- `ownership`:
- "Один и тот же селектор назначен разным клиентам. Это может вызвать нестабильную маршрутизацию."
- `cidr_overlap`:
- "CIDR-подсети пересекаются между клиентами. Пакеты могут идти не по ожидаемому интерфейсу."
- `unknown_client`:
- "Политика ссылается на несуществующий клиент. Сначала добавьте/включите клиент."
Force confirm warning:
- "Принудительное применение может вызвать кратковременный обрыв активных сессий и смену маршрута для части трафика."
## 5) UX-правила безопасности
- Без `validate` кнопка `apply` неактивна.
- `confirm_token` не хранится между сессиями UI и считается одноразовым.
- При смене `policy_revision` в фоне UI обязан повторно выполнить validate.
- При `POLICY_REVISION_MISMATCH` UI показывает "Конфигурация изменилась, нужно повторить проверку".
## 6) Web/iOS/Android паритет
- Один и тот же флоу и тексты рисков для всех клиентов.
- Разница только в визуальном представлении:
- web: side panel + modal,
- mobile: full-screen sheet.
- Логика decision-state остается одинаковой:
- draft -> validate -> (safe|risky) -> confirm -> apply.
## 7) Минимальный UI-чеклист внедрения
- Отображение `summary.block_count/warn_count`.
- Таблица `conflicts[]` с фильтром по severity/type.
- Видимый diff `added/changed/removed`.
- Модал force confirmation с явным перечислением рисков.
- Бейдж текущей ревизии policy (`policy_revision`).
- SSE-подписка на `transport_policy_validated`, `transport_policy_applied`, `transport_conflict_detected`.
- Для engine UX:
- индикатор `desired_engine / active_engine`,
- кнопки `Connect`/`Switch`,
- блокировка повторного switch, пока предыдущий не завершён,
- action `Rollback to previous engine` при неуспехе.
## 8) Статус внедрения (2026-03-07)
- E4.2 foundation в GUI controller реализован: `draft -> validate -> confirm -> apply`.
- E4.3.1 foundation в GUI реализован:
- на вкладке `AdGuardVPN` был добавлен foundation-блок `Transport engine`;
- доступен выбор client и действия `Prepare/Connect/Disconnect/Restart` через API `/api/v1/transport/clients/{id}/*`;
- отображается runtime-состояние выбранного engine (`status/iface/table/latency/last_error`);
- refresh блока привязан к transport SSE-событиям.
- E4.3.2 реализован:
- engine-блок вынесен в отдельную вкладку `SingBox`;
- `Connect/Switch` переведён на pipeline `validate -> confirm -> apply`, direct `start` для switch больше не используется;
- добавлен `Rollback policy` button.
- Следующий UX-этап:
- `desired_engine/active_engine` индикаторы и блокировка повторного switch;
- settings-переключатель видимости protocol tabs (`SingBox/DNSTT/Phoenix`).

View File

@@ -0,0 +1,75 @@
# E5.2 SingBox Desktop Dashboard Spec
Дата: 2026-03-07
Статус: in-progress (foundation implemented)
Владелец: Engineering
## 1) Цель
- Зафиксировать дизайн вкладки `SingBox` для desktop, чтобы не смешивать runtime-управление и конфигурацию профилей.
- Подготовить структуру, которую позже можно переиспользовать в web/mobile.
## 2) Структура вкладки (fixed)
- Визуальная модель: `card-based dashboard` (верхние metric cards + grid profile cards + control panels).
### A. Connection card (runtime)
- Показывает только текущее состояние подключения:
- runtime status,
- active profile,
- protocol/transport,
- routing/dns/kill-switch effective state,
- last update timestamp.
- Быстрые действия:
- `Prepare`,
- `Connect/Switch`,
- `Disconnect`,
- `Restart`,
- `Rollback policy`.
### B. Profile settings (per profile)
- Настройки конкретного профиля:
- `Routing mode`,
- `DNS mode`,
- `Kill-switch`.
- Для каждого блока поддерживается `Use global ...` (наследование).
- Действия профиля:
- `Save draft`,
- `Validate profile`,
- `Apply profile`.
Примечание этапа:
- `Validate/Apply profile` через `/api/v1/transport/singbox/profiles/*` будут полноценно подключены на шагах `E5.2/E5.3`.
- На foundation-этапе эти кнопки логируют намерение и не ломают runtime-flow.
### C. Global defaults
- Общие дефолты для всех профилей:
- default routing mode,
- default DNS mode,
- default kill-switch.
- Сохранение настроек и применение в effective-резолюции профиля.
## 3) Правило приоритета (обязательное)
- `Profile override` > `Global default`.
- Если в профиле включено `Use global ...`, используется глобальное значение.
- Runtime card всегда показывает итоговое effective состояние.
## 4) Границы ответственности
- Вкладка `SingBox` управляет только `SingBox` профилями/engine.
- `DNSTT/Phoenix` в этом этапе не добавляются во вкладку (backend-ready трек отдельно).
- `Routing policy` ownership/anti-conflict остаётся в Go API pipeline `validate -> confirm -> apply`.
## 5) Что уже реализовано в GUI foundation
- Перестроена вкладка на 3 секции: runtime card, profile settings, global defaults.
- Добавлен card-based UI слой:
- верхний ряд compact metric cards,
- кликабельный grid connection profile cards (выбор карточки синхронизирован с active engine selector).
- Реализован compact-mode для настроек:
- `Runtime details`, `Profile settings`, `Global defaults`, `Activity log` открываются кнопками, по умолчанию скрыты.
- Добавлены `Use global` переключатели и effective summary.
- Добавлено локальное сохранение настроек (`QSettings`) для global/profile режимов.
- Сохранены рабочие runtime-кнопки `Prepare/Connect-Switch/Disconnect/Restart/Rollback`.
## 6) Следующий технический шаг
- Подключить профильные кнопки `Validate/Apply` к реальному Go API:
- `POST /api/v1/transport/singbox/profiles/{id}/validate`,
- `POST /api/v1/transport/singbox/profiles/{id}/apply`,
- с обработкой `base_revision`, ошибок и rollback-подсказок в UI.

View File

@@ -0,0 +1,101 @@
# E5 SingBox Client Form Matrix (UI-first, VLESS baseline)
Дата: 2026-03-09
Статус: active
Владелец: Engineering
## 1) Что берём из твоего примера
- Берём структуру "блоками", как на скрине:
- базовый блок,
- transport,
- security,
- sniffing,
- advanced toggles.
- Не копируем серверные поля биллинга/лимитов/истечения, потому что это не клиентский outbound.
## 2) Что исключаем (server-only)
- `Email`, `Subscription`, `Total used`, `Traffic reset`, `Expire date`, `Fallbacks` (как серверный inbound list), и прочие учёт/статистика-поля.
## 3) Клиентская форма (VLESS + Reality) — целевая MVP
### 3.1 Block A: Profile
| UI field | JSON path | Required | Notes |
|---|---|---|---|
| Profile name | `profile.name` | yes | имя карточки |
| Enabled | `profile.enabled` | yes | локальный toggle |
| Protocol | `outbound.type` | yes | для этого шаблона фикс `vless` |
### 3.2 Block B: Server/Auth
| UI field | JSON path | Required | Notes |
|---|---|---|---|
| Address | `outbound.server` | yes | домен/IP |
| Port | `outbound.server_port` | yes | `1..65535` |
| UUID | `outbound.uuid` | yes | vless user id |
| Flow | `outbound.flow` | no | напр. `xtls-rprx-vision` |
| Packet encoding | `outbound.packet_encoding` | no | default `none` |
### 3.3 Block C: Transport
| UI field | JSON path | Required | Notes |
|---|---|---|---|
| Transport type | `outbound.transport.type` | no | `tcp|ws|grpc|http|httpupgrade|quic` |
| Path | `outbound.transport.path` | depends | для `ws/http/httpupgrade` |
| Host/Headers | `outbound.transport.headers` | no | advanced |
| Service name | `outbound.transport.service_name` | depends | для `grpc` |
### 3.4 Block D: Security
| UI field | JSON path | Required | Notes |
|---|---|---|---|
| Security mode | `outbound.tls.enabled` + `outbound.tls.reality.enabled` | yes | `none|tls|reality` (segmented control) |
| SNI | `outbound.tls.server_name` | depends | для `tls/reality` |
| uTLS fingerprint | `outbound.tls.utls.fingerprint` | no | `chrome|firefox|safari|...` |
| Reality public key | `outbound.tls.reality.public_key` | depends | must для reality |
| Reality short id | `outbound.tls.reality.short_id` | no | обычно короткий hex |
| Insecure | `outbound.tls.insecure` | no | advanced toggle |
### 3.5 Block E: Sniffing/Local inbound (опционально)
| UI field | JSON path | Required | Notes |
|---|---|---|---|
| Sniffing enabled | `inbounds[*].sniff` | no | если используем локальный socks inbound в generated config |
| Sniff override destination | `inbounds[*].sniff_override_destination` | no | advanced |
### 3.6 Block F: Advanced Dial
| UI field | JSON path | Required | Notes |
|---|---|---|---|
| Network | `outbound.network` | no | `tcp|udp` |
| Connect timeout | `outbound.connect_timeout` | no | duration |
| Bind interface | `outbound.bind_interface` | no | advanced |
| Routing mark | `outbound.routing_mark` | no | advanced |
| Multiplex | `outbound.multiplex` | no | advanced group |
| UDP over TCP | `outbound.udp_over_tcp` | no | advanced group |
## 4) Guardrails (обязательные)
- `CV-001`: при `security=reality` автоматически `tls.enabled=true`, `tls.reality.enabled=true`.
- `CV-002`: при `security=reality` поле `reality.public_key` обязательно.
- `CV-003`: при `transport=grpc` поле `service_name` обязательно.
- `CV-004`: при `transport=ws|http|httpupgrade` поле `path` обязательно.
- `CV-005`: `address/port/uuid` обязательны всегда.
## 5) UI-компоновка (desktop)
- Compact panel:
- `Profile`,
- `Server/Auth`,
- `Transport`,
- `Security`,
- collapsed `Advanced`.
- Actions:
- `Preview render`,
- `Validate profile`,
- `Apply profile`,
- `History`,
- `Rollback profile`.
## 6) Расширение на другие протоколы
- Сохраняем те же блоки UI, меняются только поля блока `Server/Auth` + часть `Security/Transport`.
- То есть форма будет модульной, а не отдельное "окно под каждый протокол".

View File

@@ -0,0 +1,252 @@
{
"matrix_version": "2026-03-08",
"singbox_version_target": ">=1.12.0",
"schema": "e5.singbox.protocol.matrix.v1",
"protocols": [
{
"id": "vless",
"mode": "typed+raw",
"fields": [
{
"path": "outbound.server",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.server_port",
"type": "int",
"required": true,
"ui_level": "mvp",
"constraints": {
"min": 1,
"max": 65535
}
},
{
"path": "outbound.uuid",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.flow",
"type": "string",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.tls.reality.public_key",
"type": "string",
"required": false,
"ui_level": "mvp"
}
],
"guardrails": [
{
"id": "VLESS-001",
"condition": "outbound.tls.reality.enabled == true",
"constraint": "outbound.tls.enabled == true",
"level": "block"
}
]
},
{
"id": "trojan",
"mode": "typed+raw",
"fields": [
{
"path": "outbound.server",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.server_port",
"type": "int",
"required": true,
"ui_level": "mvp",
"constraints": {
"min": 1,
"max": 65535
}
},
{
"path": "outbound.password",
"type": "secret",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.tls.server_name",
"type": "string",
"required": false,
"ui_level": "mvp"
}
]
},
{
"id": "shadowsocks",
"mode": "typed+raw",
"fields": [
{
"path": "outbound.server",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.server_port",
"type": "int",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.method",
"type": "enum",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.password",
"type": "secret",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.plugin",
"type": "string",
"required": false,
"ui_level": "advanced"
}
]
},
{
"id": "wireguard",
"mode": "typed+raw",
"fields": [
{
"path": "outbound.server",
"type": "string",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.server_port",
"type": "int",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.local_address",
"type": "array[string]",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.private_key",
"type": "secret",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.peer_public_key",
"type": "string",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.peers",
"type": "array[object]",
"required": false,
"ui_level": "advanced"
}
]
},
{
"id": "hysteria2",
"mode": "typed+raw",
"fields": [
{
"path": "outbound.server",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.server_port",
"type": "int",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.password",
"type": "secret",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.up_mbps",
"type": "int",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.down_mbps",
"type": "int",
"required": false,
"ui_level": "mvp"
},
{
"path": "outbound.obfs",
"type": "string",
"required": false,
"ui_level": "advanced"
}
]
},
{
"id": "tuic",
"mode": "typed+raw",
"fields": [
{
"path": "outbound.server",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.server_port",
"type": "int",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.uuid",
"type": "string",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.password",
"type": "secret",
"required": true,
"ui_level": "mvp"
},
{
"path": "outbound.congestion_control",
"type": "string",
"required": false,
"ui_level": "advanced"
},
{
"path": "outbound.udp_relay_mode",
"type": "string",
"required": false,
"ui_level": "advanced"
}
]
}
]
}

View File

@@ -0,0 +1,229 @@
# E5 SingBox Protocols Matrix (Desktop-first)
Дата: 2026-03-08
Статус: active
Владелец: Engineering
Цель: зафиксировать поля подключения до реализации форм протоколов в GUI.
## 1) Источники (primary)
- Outbound overview: https://sing-box.sagernet.org/configuration/outbound/
- VLESS: https://sing-box.sagernet.org/configuration/outbound/vless/
- Trojan: https://sing-box.sagernet.org/configuration/outbound/trojan/
- Shadowsocks: https://sing-box.sagernet.org/configuration/outbound/shadowsocks/
- WireGuard: https://sing-box.sagernet.org/configuration/outbound/wireguard/
- Hysteria2: https://sing-box.sagernet.org/configuration/outbound/hysteria2/
- TUIC: https://sing-box.sagernet.org/configuration/outbound/tuic/
- Shared TLS: https://sing-box.sagernet.org/configuration/shared/tls/
- Shared V2Ray transport: https://sing-box.sagernet.org/configuration/shared/v2ray-transport/
- Shared Dial fields: https://sing-box.sagernet.org/configuration/shared/dial/
- Multiplex: https://sing-box.sagernet.org/configuration/shared/multiplex/
- UDP over TCP: https://sing-box.sagernet.org/configuration/shared/udp-over-tcp/
## 2) Фрейм реализации
- Target: `sing-box >= 1.12.x` (текущий runtime у нас `1.12.12`).
- UI-стратегия:
- `MVP`: критичные поля подключения.
- `Advanced`: дополнительные tuning-поля.
- `Raw-only`: редкие/экзотические поля, остаются в raw JSON режиме.
---
## 3) Общие блоки (для большинства outbound)
### 3.1 Базовые outbound-поля
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | enum/string | yes | MVP | фиксируется выбранным протоколом |
| `outbound.tag` | string | yes | MVP | стабильный id карточки |
| `outbound.detour` | string | no | Advanced | цепочка через другой outbound |
### 3.2 Shared Dial fields (ядро соединения)
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.server` | string | yes | MVP | hostname/IP сервера |
| `outbound.server_port` | int | yes | MVP | 1..65535 |
| `outbound.network` | enum | no | MVP | `tcp|udp` (по протоколу) |
| `outbound.connect_timeout` | duration | no | Advanced | таймаут dial |
| `outbound.tcp_fast_open` | bool | no | Advanced | TCP Fast Open |
| `outbound.tcp_multi_path` | bool | no | Advanced | MPTCP (если поддерживается) |
| `outbound.udp_fragment` | bool | no | Advanced | UDP fragmentation |
| `outbound.domain_resolver` | string/object | no | Advanced | используется с новым DNS-форматом |
| `outbound.bind_interface` | string | no | Advanced | привязка к интерфейсу |
| `outbound.routing_mark` | int | no | Advanced | fwmark для выхода |
### 3.3 Shared TLS/Reality блок (где применимо)
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.tls.enabled` | bool | depends | MVP | must be `true` для Reality |
| `outbound.tls.server_name` | string | depends | MVP | SNI |
| `outbound.tls.insecure` | bool | no | Advanced | skip verify |
| `outbound.tls.alpn` | []string | no | Advanced | ALPN list |
| `outbound.tls.utls.enabled` | bool | no | Advanced | uTLS toggle |
| `outbound.tls.utls.fingerprint` | enum/string | no | MVP | для Reality-сценариев обычно обязательно |
| `outbound.tls.reality.enabled` | bool | depends | MVP | Reality mode |
| `outbound.tls.reality.public_key` | string | depends | MVP | Reality PBK |
| `outbound.tls.reality.short_id` | string | no | MVP | Reality SID |
### 3.4 Shared V2Ray transport блок (где применимо)
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.transport.type` | enum/string | no | MVP | `ws|http|grpc|quic|httpupgrade|...` |
| `outbound.transport.path` | string | depends | MVP | чаще для `ws/http/httpupgrade` |
| `outbound.transport.host` | string/[]string | depends | Advanced | Host header/SNI-like values |
| `outbound.transport.service_name` | string | depends | MVP | обычно для `grpc` |
| `outbound.transport.headers` | object | no | Advanced | custom headers |
### 3.5 Shared Multiplex / UDP-over-TCP (где применимо)
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.multiplex` | object | no | Advanced | pooling/stream mux |
| `outbound.udp_over_tcp` | object | no | Advanced | туннелирование UDP через TCP |
---
## 4) Протокольные матрицы
## 4.1 VLESS outbound
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | const `vless` | yes | MVP | |
| `outbound.server` | string | yes | MVP | |
| `outbound.server_port` | int | yes | MVP | |
| `outbound.uuid` | string | yes | MVP | UUID |
| `outbound.flow` | string | no | MVP | напр. `xtls-rprx-vision` |
| `outbound.packet_encoding` | enum/string | no | Advanced | с `1.11` default `none` |
| `outbound.network` | enum | no | MVP | |
| `outbound.tls` | object | depends | MVP | TLS/Reality для production-сценариев |
| `outbound.transport` | object | no | MVP | V2Ray transport block |
Guardrails:
- `VLESS-001`: если `tls.reality.enabled=true` -> `tls.enabled=true`.
- `VLESS-002`: если `flow=xtls-rprx-vision` -> transport должен быть совместим с сервером и TLS включён.
## 4.2 Trojan outbound
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | const `trojan` | yes | MVP | |
| `outbound.server` | string | yes | MVP | |
| `outbound.server_port` | int | yes | MVP | |
| `outbound.password` | string | yes | MVP | secret |
| `outbound.network` | enum | no | MVP | |
| `outbound.tls` | object | yes (обычно) | MVP | TLS-блок |
| `outbound.transport` | object | no | MVP | V2Ray transport |
Guardrails:
- `TRJ-001`: пустой `password` блокирует apply.
- `TRJ-002`: без TLS для стандартного trojan-сервера помечать как risky.
## 4.3 Shadowsocks outbound
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | const `shadowsocks` | yes | MVP | |
| `outbound.server` | string | yes | MVP | |
| `outbound.server_port` | int | yes | MVP | |
| `outbound.method` | enum/string | yes | MVP | cipher method |
| `outbound.password` | string | yes | MVP | secret |
| `outbound.plugin` | string | no | Advanced | SIP003 plugin |
| `outbound.plugin_opts` | string | no | Advanced | plugin options |
| `outbound.network` | enum | no | Advanced | |
| `outbound.udp_over_tcp` | object | no | Advanced | shared UDP-over-TCP fields |
| `outbound.multiplex` | object | no | Advanced | shared multiplex fields |
Guardrails:
- `SS-001`: `method` обязателен и валидируется против поддерживаемых cipher.
- `SS-002`: если `plugin` задан, но `plugin_opts` пуст и plugin его требует -> warning/block.
## 4.4 WireGuard outbound
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | const `wireguard` | yes | MVP | |
| `outbound.server` | string | depends | MVP | при single-peer setup |
| `outbound.server_port` | int | depends | MVP | |
| `outbound.system_interface` | bool | no | Advanced | |
| `outbound.interface_name` | string | deprecated | Raw-only | deprecated с `1.11` |
| `outbound.local_address` | []string | yes | MVP | local tunnel address(es) |
| `outbound.private_key` | string | yes | MVP | secret |
| `outbound.peer_public_key` | string | depends | MVP | single-peer |
| `outbound.pre_shared_key` | string | no | Advanced | secret |
| `outbound.reserved` | []int | no | Advanced | |
| `outbound.workers` | int | no | Advanced | |
| `outbound.mtu` | int | no | Advanced | |
| `outbound.network` | enum | no | Advanced | |
| `outbound.peers` | []object | depends | Advanced | multi-peer schema |
| `outbound.peer_allowed_ips` | []string | no | Advanced | |
| `outbound.packet_encoding` | enum/string | no | Advanced | |
Guardrails:
- `WG-001`: `private_key` обязателен.
- `WG-002`: нужен или `peer_public_key` (single) или `peers[]` (multi).
## 4.5 Hysteria2 outbound
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | const `hysteria2` | yes | MVP | |
| `outbound.server` | string | yes | MVP | |
| `outbound.server_port` | int | yes | MVP | |
| `outbound.up_mbps` | int | no | MVP | upload bandwidth hint |
| `outbound.down_mbps` | int | no | MVP | download bandwidth hint |
| `outbound.obfs` | string | no | Advanced | obfuscation method |
| `outbound.obfs_password` | string | depends | Advanced | secret |
| `outbound.password` | string | depends | MVP | auth password |
| `outbound.network` | enum | no | Advanced | |
| `outbound.tls` | object | yes (обычно) | MVP | TLS block |
| `outbound.brutal_debug` | bool | no | Raw-only | debug option |
Guardrails:
- `HY2-001`: если задан `obfs`, то `obfs_password` обязателен.
- `HY2-002`: пустой `password` при password-auth конфиге блокирует apply.
## 4.6 TUIC outbound
| JSON path | Type | Required | UI level | Notes |
|---|---|---|---|---|
| `outbound.type` | const `tuic` | yes | MVP | |
| `outbound.server` | string | yes | MVP | |
| `outbound.server_port` | int | yes | MVP | |
| `outbound.uuid` | string | yes | MVP | auth uuid |
| `outbound.password` | string | yes | MVP | secret |
| `outbound.congestion_control` | enum/string | no | Advanced | algo |
| `outbound.udp_relay_mode` | enum/string | no | Advanced | relay mode |
| `outbound.udp_over_stream` | bool | no | Advanced | |
| `outbound.zero_rtt_handshake` | bool | no | Raw-only | advanced/risky |
| `outbound.heartbeat` | duration/int | no | Raw-only | keepalive tuning |
| `outbound.network` | enum | no | Advanced | |
| `outbound.tls` | object | yes (обычно) | MVP | TLS block |
Guardrails:
- `TUIC-001`: `uuid` и `password` обязательны.
- `TUIC-002`: `zero_rtt_handshake=true` помечать warning (опция совместимости/риска).
---
## 5) Срез MVP полей для первой версии GUI
- `VLESS`: `server`, `server_port`, `uuid`, `flow`, `tls.server_name`, `tls.reality.public_key`, `tls.reality.short_id`, `tls.utls.fingerprint`.
- `Trojan`: `server`, `server_port`, `password`, `tls.server_name`.
- `Shadowsocks`: `server`, `server_port`, `method`, `password`.
- `WireGuard`: `server`, `server_port`, `local_address`, `private_key`, `peer_public_key`.
- `Hysteria2`: `server`, `server_port`, `password`, `up_mbps`, `down_mbps`.
- `TUIC`: `server`, `server_port`, `uuid`, `password`, `congestion_control`.
Все остальные поля:
- либо в секции `Advanced`,
- либо в `Raw JSON` (без потери функциональности).
## 6) DoD для этапа "матрица готова"
- Шаблон и матрица лежат в `docs/phase-e`.
- Для каждого протокола есть список MVP/Advanced/Raw-only полей.
- Для каждого протокола есть минимум 2 guardrail-правила валидации.
- Есть machine-readable манифест для последующей генерации форм.

View File

@@ -0,0 +1,158 @@
# E5 Требования: SingBox Protocols (UI tab + Go API)
Дата: 2026-03-07
Статус: planned (requirements fixed)
Владелец: Engineering
## 1) Цель
- Зафиксировать требования для реализации протоколов во вкладке `SingBox` без архитектурной "мешанины".
- Зафиксировать целевой Go API для `singbox` с поддержкой "всех фишек" через typed-профили и raw-режим.
## 2) Архитектурные границы (чтобы не смешивать слои)
- Слой `Transport Engine`:
- lifecycle (`provision/start/stop/restart`), health/metrics, runtime_mode/packaging.
- уже реализован в `/api/v1/transport/clients/*`.
- Слой `Routing Policy`:
- `validate -> confirm -> apply -> rollback`.
- уже реализован в `/api/v1/transport/policies/*`.
- Слой `Protocol Profile` (новый E5):
- описание конфигов `sing-box` (outbounds/rules/dns/tun и т.д.).
- не дублирует lifecycle и не дублирует policy.
Правило:
- `Protocol Profile` готовит/валидирует/рендерит `sing-box` config.
- `Transport Engine` запускает этот config.
- `Routing Policy` решает, какой engine владеет трафиком.
## 3) Требования к вкладке `SingBox` (GUI)
### 3.1 Блоки UI (обязательные)
- `Profiles list`:
- список профилей, тип протокола, версия, последний apply, статус валидации.
- `Editor`:
- режим `Typed` (форма) и режим `Raw JSON` (полный контроль).
- `Validation`:
- синтаксис, обязательные поля, совместимость с бинарём/features.
- `Apply`:
- `Validate` -> `Preview` -> `Apply`.
- при рисках/конфликтах — confirm flow.
- `Diagnostics`:
- last_error, warning, render-diff, health summary.
- `Rollback`:
- откат к предыдущей рабочей версии профиля.
### 3.2 Поведение
- Нельзя применять профиль без успешной валидации.
- `Apply` профиля не должен неявно менять transport-policy ownership.
- Секреты в UI маскируются, редактируются только write-only полями.
- Все mutating-операции идемпотентны и привязаны к ревизии.
### 3.3 Desktop layout freeze
- Вкладка `SingBox` на desktop фиксируется в 3 секции:
- `Connection card (runtime)`,
- `Profile settings (per profile)`,
- `Global defaults`.
- Для profile-параметров используется правило `Use global`/`Override`.
- Детальная спецификация: `docs/phase-e/E5_2_SINGBOX_DESKTOP_DASHBOARD_SPEC.md`.
## 4) Требования к модели профилей (Go)
### 4.1 Поддерживаемые режимы профиля
- `typed`:
- структурированная схема под основные протоколы.
- `raw`:
- полный `sing-box` JSON для "всех фишек", не покрытых формой.
### 4.2 Минимальный набор протоколов для typed-режима
- `vless` (в т.ч. reality/tls варианты),
- `trojan`,
- `shadowsocks`,
- `wireguard`,
- `hysteria2`,
- `tuic`.
Примечание:
- Для нестандартных/редких фич используется `raw` без потери совместимости.
### 4.3 Версионирование
- `schema_version` для профиля.
- `profile_revision` (optimistic lock).
- `render_revision` (версия сгенерированного итогового конфига).
### 4.4 DNS (встроенный resolver sing-box)
- Профиль `singbox` должен включать секцию DNS как часть единого конфига (`dns.servers`, `dns.rules`, `strategy`, `final` и связанные поля).
- В typed-режиме нужен базовый DNS-набор:
- выбор DNS-провайдеров/серверов,
- rule-based DNS routing (по доменам/гео/режимам),
- переключение стратегий резолва (например, prefer IPv4/IPv6 по профилю).
- Для расширенных сценариев (fakeip, cache tuning, нетиповые rules/actions) используется `raw` режим без ограничений формы.
- Ограничение границ:
- DNS `sing-box` отвечает за резолв внутри transport-профиля,
- системный selective routing policy (`/transport/policies/*`) остаётся отдельным слоем и не дублируется в profile editor.
## 5) Требования к Go API `singbox`
### 5.1 Новые endpoint-группы (target)
- `GET/POST /api/v1/transport/singbox/profiles`
- `GET/PATCH/DELETE /api/v1/transport/singbox/profiles/{id}`
- `POST /api/v1/transport/singbox/profiles/{id}/validate`
- `POST /api/v1/transport/singbox/profiles/{id}/render`
- `POST /api/v1/transport/singbox/profiles/{id}/apply`
- `POST /api/v1/transport/singbox/profiles/{id}/rollback`
- `GET /api/v1/transport/singbox/profiles/{id}/history`
- `GET /api/v1/transport/singbox/features`
### 5.2 Контракт операций
- `validate`:
- возвращает ошибки схемы, ошибок протокола, неподдерживаемых фич по текущему binary.
- `render`:
- детерминированно строит финальный `sing-box` config + diff к текущему active.
- `apply`:
- атомарно: запись конфига -> backend provision -> optional restart/start -> health check.
- при fail: автоматический rollback.
- `rollback`:
- откат к последнему успешному render/apply snapshot.
### 5.3 Совместимость и безопасность
- Не ломать текущий `/api/v1/transport/*` контракт.
- Для mutating запросов:
- `Idempotency-Key`,
- `base_revision`.
- Секреты:
- отдельное хранение, выдача в API только masked.
- запрет на логирование plain-secret в trace/events.
## 6) "Все фишки" sing-box: как закрываем требование
- Typed-форма покрывает частые production-сценарии.
- Raw-режим гарантирует доступ к полному синтаксису sing-box.
- `GET /transport/singbox/features` отражает реальные возможности текущего binary:
- поддерживаемые протоколы,
- transport/tls/reality/quic опции,
- ограничения версии.
## 7) Хранение и артефакты
- Профили: `/var/lib/selective-vpn/transport/singbox-profiles.json`
- История/snapshots: `/var/lib/selective-vpn/transport/singbox-history/*.json`
- Rendered configs: `/var/lib/selective-vpn/transport/singbox-rendered/{profile_id}.json`
- Secrets store: `/var/lib/selective-vpn/transport/secrets/singbox/{profile_id}.json` (`0600`)
## 8) SSE-события (обязательные)
- `singbox_profile_saved`
- `singbox_profile_validated`
- `singbox_profile_rendered`
- `singbox_profile_applied`
- `singbox_profile_rollback`
- `singbox_profile_failed`
## 9) Поэтапная реализация (рекомендуемый порядок)
1. `E5.1` Requirements freeze (этот документ).
2. `E5.2` Go: state/model + CRUD + versioning + secrets storage.
3. `E5.3` Go: validate/render/apply/rollback + SSE events.
4. `E5.4` GUI: SingBox profiles tab (`list/editor/validate/preview/apply/rollback`).
5. `E5.5` Совместимость web/mobile: reuse того же API-контракта.
## 10) Критерии готовности E5
- Можно создать/редактировать профиль `singbox` без ручного правки файлов на хосте.
- `Apply` проходит через validate и имеет rollback safety.
- Raw-режим позволяет применить фичи, которых нет в typed-форме.
- Наблюдаемость достаточна для диагностики (events + health + last_error + history).

View File

@@ -0,0 +1,42 @@
# E5 SingBox Protocol Matrix Template
Дата: 2026-03-08
Статус: active template
Владелец: Engineering
## 1) Назначение
- Единый шаблон, по которому фиксируем поля протокола `sing-box` до реализации GUI.
- Используется для desktop сейчас и для web/iOS/Android позже.
## 2) Карточка протокола
- `Protocol`: `<vless|trojan|shadowsocks|wireguard|hysteria2|tuic|...>`
- `Mode`: `typed` / `raw` / `typed+raw`
- `Target sing-box`: `>=1.12`
- `Source docs`: ссылки на официальные страницы (`sing-box.sagernet.org`)
- `UI phase`: `MVP` / `Phase-2` / `Raw-only`
## 3) Матрица полей (шаблон)
| JSON path | Type | Required | Default | Since | Validation | UI level | Notes |
|---|---|---|---|---|---|---|---|
| `outbound.type` | enum/string | yes | `<protocol>` | - | fixed const | MVP | |
| `...` | | | | | | | |
## 4) Зависимости/guardrails (шаблон)
| Rule ID | Condition | Constraint | Error text |
|---|---|---|---|
| `R-001` | `...` | `...` | `...` |
## 5) Runtime/apply flow (шаблон)
- `Preview render` -> `Validate` -> `Apply` -> `History` -> `Rollback`.
- `Apply` только после `Validate ok=true`.
- Mutating операции через `base_revision` (optimistic lock).
## 6) Минимум для GUI
- Секция `Server/Auth` (endpoint + credentials).
- Секция `TLS/Reality` (если применимо).
- Секция `Transport` (если применимо).
- Секция `Advanced` (скрыта по умолчанию).
- `Raw JSON` всегда доступен как escape hatch.

View File

@@ -0,0 +1,171 @@
# E6 API-контракт: egress identity (IP + country)
Дата: 2026-03-10
Статус: draft (E6.1 freeze)
Владелец: Engineering
## 1) Цель
- Ввести единый backend-контракт для определения текущей egress-идентичности для любого движка.
- Избежать дублирования логики в desktop/web/mobile: UI только читает готовые поля из Go API.
- Дать унифицированные поля для показа `IP + страна` и построения флага в UI из `country_code`.
## 2) Область и ограничения
- Источник истины: только Go-ядро.
- UI не делает внешние IP/Geo запросы напрямую.
- Поддерживаемые scope:
- `adguardvpn`
- `transport:<client_id>`
- `system`
- Первичный формат ответа: `HTTP 200 + ok/message` (совместимо с текущим API-паттерном).
## 3) Модель данных
### 3.1 EgressIdentity
```json
{
"scope": "transport:sg-realnetns",
"source": "transport",
"source_id": "sg-realnetns",
"ip": "203.0.113.10",
"country_code": "SG",
"country_name": "Singapore",
"updated_at": "2026-03-10T07:20:00Z",
"stale": false,
"refresh_in_progress": false,
"last_error": "",
"next_retry_at": ""
}
```
Пояснения:
- `scope`: нормализованный ключ области.
- `source`: `adguardvpn|transport|system`.
- `source_id`: для `transport` это `client_id`, иначе пусто.
- `ip`: egress IP для выбранного scope.
- `country_code`: ISO-2 uppercase (например `US`, `SG`).
- `country_name`: человекочитаемое имя страны.
- `updated_at/stale/refresh_in_progress/last_error/next_retry_at`: SWR-метаданные.
### 3.2 EgressIdentityResponse
```json
{
"ok": true,
"message": "ok",
"item": {
"scope": "adguardvpn",
"source": "adguardvpn",
"source_id": "",
"ip": "198.51.100.5",
"country_code": "NL",
"country_name": "Netherlands",
"updated_at": "2026-03-10T07:20:00Z",
"stale": false,
"refresh_in_progress": false,
"last_error": "",
"next_retry_at": ""
}
}
```
### 3.3 EgressIdentityRefreshRequest
```json
{
"scopes": ["adguardvpn", "transport:sg-realnetns"],
"force": false
}
```
### 3.4 EgressIdentityRefreshResponse
```json
{
"ok": true,
"message": "refresh queued",
"count": 2,
"queued": 1,
"skipped": 1,
"items": [
{
"scope": "adguardvpn",
"status": "queued",
"queued": true,
"reason": ""
},
{
"scope": "transport:sg-realnetns",
"status": "skipped",
"queued": false,
"reason": "throttled or already fresh"
}
]
}
```
## 4) Endpoint-ы
### 4.1 `GET /api/v1/egress/identity`
Назначение:
- Получить текущий snapshot egress identity для одного scope.
Query:
- `scope` (required): `adguardvpn|transport:<id>|system`
- `refresh=1` (optional): best-effort trigger фонового refresh перед возвратом snapshot.
Response:
- `200 + EgressIdentityResponse`.
Ошибки запроса:
- `400` при невалидном `scope`.
### 4.2 `POST /api/v1/egress/identity/refresh`
Назначение:
- Поставить refresh в очередь для одного или нескольких scope без блокировки UI.
Request body:
- `scopes[]` optional (если пусто -> refresh всех известных scope);
- `force` optional (`true` игнорирует freshness TTL, но уважает single-flight lock).
Response:
- `200 + EgressIdentityRefreshResponse`.
Ошибки запроса:
- `400` bad json / invalid scope format.
## 5) Правила freshness/SWR
- Refresh делается в фоне, UI получает последний cache snapshot мгновенно.
- Для каждого scope:
- single-flight (не запускать параллельные одинаковые refresh);
- backoff при ошибках;
- `stale=true` если snapshot устарел или нет новых данных;
- `next_retry_at` выставляется при backoff.
- Рекомендуемая стратегия UI:
- сразу рисовать cache (`GET`),
- отдельным действием триггерить `POST .../refresh`,
- обновляться по SSE/ручному poll.
## 6) SSE событие
Событие для подписчиков:
- `egress_identity_changed`
Payload (минимум):
```json
{
"scope": "transport:sg-realnetns",
"ip": "203.0.113.10",
"country_code": "SG",
"country_name": "Singapore",
"updated_at": "2026-03-10T07:20:00Z",
"stale": false,
"last_error": ""
}
```
## 7) Отрисовка флага
- Backend возвращает только `country_code`.
- Флаг рендерится в UI из `country_code` (desktop/web/mobile одинаково).
- Если `country_code` пустой/невалидный: UI показывает `N/A` без флага.
## 8) Минимальные критерии готовности E6.1
- Документирован единый контракт `GET/POST` для egress identity.
- Зафиксированы scope и поля snapshot.
- Зафиксированы правила SWR и SSE-событие.
- Явно указано, что флаг строится в UI из `country_code`.

View File

@@ -0,0 +1,140 @@
# F1 План рефакторинга и модульности
Дата: 2026-03-07
Статус: planned
Владелец: Engineering
## 1) Цель
- Уменьшить размер и связность крупных файлов без изменения поведения приложения.
- Подготовить кодовую базу к быстрому развитию desktop/web/mobile поверх одного Go API.
## 2) Текущие "горячие" файлы
- `selective-vpn-gui/vpn_dashboard_qt.py``6103` строк (до старта F1.4; после текущего разреза: `116` + `main_window/ui_shell_mixin.py` + `main_window/singbox/*` + `main_window/runtime_actions_mixin.py`).
- `selective-vpn-gui/api_client.py``2220` строк.
- `selective-vpn-gui/dashboard_controller.py``1430` строк.
- `selective-vpn-api/app/transport_handlers.go``1984` строки.
- `selective-vpn-api/app/server.go` (legacy) — центральная связка entrypoints/bootstrap/routes, высокий риск регрессий при точечных изменениях.
## 3) Принципы разрезания
- Поведение не меняем: сначала вынос кода, потом точечные улучшения.
- Модули режем по domain-границам, а не "по 500 строк".
- Сохраняем один публичный entrypoint (facade) для UI и для backend-router.
- После каждого шага прогоняем существующие smoke/e2e (без новых требований к тестам от пользователя).
## 4) План разрезания GUI
### 4.1 `vpn_dashboard_qt.py` -> `gui/main_window/*`
- Цель: вынести табы и event-обвязку в отдельные модули.
- Разделение:
- `main_window.py` — сборка окна и общий lifecycle.
- `tabs/status_tab.py`
- `tabs/vpn_tab.py`
- `tabs/routes_tab.py`
- `tabs/dns_tab.py`
- `tabs/domains_tab.py`
- `tabs/trace_tab.py`
- `services/events_bridge.py` (`EventThread`, dispatch)
- `services/locations_loader.py` (`LocationsThread`, SWR-ui orchestration)
- Критерий: исходный файл `vpn_dashboard_qt.py` становится thin-bootstrap.
- Статус: выполнено (`2026-03-10`).
- Уже сделано:
- добавлены `main_window/constants.py` и `main_window/workers.py` (shared constants + `EventThread`/`LocationsThread`);
- вынесен UI shell (`build tabs + helpers + locations/egress`) в `main_window/ui_shell_mixin.py`;
- `ui_tabs_singbox_mixin.py` дополнительно разрезан на `ui_tabs_singbox_layout_mixin.py` и `ui_tabs_singbox_editor_mixin.py`;
- SingBox-контур дополнительно разрезан на подпакет `main_window/singbox/*` (`editor`, `cards`, `links`, `runtime`) с фасадом `main_window/singbox_mixin.py`;
- вынесен runtime/refresh/actions контур в `main_window/runtime_actions_mixin.py`;
- проведён второй проход декомпозиции: `ui_tabs_*`, `runtime_{state,refresh,auth,ops}`, `singbox/{links_*,runtime_*}`; фасады оставлены для совместимости;
- `MainWindow` переведён на наследование mixin-классов, поведение сохранено; в итоге крупные GUI-модули уложены в читаемые размеры (максимум `~633` строк).
### 4.2 `dashboard_controller.py` -> `controllers/*`
- Разделение:
- `controllers/status_controller.py`
- `controllers/vpn_controller.py`
- `controllers/routes_controller.py`
- `controllers/dns_controller.py`
- `controllers/domains_controller.py`
- `controllers/transport_controller.py`
- `controllers/trace_controller.py`
- Сохраняется фасад `DashboardController` (совместимость с текущим UI-кодом).
- Статус: выполнено (`2026-03-10`).
- Что сделано:
- добавлен пакет `selective-vpn-gui/controllers/` (`core + views + domain mixin-модули`);
- `selective-vpn-gui/dashboard_controller.py` переведён в thin-facade с прежней точкой входа;
- `py_compile` и импорты UI-модулей (`vpn_dashboard_qt.py`, `traffic_mode_dialog.py`, `dns_benchmark_dialog.py`) проходят.
### 4.3 `api_client.py` -> `api/*`
- Разделение:
- `api/client.py` (HTTP/SSE base + shared helpers)
- `api/models.py` (dataclasses)
- `api/status.py`, `api/vpn.py`, `api/routes.py`, `api/traffic.py`, `api/dns.py`, `api/domains.py`, `api/trace.py`
- `api/transport.py` facade + `api/transport_clients.py`, `api/transport_policy.py`, `api/transport_singbox.py`
- Сохраняется фасад `ApiClient` (старая точка импорта для UI).
- Статус: выполнено (`2026-03-10`).
- Что сделано:
- добавлен пакет `selective-vpn-gui/api/` с доменными mixin-модулями;
- `api_client.py` переведён в backward-compatible facade (legacy imports сохранены);
- `api/client.py` сокращён до base-слоя, доменные методы вынесены по подпапкам;
- `api/transport.py` разрезан на subdomain-модули с сохранением класса `TransportApiMixin`.
## 5) План разрезания Go-ядра
### 5.1 `transport_handlers.go` -> `app/transport_*`
- Разделение:
- `transport_handlers_clients.go` (CRUD/lifecycle/health/metrics)
- `transport_handlers_policy.go` (validate/apply/rollback/conflicts)
- `transport_validator.go` (normalize + conflict detection)
- `transport_confirm_token.go` (token lifecycle)
- `transport_state_store.go` (load/save state/snapshots)
- Сохраняем текущие endpoint-path и response-контракт без изменений.
### 5.2 `server.go` -> `app/entrypoints.go + api_bootstrap.go + api_routes.go`
- Цель: развязать entrypoint-логику (`Run*CLI`), bootstrap API-сервера и registry маршрутов.
- Статус: выполнено (`2026-03-10`).
- Состав:
- `entrypoints.go``Run`, `RunAPIServer`, `RunRoutesUpdateCLI`, `RunRoutesClearCLI`, `RunAutoloopCLI`, legacy-dispatch;
- `api_bootstrap.go` — bootstrap и lifecycle HTTP-сервера (`runAPIServerAtAddr`);
- `api_routes.go` — thin-facade `registerAPIRoutes`;
- `api_routes_*.go` — доменные registrars (`core`, `traffic`, `transport`, `trace`, `dns`, `vpn`) без изменения endpoint-path.
### 5.3 `app/*` -> подпакеты runtime (`app/cli`, `app/bootstrap`)
- Цель: убрать "кучу" в корне `app` и перенести исполнимые runner-модули в отдельные папки, сохранив фасады в `app`.
- Статус: выполнено (`2026-03-10`).
- Состав:
- `app/cli/*``routes-update`, `routes-clear`, `autoloop` раннеры с dependency-injection (callback deps);
- `app/bootstrap/server_runner.go` — единый HTTP server runner;
- `app/entrypoints.go` и `app/api_bootstrap.go` оставлены thin-facade слоями.
### 5.4 `transport_handlers.go` -> модульные transport файлы
- Цель: убрать крупнейший монолит transport control-plane без изменения endpoint-контракта.
- Статус: выполнено (`2026-03-10`).
- Состав:
- `transport_shared.go` — state/version constants, mutex, общие типы;
- `transport_handlers_clients.go` — clients CRUD/lifecycle/netns toggle handlers;
- `transport_handlers_policy.go` — policy validate/apply/rollback/conflicts/capabilities handlers;
- `transport_policy_validate.go` — normalize/validate/diff/conflict helpers;
- `transport_client_runtime.go` — runtime/health/lifecycle/allocation helpers;
- `transport_tokens_state.go` — confirm-token и state persistence.
### 5.5 Подпакеты transport (без циклических зависимостей)
- Цель: выносить переиспользуемые части transport-логики в подпапки только там, где не возникает import-cycle с `app`.
- Статус: in_progress (`2026-03-10`).
- Уже вынесено:
- `app/transporttoken/store.go` — confirm-token store (`issue/consume/ttl cleanup`) и token generator.
- Дальше:
- выделить в подпакеты часть stateless helper-функций (при сохранении фасадов в `app`).
## 6) Порядок выполнения (безопасный)
1. `api_client.py` (минимальный риск для UI, проще ревьюить).
2. `dashboard_controller.py`.
3. `vpn_dashboard_qt.py` (таб за табом).
4. `transport_handlers.go` в Go.
## 7) Definition of Done
- Все текущие smoke/e2e проходят:
- `./tests/run_all.sh`
- плюс ручная проверка пользователем в desktop приложении.
- Нет изменений API-контракта (`/api/v1/*`) и event-kind.
- В каждом крупном модуле остаётся читаемый размер (ориентир `< 700` строк; предпочтительно `< 500`).
## 8) Дополнительно (после F1)
- Добавить settings-механику видимости вкладок протоколов (`SingBox`, `DNSTT`, `Phoenix`) через `QSettings`/profile, чтобы включать только нужные модули UI.

View File

@@ -0,0 +1,36 @@
# F3 План: библиотека модулей Go-ядра (deferred)
Дата: 2026-03-14
Статус: deferred
Владелец: Engineering
## 1) Цель
- Подготовить ядро к формату внутренней библиотеки модулей, чтобы backend собирался из чётких переиспользуемых пакетов.
- Снизить связность `app/*` и закрепить стабильные интерфейсы orchestration/PBR/transport/resolver.
## 2) Принцип scope
- `linux-first`: библиотека ориентирована на Linux runtime backend.
- `netns` — отладочный/инфраструктурный модуль для backend/desktop тестового контура.
- `mobile (iOS/Android)` не является целью для `netns`-модуля.
- Для mobile не планируется `netns` слой: только вызовы backend API (control-plane), без platform-specific system hooks.
- GUI/Web/Mobile должны использовать только API control-plane, а не прямые системные модули.
## 3) Целевые пакеты
- `pkg/orchestrator` — lifecycle orchestration (`prepare/start/stop/restart/rollback`) и locking.
- `pkg/transport` — shared transport-модели/валидация/runtime helpers.
- `pkg/pbr` — marks/tables/prefs allocator + compiler.
- `pkg/resolver` — resolver policy/execution primitives.
- `pkg/netnsdebug` (или `pkg/netns`) — Linux-only netns setup/cleanup helpers для debug/test-contour.
## 4) Шаги реализации (будущий этап)
1. Выделить публичные интерфейсы зависимостей (runner, nft/ip adapters, state store, logger).
2. Перенести код в `pkg/*` без изменения поведения.
3. Оставить `app/*` как thin-facade + HTTP слой.
4. Добавить compile/runtime guardrails для Linux-only модулей.
5. Обновить runbook миграции и интеграционные чеклисты.
## 5) Критерии готовности
- Backend-сборка не зависит от монолитных `app/*` helper-блоков.
- `netns` модуль чётко помечен как Linux-only и debug/test-oriented.
- Внешние API (`/api/v1/*`) не меняются.
- `go test ./...` зелёный после миграции.