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,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).