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>
11 KiB
План: самопроверка через 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) сохраняется, новый добавляется как опция агента.
Решение в двух строках
control-apiполучает ручку «с какого адреса ты меня видишь».- Агент получает настройку
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(проверка обратной совместимости).
Выкладка
control-apiс новой ручкой (поведение не меняется): пересборка образа, перезапуск.- Агенты на ВМ-валидаторах: новый
bin/validator-agentиmethods: [control_api, ip_echo]в их конфиге. Один валидатор для начала, проверитьdetailв событиях (matched (control_api)), затем остальные.
Решения пользователя (2026-10-02)
- Подключение валидаторов к
control-api— напрямую (без обратного прокси):trusted_proxiesне нужен. - Ручка
observed-ip— открытая. - Достаточно одной успешной самопроверки любым из способов; при внешнем размещении API способ через ручку API — в приоритете (первый в списке).