Files
elmprodvpn/docs/phase-e/E2_TRANSPORT_API_CONTRACT.md

603 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).