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:
1 parent
146259cabb
commit
abbee9a08a
23 files changed
+765
-35
No files matched your search
+32
-4
@@ -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)"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+14
-5
@@ -701,10 +701,17 @@ docker run -d --platform linux/amd64 --cap-add NET_RAW --name validator-agent \
|
||||
| `VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS` | нет | `5` |
|
||||
|
||||
`--cap-add NET_RAW` обязателен для ICMP-проверок, как и у `prober`.
|
||||
`self_check.ip_echo_urls` в переменные не вынесен — при отсутствии в
|
||||
конфиге агент сам подставляет дефолт (`api.ipify.org`, `ifconfig.me`);
|
||||
свой список задавайте через смонтированный конфиг вместо шаблона, если
|
||||
нужно переопределить.
|
||||
`self_check.ip_echo_urls` и `self_check.methods` в переменные не вынесены —
|
||||
при отсутствии в конфиге агент сам подставляет дефолты (`api.ipify.org`,
|
||||
`ifconfig.me` и `methods: [ip_echo]`); свои значения задавайте через
|
||||
смонтированный конфиг вместо шаблона, если нужно переопределить.
|
||||
`methods` — способы самопроверки в порядке приоритета (`ip_echo`,
|
||||
`control_api`), достаточно подтверждения любым. Способ `control_api`
|
||||
спрашивает у control-api, с какого адреса он видит валидатора
|
||||
(`GET /agents/{id}/observed-ip`); при внешнем размещении control-api
|
||||
рекомендуется `[control_api, ip_echo]`. Ограничение: если control-api
|
||||
достижим из облака по внутренней сети, он увидит приватный адрес
|
||||
валидатора и этот способ всегда даст несовпадение — используйте `ip_echo`.
|
||||
|
||||
### Обновление образов после изменения кода
|
||||
|
||||
@@ -804,7 +811,9 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
|
||||
через floating IP, который в данный момент привязан к валидатору.
|
||||
- `validator-agent` → внешние IP-echo сервисы из `self_check.ip_echo_urls`
|
||||
(по умолчанию `api.ipify.org`, `ifconfig.me`) — **обязательно вне
|
||||
облака**: это и есть механизм self-check (см.
|
||||
облака**: это и есть механизм self-check способом `ip_echo` (при
|
||||
`self_check.methods` с `control_api` достаточно ещё и доступа к control-api
|
||||
по внешней сети; см.
|
||||
[DIAGRAMS.md](DIAGRAMS.md#2-поток-данных-от-валидатора-к-целевому-серверу-egress-проверка)).
|
||||
Если валидатор не может достучаться ни до одного из этих адресов,
|
||||
self-check никогда не пройдёт и IP будет бесконечно возвращаться в
|
||||
|
||||
+9
-2
@@ -742,7 +742,9 @@ curl -s -X PUT http://<control-api>:8080/api/v1/admin/config/orchestrator \
|
||||
дело обычно в self-check: он запрашивает внешние (вне облака) сервисы из
|
||||
`self_check.ip_echo_urls` в конфиге валидатора (по умолчанию
|
||||
`api.ipify.org`, `ifconfig.me`) — если у ВМ-валидатора нет исходящего
|
||||
доступа в интернет к этим адресам, запрос не проходит вообще, и агент
|
||||
доступа в интернет к этим адресам, запрос не проходит вообще (при
|
||||
`self_check.methods: [control_api, ip_echo]` агент сперва спросит адрес у
|
||||
control-api, и проверка может пройти и без IP-echo), и агент
|
||||
даже не может *сообщить* результат control-api (ни успешный, ни
|
||||
неуспешный) — тогда статус реально зависает до истечения
|
||||
`orchestrator.lease_ttl_seconds`, после чего адрес возвращается в
|
||||
@@ -763,7 +765,12 @@ https://api.ipify.org`) и логи `journalctl -u validator-agent` на пре
|
||||
облака (см. `self_check.ip_echo_urls`) — запрос к чему-либо внутри
|
||||
проекта (в том числе к самому control-api, если он в той же внутренней
|
||||
сети) покажет приватный адрес валидатора независимо от того, правильно
|
||||
ли привязан FIP, и всегда будет давать ложный провал.
|
||||
ли привязан FIP, и всегда будет давать ложный провал. Это относится и к
|
||||
способу `control_api` (`self_check.methods`): он корректен только когда
|
||||
валидатор ходит к control-api через внешнюю сеть; при внутреннем доступе в
|
||||
`detail` будет подсказка про приватный адрес — оставьте `ip_echo`. В
|
||||
`detail` события `self_check_result` указан сработавший способ
|
||||
(`matched (control_api)`) либо причина по каждому способу.
|
||||
|
||||
**Площадка (`site-N`) никогда не отчитывается по конкретному IP.**
|
||||
Сперва проверьте статус самой площадки — `GET
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# План: самопроверка через control-api (дополнительный способ сверки публичного IP)
|
||||
|
||||
> Дата: 2026-10-02 03:06 · Статус: **реализовано и проверено** (юнит-тесты, локальный e2e с `ip_echo` и с `control_api`) (решения пользователя — в конце)
|
||||
|
||||
## Зачем
|
||||
|
||||
Самопроверка агента подтверждает, что исходящий трафик валидатора идёт через выданный Floating IP: агент спрашивает свой
|
||||
публичный адрес у внешнего IP-echo сервиса и сверяет с назначенным адресом. Сейчас это единственный способ, и он хрупкий:
|
||||
1 октября таймауты `https://ifconfig.me/ip` дали волну провалов self-check (33 случая за вечер).
|
||||
|
||||
`control-api` в текущем развёртывании стоит **во внешнем окружении** (вне облака), валидаторы подключаются к нему **напрямую**.
|
||||
Значит, соединение валидатора с ним выходит наружу через Floating IP, и `control-api` сам видит публичный адрес источника.
|
||||
Это даёт второй способ сверки без сторонних сервисов: агент спрашивает у `control-api`, с какого адреса тот его видит.
|
||||
|
||||
Требование: **существующий способ (IP-echo) сохраняется**, новый добавляется как опция агента.
|
||||
|
||||
## Решение в двух строках
|
||||
|
||||
1. `control-api` получает ручку «с какого адреса ты меня видишь».
|
||||
2. Агент получает настройку `self_check.methods` — список способов в порядке приоритета; по умолчанию `[ip_echo]` (всё как сейчас).
|
||||
|
||||
## Конфигурация агента
|
||||
|
||||
```yaml
|
||||
self_check:
|
||||
timeout_seconds: 10
|
||||
methods: [control_api, ip_echo] # по умолчанию [ip_echo]
|
||||
ip_echo_urls: [...] # без изменений
|
||||
```
|
||||
|
||||
- Допустимые значения: `ip_echo` (текущий способ), `control_api` (новый). Неизвестное значение — ошибка при старте агента.
|
||||
- **Самопроверка успешна, если её подтвердил любой из способов.** Способы пробуются по порядку приоритета (первый —
|
||||
главный); остановка на первом успешном. К следующему способу переходим и при отсутствии ответа (ошибка, таймаут,
|
||||
404/5xx), и при несовпадении адреса. Провал — только если не подтвердил ни один способ; в `detail` попадает причина по
|
||||
каждому способу.
|
||||
- Внутри способа `ip_echo` поведение прежнее: URL перебираются по порядку, переход к следующему URL только при ошибке.
|
||||
- Пустой список или отсутствие ключа → `[ip_echo]`. Агент без новой настройки ведёт себя ровно как раньше.
|
||||
- Для внешнего размещения `control-api` в примерах и рекомендациях стоит `[control_api, ip_echo]`: способ через API в приоритете.
|
||||
- Итог в `detail`: `detected_egress_ip=<ip> matched (control_api)`; видно в событиях адреса и в дашборде.
|
||||
|
||||
## Control API
|
||||
|
||||
**Новая ручка:** `GET /api/v1/agents/{id}/observed-ip` → `200 {"ip":"90.156.213.5","source":"remote_addr"}`.
|
||||
|
||||
- Уровень доступа — **открыто**, как `heartbeat` и `assignment`: ручка отдаёт только адрес самого вызывающего, секретов нет.
|
||||
- `{id}` должен быть известным валидатором, иначе `404` (чтобы ручка не превращалась в публичный «узнай свой IP»).
|
||||
- Адрес берётся только из `r.RemoteAddr`, приводится к каноничному виду (`::ffff:1.2.3.4` → `1.2.3.4`).
|
||||
- Заголовки `X-Forwarded-For`/`X-Real-IP` **не учитываются**: подключение прямое, а доверие к заголовку позволило бы
|
||||
валидатору подделать адрес и пройти проверку. Если появится обратный прокси, понадобится отдельная настройка
|
||||
доверенных прокси — сейчас она не нужна (решение 1).
|
||||
|
||||
## Агент (`internal/agentcore`)
|
||||
|
||||
- Новая функция `detectViaControlAPI`: `GET /api/v1/agents/{id}/observed-ip` на `control_api_url`.
|
||||
- **Новое TCP-соединение на каждый вызов** (отдельный `http.Transport` с `DisableKeepAlives`). Это ключевой момент:
|
||||
соединение, открытое до привязки Floating IP (heartbeat, assignment), остаётся в старом NAT-состоянии и покажет
|
||||
прежний адрес; общий клиент `apiclient` использовать нельзя.
|
||||
- Токен агентов в этот запрос не нужен (ручка открытая) и не отправляется.
|
||||
- Таймаут `self_check.timeout_seconds` (сейчас 10 с) действует **на каждый способ отдельно**: при общем дедлайне зависший
|
||||
первый способ (приоритетный `control_api`) съел бы всё время, и запасной не успел бы ответить. Общий предел — таймаут × число способов.
|
||||
- `detectPublicIP` заменяется перебором `methods` по приоритету; `fetchIPEcho` не меняется.
|
||||
- Диагностика: если `control-api` вернул **частный** адрес (RFC 1918 и т. п.), в `detail` пишется подсказка: «control-api
|
||||
доступен по внутренней сети, самопроверка через него невозможна; используйте ip_echo».
|
||||
|
||||
## Ограничение способа (важно для документации)
|
||||
|
||||
Способ `control_api` корректен только если соединение валидатора с `control-api` **выходит через внешнюю сеть**
|
||||
(SNAT Floating IP). Если `control-api` достижим из облака по внутренней сети, он увидит частный адрес валидатора, и
|
||||
этот способ всегда будет давать несовпадение (при `[control_api, ip_echo]` проверка пройдёт по `ip_echo`).
|
||||
Если порт `control-api` опубликован через Docker, при выкладке проверить, что ручка показывает внешний адрес клиента, а
|
||||
не адрес шлюза Docker.
|
||||
|
||||
## Откат и совместимость
|
||||
|
||||
- Новый агент + старый `control-api`: ручки нет (`404`), при `methods: [control_api, ip_echo]` агент переходит на `ip_echo`.
|
||||
- Старый агент + новый `control-api`: ничего не меняется, ручка просто не вызывается.
|
||||
- Откат: убрать `methods` из конфига агента (или вернуть `[ip_echo]`) и перезапустить агент.
|
||||
- Схема БД и протокол `self-check` (`POST /agents/{id}/self-check`) не меняются.
|
||||
|
||||
## Затрагиваемые файлы
|
||||
|
||||
| Файл | Изменение |
|
||||
|---|---|
|
||||
| `internal/config/config.go` | `SelfCheckCfg.Methods`, дефолт `[ip_echo]` и проверка значений |
|
||||
| `internal/httpapi/routes.go`, `handlers_agent.go` | маршрут и обработчик `observed-ip` |
|
||||
| `internal/agentcore/agentcore.go` | `detectViaControlAPI`, перебор `methods` по приоритету |
|
||||
| `configs/validator-agent.example.yaml` | пример и комментарии |
|
||||
| `docs/API.md`, `docs/SETUP.md`, `docs/USAGE.md`, `README.md` | описание ручки, опции, ограничения |
|
||||
| `bin/validator-agent`, `bin/control-api`, `SHA256SUMS` | пересборка |
|
||||
|
||||
## Тесты
|
||||
|
||||
- **config:** дефолт `[ip_echo]`; допустимые значения; ошибка на неизвестном способе.
|
||||
- **httpapi:** прямой адрес; IPv4-mapped IPv6; заголовок `X-Forwarded-For` игнорируется; неизвестный валидатор → `404`.
|
||||
- **agentcore:** порядок способов; успех второго способа после ошибки или несовпадения первого; провал, когда не
|
||||
подтвердил ни один; каждый вызов открывает новое соединение (тестовый сервер считает соединения); подсказка про частный адрес.
|
||||
- **e2e:** `scripts/run-local-e2e.sh` остаётся на `ip_echo` (проверка обратной совместимости).
|
||||
|
||||
## Выкладка
|
||||
|
||||
1. `control-api` с новой ручкой (поведение не меняется): пересборка образа, перезапуск.
|
||||
2. Агенты на ВМ-валидаторах: новый `bin/validator-agent` и `methods: [control_api, ip_echo]` в их конфиге. Один валидатор
|
||||
для начала, проверить `detail` в событиях (`matched (control_api)`), затем остальные.
|
||||
|
||||
## Решения пользователя (2026-10-02)
|
||||
|
||||
1. Подключение валидаторов к `control-api` — **напрямую** (без обратного прокси): `trusted_proxies` не нужен.
|
||||
2. Ручка `observed-ip` — **открытая**.
|
||||
3. Достаточно **одной успешной самопроверки любым из способов**; при внешнем размещении API способ через ручку API —
|
||||
**в приоритете** (первый в списке).
|
||||
Reference in new issue
Block a user