cc3640f13c
- docs/openapi.yaml: все эндпоинты ТЗ, единый формат ошибок {code,message,details},
writeOnly для private_key/preshared_key/password, WS-кадры metrics/stream.
- requirements-contract.txt: зависимости контрактных тестов (jsonschema,
openapi-spec-validator, PyYAML) до мержа каркаса PR #47.
- .gitea/workflows/contract-openapi.yml: lint + контрактный тест + проверка
актуальности сгенерированного TS-клиента.
1255 lines
36 KiB
YAML
1255 lines
36 KiB
YAML
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 <token>` для 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 <token>`, либо 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 <token>`)"
|
|
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
|