# 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`: ` run -c `; - `phoenix`: ` -config `. Примечание для `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).