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

22 KiB
Raw Blame History

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 Базовые поля ответа

{
  "ok": true,
  "message": "ok",
  "request_id": "req-01JABC...",
  "data": {}
}

3.2 Ошибка доменной валидации

{
  "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

{
  "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

{
  "selector_type": "domain",
  "selector_value": "youtube.com",
  "client_id": "phoenix-eu",
  "priority": 100,
  "mode": "strict"
}

4.3 ConflictRecord

{
  "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:

{
  "ok": true,
  "message": "ok",
  "items": [],
  "count": 3
}

5.2 POST /api/v1/transport/clients

  • Назначение: создать клиента.

Request:

{
  "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:

{
  "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).

Пример:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "ok": true,
  "message": "ok",
  "policy_revision": 12,
  "intents": []
}

6.2 POST /api/v1/transport/policies/validate

  • Назначение: dry-run валидация без применения.

Request:

{
  "base_revision": 12,
  "intents": [],
  "options": {
    "allow_warnings": true,
    "force_override": false
  }
}

Response:

{
  "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:

{
  "base_revision": 12,
  "intents": [],
  "options": {
    "force_override": true,
    "confirm_token": "cnf-01JABC..."
  }
}

Response:

{
  "ok": true,
  "message": "policy applied",
  "policy_revision": 13,
  "apply_id": "apl-01JABC...",
  "rollback_available": true
}

Ошибка конкурентного изменения:

{
  "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:

{
  "base_revision": 13
}

Response:

{
  "ok": true,
  "message": "policy rollback applied",
  "policy_revision": 14,
  "apply_id": "rbk-01JABC...",
  "rollback_available": true
}

6.5 GET /api/v1/transport/conflicts

  • Назначение: получить актуальные конфликты активной конфигурации.

Response:

{
  "ok": true,
  "message": "ok",
  "items": [],
  "has_blocking": true
}

6.6 GET /api/v1/transport/capabilities

  • Назначение: матрица возможностей backend-клиентов и текущей платформы.

Response:

{
  "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).