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

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`.