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