platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
188
docs/phase-e/E1_MULTI_CLIENT_PBR_DESIGN.md
Normal file
188
docs/phase-e/E1_MULTI_CLIENT_PBR_DESIGN.md
Normal 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`.
|
||||
602
docs/phase-e/E2_TRANSPORT_API_CONTRACT.md
Normal file
602
docs/phase-e/E2_TRANSPORT_API_CONTRACT.md
Normal 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).
|
||||
91
docs/phase-e/E3_MULTI_INTERFACE_EXECUTION_PLAN.md
Normal file
91
docs/phase-e/E3_MULTI_INTERFACE_EXECUTION_PLAN.md
Normal 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`).
|
||||
101
docs/phase-e/E4_2_MULTI_INTERFACE_GUI_DESIGN.md
Normal file
101
docs/phase-e/E4_2_MULTI_INTERFACE_GUI_DESIGN.md
Normal 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.
|
||||
126
docs/phase-e/E4_VALIDATE_CONFIRM_APPLY_UX.md
Normal file
126
docs/phase-e/E4_VALIDATE_CONFIRM_APPLY_UX.md
Normal 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`).
|
||||
75
docs/phase-e/E5_2_SINGBOX_DESKTOP_DASHBOARD_SPEC.md
Normal file
75
docs/phase-e/E5_2_SINGBOX_DESKTOP_DASHBOARD_SPEC.md
Normal 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.
|
||||
101
docs/phase-e/E5_SINGBOX_CLIENT_FORM_MATRIX.md
Normal file
101
docs/phase-e/E5_SINGBOX_CLIENT_FORM_MATRIX.md
Normal 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`.
|
||||
- То есть форма будет модульной, а не отдельное "окно под каждый протокол".
|
||||
|
||||
252
docs/phase-e/E5_SINGBOX_PROTOCOLS_MANIFEST.example.json
Normal file
252
docs/phase-e/E5_SINGBOX_PROTOCOLS_MANIFEST.example.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
229
docs/phase-e/E5_SINGBOX_PROTOCOLS_MATRIX.md
Normal file
229
docs/phase-e/E5_SINGBOX_PROTOCOLS_MATRIX.md
Normal 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 манифест для последующей генерации форм.
|
||||
|
||||
158
docs/phase-e/E5_SINGBOX_PROTOCOLS_REQUIREMENTS.md
Normal file
158
docs/phase-e/E5_SINGBOX_PROTOCOLS_REQUIREMENTS.md
Normal 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).
|
||||
42
docs/phase-e/E5_SINGBOX_PROTOCOL_MATRIX_TEMPLATE.md
Normal file
42
docs/phase-e/E5_SINGBOX_PROTOCOL_MATRIX_TEMPLATE.md
Normal 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.
|
||||
|
||||
171
docs/phase-e/E6_EGRESS_IDENTITY_API_CONTRACT.md
Normal file
171
docs/phase-e/E6_EGRESS_IDENTITY_API_CONTRACT.md
Normal 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`.
|
||||
Reference in New Issue
Block a user