Files
cloud-ip-validator/docs/changes/2026-10-02_03-06_self-check-control-api-plan.md
ayurishchevandClaude Sonnet 5.5 abbee9a08a 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>
2026-10-02 03:24:20 +03:00

11 KiB
Raw Permalink Blame History

План: самопроверка через 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] (всё как сейчас).

Конфигурация агента

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 — в приоритете (первый в списке).