openapi: 3.1.0 info: title: awg-dash API version: 0.1.0 summary: REST + WebSocket control plane of the AmneziaWG gateway dashboard description: | Контракт API v1 (source of truth, ADR-008). Спецификация — источник истины: реализация и UI-клиент генерируются/проверяются от неё. Соглашения: * Единый формат ошибки — схема `Error` (`{code, message, details}`) для любого не-2xx ответа, включая 422 и ошибки внутренних исключений. * Секреты (`private_key`, `preshared_key`, `password`) помечены `writeOnly`: они допустимы в запросах и ЗАПРЕЩЕНЫ в ответах (`additionalProperties: false` + удаление `writeOnly`-свойств из ответной валидации). * Аутентификация: `Authorization: Bearer ` для REST, cookie-сессия + `X-CSRF-Token` для UI. `/api/v1/health`, `/api/v1/ready`, `/metrics` — открыты. * Все таймстемпы — RFC 3339 (UTC), целые размеры — байты, длительности — секунды. Генерация TS-клиента: `ui/scripts/generate-api.sh` → `ui/src/api/generated/schema.d.ts`. Контрактный тест: `pytest backend/tests/test_contract_responses.py`. license: name: MIT identifier: MIT servers: - url: / description: Встроенный uvicorn (ADR-007), тот же origin, что и UI tags: - name: ops description: Служебные эндпоинты (health/ready/metrics), без аутентификации - name: status description: Состояние туннеля и сводный статус - name: config description: Чтение, запись и валидация конфигурации AmneziaWG - name: routing description: Правила маршрутизации, режим split-tunnel, GeoIP, импорт - name: devices description: Устройства за шлюзом и их маршрутизация - name: logs description: Журнал событий шлюза - name: auth description: Аутентификация UI и API - name: metrics description: Потоковые метрики (WebSocket) security: - bearerAuth: [] paths: /api/v1/health: get: tags: [ops] operationId: getHealth summary: Liveness-проба security: [] responses: "200": description: Процесс жив content: application/json: schema: $ref: "#/components/schemas/HealthResponse" "500": $ref: "#/components/responses/InternalError" /api/v1/ready: get: tags: [ops] operationId: getReady summary: Readiness-проба (готовность агента и зависимостей) security: [] responses: "200": description: Состояние готовности (в том числе при ready=false) content: application/json: schema: $ref: "#/components/schemas/ReadyResponse" "503": $ref: "#/components/responses/AgentUnavailable" "500": $ref: "#/components/responses/InternalError" /metrics: get: tags: [ops] operationId: getPrometheusMetrics summary: Метрики в формате Prometheus text exposition security: [] responses: "200": description: Текстовый экспорт метрик content: text/plain: schema: type: string description: Prometheus text exposition format (version 0.0.4) /api/v1/status: get: tags: [status] operationId: getStatus summary: Сводный статус шлюза, туннеля и GeoIP responses: "200": description: Статус content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "401": $ref: "#/components/responses/Unauthorized" "503": $ref: "#/components/responses/AgentUnavailable" "500": $ref: "#/components/responses/InternalError" /api/v1/config: get: tags: [config] operationId: getConfig summary: Текущая конфигурация (без секретов) responses: "200": description: Конфигурация; секретные поля отсутствуют content: application/json: schema: $ref: "#/components/schemas/ConfigResponse" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" put: tags: [config] operationId: putConfig summary: Атомарное применение конфигурации (validate → staging → apply → rollback) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ConfigUpdateRequest" responses: "200": description: Конфигурация применена content: application/json: schema: $ref: "#/components/schemas/ConfigResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationFailed" "500": $ref: "#/components/responses/InternalError" /api/v1/config/validate: post: tags: [config] operationId: validateConfig summary: Сухая валидация конфигурации без применения requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ConfigUpdateRequest" responses: "200": description: Результат валидации (в том числе при valid=false) content: application/json: schema: $ref: "#/components/schemas/ValidateResponse" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/ValidationFailed" "500": $ref: "#/components/responses/InternalError" /api/v1/routing/rules: get: tags: [routing] operationId: listRoutingRules summary: Список правил маршрутизации parameters: - name: enabled in: query required: false description: Фильтр по признаку включённости schema: type: boolean - name: kind in: query required: false schema: $ref: "#/components/schemas/RuleKind" responses: "200": description: Список правил content: application/json: schema: $ref: "#/components/schemas/RoutingRuleList" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" post: tags: [routing] operationId: createRoutingRule summary: Создать правило маршрутизации requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RoutingRuleCreate" responses: "201": description: Правило создано content: application/json: schema: $ref: "#/components/schemas/RoutingRule" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationFailed" "500": $ref: "#/components/responses/InternalError" /api/v1/routing/rules/{rule_id}: parameters: - $ref: "#/components/parameters/RuleId" get: tags: [routing] operationId: getRoutingRule summary: Правило по идентификатору responses: "200": description: Правило content: application/json: schema: $ref: "#/components/schemas/RoutingRule" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: tags: [routing] operationId: updateRoutingRule summary: Изменить правило requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RoutingRuleUpdate" responses: "200": description: Обновлённое правило content: application/json: schema: $ref: "#/components/schemas/RoutingRule" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" "500": $ref: "#/components/responses/InternalError" delete: tags: [routing] operationId: deleteRoutingRule summary: Удалить правило responses: "204": description: Правило удалено "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/routing/mode: put: tags: [routing] operationId: putRoutingMode summary: Переключить режим split-tunnel requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ModeUpdateRequest" responses: "200": description: Режим применён content: application/json: schema: $ref: "#/components/schemas/ModeUpdateResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/ValidationFailed" "503": $ref: "#/components/responses/AgentUnavailable" "500": $ref: "#/components/responses/InternalError" /api/v1/routing/geoip/update: post: tags: [routing] operationId: updateGeoIp summary: Обновить снапшот GeoIP (RIPE Stat → локальный кэш) requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/GeoIpUpdateRequest" responses: "200": description: Обновление выполнено (или взято из кэша) content: application/json: schema: $ref: "#/components/schemas/GeoIpUpdateResponse" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/ValidationFailed" "503": $ref: "#/components/responses/UpstreamUnavailable" "500": $ref: "#/components/responses/InternalError" /api/v1/routing/import: post: tags: [routing] operationId: importRoutingConfig summary: Импорт .conf / JSON (пиры и правила) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ImportRequest" responses: "200": description: Результат импорта content: application/json: schema: $ref: "#/components/schemas/ImportResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/ValidationFailed" "500": $ref: "#/components/responses/InternalError" /api/v1/metrics/stream: get: tags: [metrics] operationId: streamMetrics summary: WebSocket-поток метрик и статуса description: | WebSocket-апгрейд на том же пути. Клиент передаёт токен либо заголовком `Authorization: Bearer `, либо query-параметром `token`. Сервер отправляет текстовые JSON-кадры схемы `WsFrame`; при `type=metrics` поле `data` соответствует `MetricsSample`, при `type=status` — `StatusResponse`, при `type=error` — `Error`. parameters: - name: token in: query required: false description: Токен доступа для сред, где нельзя задать заголовок schema: type: string - name: interval_ms in: query required: false description: Период отправки кадров, мс schema: type: integer minimum: 250 maximum: 60000 default: 1000 responses: "101": description: Переключение протокола на WebSocket "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/v1/devices: get: tags: [devices] operationId: listDevices summary: Устройства за шлюзом responses: "200": description: Список устройств content: application/json: schema: $ref: "#/components/schemas/DeviceList" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/v1/logs: get: tags: [logs] operationId: listLogs summary: Журнал событий parameters: - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 500 default: 100 - name: cursor in: query required: false description: Курсор пагинации из предыдущего ответа (`next_cursor`) schema: type: string - name: level in: query required: false schema: $ref: "#/components/schemas/LogLevel" - name: q in: query required: false description: Подстрока поиска по сообщению schema: type: string responses: "200": description: Страница журнала content: application/json: schema: $ref: "#/components/schemas/LogList" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/ValidationFailed" "500": $ref: "#/components/responses/InternalError" /api/v1/auth/login: post: tags: [auth] operationId: login summary: Аутентификация (сессия UI + токен API) security: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AuthLoginRequest" responses: "200": description: Сессия создана content: application/json: schema: $ref: "#/components/schemas/AuthSession" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/ValidationFailed" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" /api/v1/auth/logout: post: tags: [auth] operationId: logout summary: Завершить сессию responses: "200": description: Сессия завершена content: application/json: schema: $ref: "#/components/schemas/AuthSession" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/v1/auth/session: get: tags: [auth] operationId: getAuthSession summary: Текущая сессия и CSRF-токен responses: "200": description: Состояние сессии content: application/json: schema: $ref: "#/components/schemas/AuthSession" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: opaque description: "Токен доступа API (заголовок `Authorization: Bearer `)" sessionCookie: type: apiKey in: cookie name: awg_session description: Сессионная cookie UI (требуется вместе с `X-CSRF-Token` для изменяющих запросов) parameters: RuleId: name: rule_id in: path required: true description: Идентификатор правила schema: type: string format: uuid responses: BadRequest: description: Некорректный запрос content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Отсутствует или недействителен токен доступа content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: Недостаточно прав content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Объект не найден content: application/json: schema: $ref: "#/components/schemas/Error" Conflict: description: Конфликт состояния (например, apply уже выполняется) content: application/json: schema: $ref: "#/components/schemas/Error" ValidationFailed: description: Тело запроса не соответствует схеме content: application/json: schema: $ref: "#/components/schemas/Error" RateLimited: description: Превышен лимит запросов content: application/json: schema: $ref: "#/components/schemas/Error" UpstreamUnavailable: description: Внешний источник (RIPE Stat) недоступен content: application/json: schema: $ref: "#/components/schemas/Error" AgentUnavailable: description: Агент шлюза недоступен (unix-socket не отвечает) content: application/json: schema: $ref: "#/components/schemas/Error" InternalError: description: Внутренняя ошибка content: application/json: schema: $ref: "#/components/schemas/Error" schemas: ErrorCode: type: string description: Машиночитаемый код ошибки enum: - bad_request - validation_failed - unauthorized - forbidden - not_found - conflict - rate_limited - upstream_unavailable - agent_unavailable - internal_error Error: type: object description: Единый формат ошибки для всех не-2xx ответов additionalProperties: false required: [code, message, details] properties: code: $ref: "#/components/schemas/ErrorCode" message: type: string minLength: 1 description: Человекочитаемое описание details: description: Машиночитаемые детали (для validation_failed — список issues) type: [object, "null"] additionalProperties: true default: null Check: type: object additionalProperties: false required: [name, ok] properties: name: type: string ok: type: boolean detail: type: [string, "null"] HealthResponse: type: object additionalProperties: false required: [status, version, uptime_s, checks] properties: status: type: string enum: [ok, degraded, down] version: type: string uptime_s: type: integer minimum: 0 checks: type: array items: $ref: "#/components/schemas/Check" ReadyResponse: type: object additionalProperties: false required: [ready, checks] properties: ready: type: boolean checks: type: array items: $ref: "#/components/schemas/Check" RoutingMode: type: string description: Режим split-tunnel enum: [geoip, dns, all-vpn, direct-only] RuleKind: type: string enum: [domain, cidr, geoip, asn] RuleAction: type: string enum: [vpn, direct] LogLevel: type: string enum: [debug, info, warning, error] TunnelState: type: string enum: [up, down, error] TunnelStatus: type: object additionalProperties: false required: [interface, state, handshake_age_s, rx_bytes, tx_bytes, endpoint, last_handshake_at] properties: interface: type: string examples: [awg0] state: $ref: "#/components/schemas/TunnelState" handshake_age_s: type: [integer, "null"] minimum: 0 rx_bytes: type: integer minimum: 0 tx_bytes: type: integer minimum: 0 endpoint: type: [string, "null"] last_handshake_at: type: [string, "null"] format: date-time GeoIpSource: type: string enum: [ripe-stat, cache, none] GeoIpStatus: type: object additionalProperties: false required: [source, prefix_count, updated_at] properties: source: $ref: "#/components/schemas/GeoIpSource" prefix_count: type: integer minimum: 0 updated_at: type: [string, "null"] format: date-time StatusResponse: type: object additionalProperties: false required: [version, uptime_s, mode, tunnel, geoip, kill_switch, dns_leak_protection] properties: version: type: string uptime_s: type: integer minimum: 0 mode: $ref: "#/components/schemas/RoutingMode" tunnel: $ref: "#/components/schemas/TunnelStatus" geoip: $ref: "#/components/schemas/GeoIpStatus" kill_switch: type: boolean dns_leak_protection: type: boolean TunnelConfig: type: object additionalProperties: false required: [address, listen_port, mtu, private_key] properties: address: type: string description: Адрес интерфейса в CIDR examples: ["10.8.0.2/32"] listen_port: type: integer minimum: 1 maximum: 65535 mtu: type: integer minimum: 576 maximum: 1500 private_key: type: string writeOnly: true description: Приватный ключ интерфейса; только запись, в ответах отсутствует dns: type: array items: type: string PeerConfig: type: object additionalProperties: false required: [public_key, endpoint, allowed_ips] properties: public_key: type: string preshared_key: type: string writeOnly: true description: Preshared key; только запись, в ответах отсутствует endpoint: type: string allowed_ips: type: array items: type: string persistent_keepalive: type: [integer, "null"] minimum: 0 maximum: 65535 DnsConfig: type: object additionalProperties: false required: [mode, servers] properties: mode: type: string enum: [internal, external] servers: type: array items: type: string Ipv6Policy: type: string enum: [direct, tunnel, block] RoutingConfig: type: object additionalProperties: false required: [mode, kill_switch, mtu_clamping, ipv6, geoip] properties: mode: $ref: "#/components/schemas/RoutingMode" kill_switch: type: boolean mtu_clamping: type: boolean ipv6: $ref: "#/components/schemas/Ipv6Policy" geoip: $ref: "#/components/schemas/GeoIpStatus" ConfigResponse: type: object additionalProperties: false required: [tunnel, peers, dns, routing, updated_at] properties: tunnel: $ref: "#/components/schemas/TunnelConfig" peers: type: array items: $ref: "#/components/schemas/PeerConfig" dns: $ref: "#/components/schemas/DnsConfig" routing: $ref: "#/components/schemas/RoutingConfig" updated_at: type: string format: date-time ConfigUpdateRequest: type: object description: Частичное обновление конфигурации (merge в текущее состояние) additionalProperties: false minProperties: 1 properties: tunnel: $ref: "#/components/schemas/TunnelConfig" peers: type: array items: $ref: "#/components/schemas/PeerConfig" dns: $ref: "#/components/schemas/DnsConfig" routing: $ref: "#/components/schemas/RoutingConfig" ValidationIssue: type: object additionalProperties: false required: [path, code, message, severity] properties: path: type: string description: JSON Pointer до проблемного поля code: type: string message: type: string severity: type: string enum: [error, warning] ValidateResponse: type: object additionalProperties: false required: [valid, errors, warnings] properties: valid: type: boolean errors: type: array items: $ref: "#/components/schemas/ValidationIssue" warnings: type: array items: $ref: "#/components/schemas/ValidationIssue" RoutingRule: type: object additionalProperties: false required: [id, name, kind, value, action, enabled, priority, comment, created_at, updated_at] properties: id: type: string format: uuid name: type: string kind: $ref: "#/components/schemas/RuleKind" value: type: string action: $ref: "#/components/schemas/RuleAction" enabled: type: boolean priority: type: integer comment: type: [string, "null"] created_at: type: string format: date-time updated_at: type: string format: date-time RoutingRuleCreate: type: object additionalProperties: false required: [name, kind, value, action] properties: name: type: string minLength: 1 maxLength: 128 kind: $ref: "#/components/schemas/RuleKind" value: type: string minLength: 1 action: $ref: "#/components/schemas/RuleAction" enabled: type: boolean default: true priority: type: integer minimum: 0 maximum: 10000 default: 100 comment: type: [string, "null"] RoutingRuleUpdate: type: object additionalProperties: false minProperties: 1 properties: name: type: string minLength: 1 maxLength: 128 kind: $ref: "#/components/schemas/RuleKind" value: type: string minLength: 1 action: $ref: "#/components/schemas/RuleAction" enabled: type: boolean priority: type: integer minimum: 0 maximum: 10000 comment: type: [string, "null"] RoutingRuleList: type: object additionalProperties: false required: [items, total] properties: items: type: array items: $ref: "#/components/schemas/RoutingRule" total: type: integer minimum: 0 ModeUpdateRequest: type: object additionalProperties: false required: [mode] properties: mode: $ref: "#/components/schemas/RoutingMode" ModeUpdateResponse: type: object additionalProperties: false required: [mode, applied_at, reconcile_id] properties: mode: $ref: "#/components/schemas/RoutingMode" applied_at: type: string format: date-time reconcile_id: type: string format: uuid GeoIpUpdateRequest: type: object additionalProperties: false properties: force: type: boolean default: false description: Игнорировать локальный кэш и обновить из RIPE Stat GeoIpUpdateResponse: type: object additionalProperties: false required: [status, source, prefix_count, updated_at, duration_ms] properties: status: type: string enum: [updated, cached, failed] source: $ref: "#/components/schemas/GeoIpSource" prefix_count: type: integer minimum: 0 updated_at: type: string format: date-time duration_ms: type: integer minimum: 0 ImportRequest: type: object additionalProperties: false required: [format, content] properties: format: type: string enum: [amneziawg-conf, wireguard-conf, json] content: type: string minLength: 1 apply: type: boolean default: false description: Применить импорт сразу, а не только вернуть разбор ImportResponse: type: object additionalProperties: false required: [imported_peers, imported_rules, warnings] properties: imported_peers: type: integer minimum: 0 imported_rules: type: integer minimum: 0 warnings: type: array items: type: string Device: type: object additionalProperties: false required: [name, ip, mac, vpn, ipsets, last_seen_at] properties: name: type: string ip: type: string mac: type: [string, "null"] vpn: type: boolean ipsets: type: array items: type: string last_seen_at: type: [string, "null"] format: date-time DeviceList: type: object additionalProperties: false required: [items, total] properties: items: type: array items: $ref: "#/components/schemas/Device" total: type: integer minimum: 0 LogEntry: type: object additionalProperties: false required: [ts, level, logger, message, fields] properties: ts: type: string format: date-time level: $ref: "#/components/schemas/LogLevel" logger: type: string message: type: string fields: type: object additionalProperties: true LogList: type: object additionalProperties: false required: [items, total, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/LogEntry" total: type: integer minimum: 0 next_cursor: type: [string, "null"] AuthLoginRequest: type: object additionalProperties: false required: [username, password] properties: username: type: string minLength: 1 password: type: string minLength: 1 writeOnly: true AuthSession: type: object additionalProperties: false required: [authenticated, username, expires_at, csrf_token] properties: authenticated: type: boolean username: type: [string, "null"] expires_at: type: [string, "null"] format: date-time csrf_token: type: [string, "null"] MetricsSample: type: object additionalProperties: false required: [rx_bytes, tx_bytes, rx_rate_bps, tx_rate_bps, handshake_age_s, active_peers] properties: rx_bytes: type: integer minimum: 0 tx_bytes: type: integer minimum: 0 rx_rate_bps: type: integer minimum: 0 tx_rate_bps: type: integer minimum: 0 handshake_age_s: type: [integer, "null"] minimum: 0 active_peers: type: integer minimum: 0 WsFrame: type: object additionalProperties: false required: [type, ts, data] properties: type: type: string enum: [metrics, status, error] ts: type: string format: date-time data: description: | MetricsSample при type=metrics, StatusResponse при type=status, Error при type=error. type: object additionalProperties: true