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>
111 lines
11 KiB
Markdown
111 lines
11 KiB
Markdown
# План: самопроверка через 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 —
|
||
**в приоритете** (первый в списке).
|