Add self-check via control-api (self_check.methods)

control-api is hosted outside the cloud and validators reach it directly,
so it sees the floating IP as the connection's source address. New open
route GET /api/v1/agents/{id}/observed-ip returns that address (taken only
from the TCP peer; forwarding headers are ignored so a validator cannot
forge it).

The agent gets self_check.methods, a priority-ordered list of ip_echo
(unchanged) and control_api; the default stays [ip_echo]. The self-check
passes when any method confirms the address; the next method is tried on
no answer and on a mismatch. Each method has its own timeout so a hung
first method cannot starve the fallback, and control_api uses a new TCP
connection per call (a connection opened before the floating IP was
attached would keep reporting the old address).

Also: docker agent template/env, example config, docs, plan in
docs/changes, e2e script switch E2E_SELF_CHECK_METHODS, rebuilt
bin/control-api and bin/validator-agent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-02 03:24:20 +03:00
1 parent 146259cabb
commit abbee9a08a
23 files changed
+765 -35

No files matched your search

+32 -4
View File
@@ -41,10 +41,10 @@ JSON, базовый префикс прикладных методов — `/ap
|---|---|---|
| **admin** | `CONTROL_API_ADMIN_TOKEN` | все `/api/v1/admin/*` (очередь, реестр, автоцикл, `config/*`) |
| **agent** | `CONTROL_API_AGENT_TOKEN` | запись результатов: `POST /agents/{id}/self-check`, `/events`, `/results`, `/complete` и `POST /probers/{site_id}/results` |
| **открыто** | — | `GET /healthz`; `POST /agents/register`, `POST /agents/{id}/heartbeat`, `GET /agents/{id}/assignment`; `POST /probers/register`, `POST /probers/{site_id}/heartbeat`, `GET /probers/{site_id}/assignments` |
| **открыто** | — | `GET /healthz`; `POST /agents/register`, `POST /agents/{id}/heartbeat`, `GET /agents/{id}/assignment`, `GET /agents/{id}/observed-ip`; `POST /probers/register`, `POST /probers/{site_id}/heartbeat`, `GET /probers/{site_id}/assignments` |
- Токены разные: токен администратора **не** подходит для методов агентов, и наоборот.
- Валидатор и пробер могут без токена зарегистрироваться, слать heartbeat и забирать задание (настройку); отправка результатов без токена агентов — `401`.
- Валидатор и пробер могут без токена зарегистрироваться, слать heartbeat и забирать задание (настройку); валидатор также может спросить, с какого адреса его видит control-api (`observed-ip`); отправка результатов без токена агентов — `401`.
- Имена переменных меняются в секции `auth` конфига control-api (`admin_token_env`, `agent_token_env`); сами значения в YAML не хранятся.
- Ответ при отказе: `401 {"error": "unauthorized"}` с заголовком `WWW-Authenticate: Bearer`.
- Токен не задан (пустая переменная) — уровень открыт; в логе control-api при старте предупреждение. Токены нужно генерировать случайными: `openssl rand -hex 32`.
@@ -145,6 +145,30 @@ curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://<control-api>:8080/api/v1
`check_config` — уже развёрнутая конфигурация проверок (тип + список
целей), агенту не нужно самому сопоставлять группы целей.
### `GET /api/v1/agents/{id}/observed-ip`
С какого адреса control-api видит соединение валидатора. Используется
self-check способом `control_api` (`self_check.methods` в
`validator-agent.yaml`) как альтернатива внешнему IP-echo сервису. Уровень
доступа — открыто: отдаётся только адрес самого вызывающего.
Ответ `200`:
```json
{"ip": "203.0.113.10", "source": "remote_addr"}
```
- Адрес берётся только из адреса TCP-соединения (`RemoteAddr`), приведённого
к каноничному виду (`::ffff:1.2.3.4` → `1.2.3.4`). Заголовки
`X-Forwarded-For` / `X-Real-IP` **не учитываются**: иначе валидатор мог бы
подделать адрес и пройти проверку. Метод рассчитан на прямое подключение
без обратного прокси.
- `404`, если `validator_id` не зарегистрирован.
- Способ корректен, только если соединение выходит через внешнюю сеть
(SNAT Floating IP). Если control-api достижим из облака по внутренней
сети, он увидит приватный адрес валидатора. Если порт control-api
опубликован через Docker, проверьте, что ручка показывает внешний адрес
клиента, а не адрес шлюза Docker.
### `POST /api/v1/agents/{id}/self-check`
Отчёт о результате self-check — подтверждение, что исходящий трафик
@@ -152,7 +176,11 @@ curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://<control-api>:8080/api/v1
определяет это **сам**, обращаясь к внешнему (снаружи облака) IP-echo
сервису (`self_check.ip_echo_urls` в `validator-agent.yaml`, например
`api.ipify.org`) и сравнивая ответ с `ip_address` из задания — control-api
в этом определении не участвует. Важно, что ресурс должен быть именно
в этом определении не участвует. Дополнительно можно включить способ
`control_api` (`self_check.methods`): агент спрашивает у control-api через
`GET /agents/{id}/observed-ip`, с какого адреса тот его видит. Способы
пробуются по приоритету, достаточно подтверждения любым из них; без
настройки работает только IP-echo. Важно, что ресурс должен быть именно
внешним: OpenStack применяет SNAT через Floating IP только к трафику,
уходящему через внешнюю сеть, поэтому обращение к чему-либо внутри
проекта (в том числе к самому control-api, если он в той же внутренней
@@ -165,7 +193,7 @@ curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://<control-api>:8080/api/v1
"ip_id": 42,
"detected_egress_ip": "203.0.113.10",
"success": true,
"detail": "matched"
"detail": "matched (control_api)"
}
```