A failed self-check returned the address to the queue and freed the
validator in the database, but left the floating IP attached to the
validator's port. Every later association on that port then failed with
409 ("fixed IP already has a floating IP"), so one failed self-check
poisoned a validator for good; on 2026-10-01 all 20 validators were
poisoned within 23 minutes after ifconfig.me timeouts.
SelfCheckResult now disassociates the floating IP before requeueing, and
ignores a late failed report for an address the validator no longer
holds (it could belong to another validator by then).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cloud IP Validator
Система проверки освобождённых публичных IPv4-адресов перед их повторной выдачей. Каждый адрес временно привязывается как Floating IP
к ВМ-валидатору в облаке (OpenStack) и проверяется в двух направлениях одновременно: исходящий трафик валидатора (egress: HTTPS/ICMP/опционально SSH
до внешних целей) и входящая доступность самого адреса с нескольких независимых внешних площадок (inbound: TCP 22/80/443/8080 + ICMP).
Итог по каждому адресу — pass/partial/fail; полная история проверок сохраняется в реестре даже после удаления адреса из очереди.
Быстрый старт
go build ./... && go test ./... # сборка и юнит-тесты
scripts/run-local-e2e.sh # офлайн-прогон всей системы: mock OpenStack, без облака и интернета
cd deploy/docker
cp .env.example .env
cp control-api/control-api.docker.example.yaml control-api/control-api.docker.yaml
docker compose up -d --build # весь стенд на одной машине в mock-режиме (профили control-plane, dashboard, prober, validator)
- UI:
http://<хост>:8090/, API:http://<хост>:8080/api/v1, проверка живости:GET /healthz. - Docker-стенд работает с
openstack.mode: mockи тестовыми адресами203.0.113.10–12; реальный OpenStack и учётные данные не нужны. - Реальный стенд (systemd или Docker на нескольких хостах) — по шагам в docs/SETUP.md.
Конфигурация
Каждый компонент читает свой YAML (примеры — configs/*.example.yaml). Учётные данные OpenStack в YAML не хранятся — только имена переменных окружения.
control-api (control-api.yaml)
| Ключ | Назначение |
|---|---|
server.listen_addr |
Адрес API (:8080) |
database.path |
Файл SQLite (/var/lib/cloud-ip-validator/control-api.db) |
openstack.mode |
real или mock (встроенная заглушка OpenStack для разработки и тестов) |
openstack.auth_method |
token (готовый токен проекта из OS_TOKEN, сам не обновляется) или password (логин/пароль Keystone, токен перевыпускается автоматически) |
openstack.*_env |
Имена переменных окружения: OS_AUTH_URL, OS_PROJECT_ID, OS_REGION_NAME, OS_INTERFACE, OS_TOKEN либо OS_USERNAME/OS_USER_DOMAIN_NAME/OS_PASSWORD |
orchestrator.poll_interval_seconds |
Период такта оркестратора (5) |
orchestrator.self_check_timeout_seconds, max_self_check_retries |
Ожидание self-check валидатора (60) и число его повторов (3) |
orchestrator.checking_window_seconds |
Сколько ждать результаты проверок, прежде чем подвести итог (120) |
orchestrator.lease_ttl_seconds, max_retries |
Лизинг адреса за валидатором (180) и число возвратов в очередь при его истечении (3) |
orchestrator.heartbeat_timeout_seconds |
После скольких секунд тишины валидатор или площадка считаются потерянными (30) |
orchestrator.fip_settle_seconds |
Только начальное значение: пауза между привязкой FIP и self-check; далее управляется на лету. Должно выполняться fip_settle_seconds + self_check_timeout_seconds < lease_ttl_seconds |
orchestrator.fip_scan_interval_seconds |
Периодический скан Floating IP (0 — выключен). Не выполняется, пока включён автоматический цикл |
orchestrator.fip_scan_timeout_seconds |
Предел всего сканирования Floating IP (1800) |
openstack.list_page_size, list_page_retries, request_timeout_seconds |
Скан читает Floating IP страницами (200), повторяя страницу при обрыве/5xx/429 (5 раз); таймаут одного запроса к OpenStack (60 с) |
auth.admin_token_env, auth.agent_token_env |
Имена переменных окружения с токеном администратора (CONTROL_API_ADMIN_TOKEN) и токеном агентов (CONTROL_API_AGENT_TOKEN). Значения в YAML не хранятся; пустой токен — соответствующий уровень API открыт (с предупреждением в логе) |
aggregation.missing_counts_as_fail |
Отсутствие ответа источника засчитывается как провал (true) |
validators, sites, check_types, targets, inbound_checks |
Начальная загрузка пустой БД: валидаторы (validator_id + os_port_id), внешние площадки, типы и цели egress-проверок, порты inbound-проверок. Дальше источник истины — БД, правки через API/UI |
ip_addresses |
Адреса, которые доливаются в очередь при каждом старте (только новые) |
Остальные компоненты
| Компонент | Ключи |
|---|---|
validator-agent |
validator_id, control_api_url, control_api_token_env (CONTROL_API_AGENT_TOKEN), poll_interval_seconds, self_check.* (таймаут, ip_echo_urls), checks.* (таймауты HTTPS/ICMP, число ICMP-пакетов, ssh.*) |
prober |
site_id, control_api_url, control_api_token_env (CONTROL_API_AGENT_TOKEN), poll_interval_seconds, checks.* (таймауты TCP/ICMP, число ICMP-пакетов) |
admin-dashboard |
server.listen_addr (:8090), control_api.base_url, control_api.timeout_seconds, control_api.token_env (ADMIN_DASHBOARD_CONTROL_API_TOKEN), auth.username_env / password_env / session_secret_env (ADMIN_DASHBOARD_USERNAME / _PASSWORD / _SESSION_SECRET), auth.session_ttl_minutes (480), overview.last_completed_count (20), overview.poll_interval_seconds (5) |
Секреты (токены, пароль дашборда, ключ сессии) задаются только переменными окружения; генерация — openssl rand -hex 32. Подробности и порядок включения — docs/SETUP.md.
Настройки, меняющиеся на лету (пауза перед self-check, глубина истории, типы inbound-проверок, автоматический цикл, валидаторы, площадки, цели), хранятся в БД и правятся через API или страницу /settings.
Архитектура
| Слой | Технологии |
|---|---|
| Backend | Go 1.26, net/http (маршруты Go 1.22), SQLite (чистый Go-драйвер modernc.org/sqlite, WAL, одно соединение), встроенные миграции 0001–0008 |
| Облако | Интерфейс FloatingIPClient: реальный клиент OpenStack Neutron и MockClient |
| UI | Серверный рендеринг (Go-шаблоны) + htmx и Alpine.js, шрифты и скрипты лежат в репозитории, внешних зависимостей нет |
| Сборка | Статические бинарники (CGO_ENABLED=0) в bin/, Docker-образы, systemd-юниты |
| Компонент | Роль | Состояние | Где работает |
|---|---|---|---|
control-api |
Ведёт очередь адресов и реестр их истории, назначает валидаторов, агрегирует результаты. Единственная точка, с которой общаются все остальные компоненты | Хранит (SQLite) | Одна управляющая машина |
validator-agent |
Привязывает к себе выданный Floating IP и прогоняет egress-проверки до внешних целей | Без состояния | Каждая ВМ-валидатор в облаке |
prober |
Проверяет входящую доступность адреса снаружи (TCP/ICMP) — с независимой от облака сети | Без состояния | Каждая внешняя тестовая площадка |
admin-dashboard |
Браузерная админ-панель — то же, что API control-api, но графически. Опционален |
Без состояния | Любая машина с доступом к control-api |
Все четыре общаются только через HTTP API control-api. Pull-модель: validator-agent и prober сами опрашивают control-api, он к ним не обращается.
Схемы control/data plane — docs/DIAGRAMS.md.
cmd/ control-api validator-agent prober admin-dashboard
internal/ config db orchestrator httpapi openstack dashboard agentcore probercore checkrunner apiclient
db/migrations/ схема SQLite (0001–0008)
configs/ примеры конфигураций компонентов
deploy/ docker/ (compose, Dockerfile, RUN.txt) systemd/ (юниты)
rxprod-compose/ compose реального стенда (control-api на :8081, дашборд на :8091)
scripts/ run-local-e2e.sh httpstub/ (заглушки целей для e2e)
bin/ готовые бинарники linux/amd64 и SHA256SUMS
docs/ документация и планы доработок
Модель данных и правила
ip_queue (текущая очередь) · ip_registry (все адреса, когда-либо бывшие в очереди) · checks (результаты по циклам) · events (журнал) · validators · sites · target_groups · check_types · settings · inbound_checks_settings · auto_cycle.
Очередь и состояния
- Путь адреса:
queued→assigning_fip→awaiting_self_check→checking→aggregating→doneилиfailed. Все переходы, кроме команд оператора, выполняет оркестратор сам по таймеру. - Терминальные состояния:
done,failed,occupied.occupied— Floating IP к моменту привязки уже занят чужим портом: цикл проверки не стартует,overall_resultпуст, это неfail. - Оператор может отменить проверку (
failed+cancelled), перепроверить завершённый адрес (возврат вqueued) или удалить адрес из очереди. - Адреса обрабатываются в порядке
sequence; каждый свободный валидатор получает следующийqueued-адрес под лизинг. Истёкший лизинг возвращает адрес в очередь, послеmax_retries— вfailed.
Итоговый результат
pass— прошли все проверки: исходящие и все настроенные площадки по всем портам и ICMP.partial— часть прошла, часть нет.fail— не прошла ни одна проверка или адрес не дошёл до проверок (self-check не подтвердился).cancelled— остановлено оператором.- Итог подводится, когда отчитались валидатор и все настроенные площадки либо истекло
checking_window_seconds. Молчание источника — провал (aggregation.missing_counts_as_fail). - Площадки опциональны: с пустым списком
sitesитог строится только по egress-проверкам.
Реестр
- Запись реестра создаётся при первой постановке адреса и не удаляется: она переживает удаление из очереди и повторное добавление.
- История проверок хранится по циклам;
history_retention_cyclesограничивает глубину (0 — без ограничения), сама запись реестра остаётся.
Автоматический цикл (опционально, по умолчанию выключен)
- Один цикл: очистить очередь → просканировать Floating IP (фаза
scanning, фоновое задание) → дождаться, пока все адреса станут терминальными → паузаinterval_seconds→ заново. Пауза считается от завершения цикла. Сканирование не блокирует оркестратор; чужое идущее сканирование цикл дожидается, а не присоединяется к нему. interval_seconds— по умолчанию 3600, минимум 60;max_run_seconds— максимальное ожидание проверок (0 — без лимита), по истечении исходtimeout.- Исходы цикла:
completed,no_free_ips,timeout,error,stopped. Опустевшая очередь посреди цикла считается завершением. - Состояние хранится в БД и переживает перезапуск; выключение не прерывает идущие проверки. Подробности — docs/USAGE.md.
Удаление и сканирование
- Удаление (точечное, списком, «очистить всё») убирает строку очереди, но не историю в реестре.
- Скан Floating IP ставит в очередь только свободные адреса (не привязанные ни к одному порту); уже идущие проверки не трогаются. Он идёт в фоне и читает облако страницами: подходит и для тысяч адресов (на стенде 6441 Floating IP читаются ≈ 1,5–2 мин). Адреса ставятся в очередь только после полного обнаружения (кусками по 500, по возрастанию IP); при сбое чтения очередь не меняется.
dry_run=true/ «Пробное сканирование» считает адреса, не меняя очередь. - Пропускная способность: ≈ 50 с на адрес на валидатор — очередь из 6440 адресов это ≈ 18 ч на 5 валидаторах, ≈ 9 ч на 10 (рычаги: число валидаторов и
fip_settle_seconds).
API (/api/v1)
| Область | Эндпоинты |
|---|---|
| Валидатор | POST /agents/register, POST /agents/{id}/heartbeat, GET /agents/{id}/assignment, POST /agents/{id}/self-check|events|results|complete |
| Пробер | POST /probers/register, POST /probers/{site_id}/heartbeat, GET /probers/{site_id}/assignments, POST /probers/{site_id}/results |
| Очередь | GET /admin/status, GET|POST /admin/ips (limit/offset/state/q/result/order — постранично), GET /admin/ips/{ip}, POST /admin/ips/{ip}/cancel, DELETE /admin/ips/{ip}, POST /admin/ips/delete|clear, POST|GET /admin/ips/scan (фоновый скан: 202, dry_run, wait; статус и прогресс) |
| Реестр | GET /admin/registry (limit/offset/q/last_result — постранично), GET /admin/registry/{ip} |
| Автоцикл | GET|PUT /admin/auto-cycle, POST /admin/auto-cycle/start|stop |
| Конфигурация | /admin/config/validators, /sites, /targets, /check-types, GET|PUT /admin/config/orchestrator, GET|PUT /admin/config/inbound-checks |
| Служебное | GET /admin/validators, GET /healthz |
Соглашения
- Тело запросов и ответов — JSON. Успех —
200,204— когда данных нет (например, у валидатора нет назначения). - Ошибки —
4xx/5xxс телом{"error": "..."}; нет или неверен токен —401(сWWW-Authenticate: Bearer), неизвестная сущность —404, конфликт состояния —409, неверные данные —400, ошибка OpenStack при скане —502. - Токен передаётся заголовком
Authorization: Bearer <токен>(docs/API.md). - Времена — RFC 3339. Полная спецификация и примеры
curl— docs/API.md.
Безопасность
- Доступ к API — два статических Bearer-токена (без срока жизни, из переменных окружения, сравнение в константное время):
Уровень Токен Методы admin CONTROL_API_ADMIN_TOKENвсе /api/v1/admin/*agent CONTROL_API_AGENT_TOKENзапись результатов валидатора и пробера: self-check,events,results,completeоткрыто — GET /healthz,register,heartbeatи получение задания (GET assignment/assignments)Токены разные: административный не открывает методы агентов, и наоборот. Валидатор и пробер получают настройку и задание без токена, но не могут отправить результат без токена агентов. - Пустой токен — уровень открыт (обратная совместимость):
control-apiстартует с предупреждением в логе. На реальном стенде задайте оба токена и ограничьте доступ на уровне сети (docs/SETUP.md); токены идут открытым текстом без TLS — публикуйте через reverse-proxy с TLS. Включать токен агентов нужно после его раздачи валидаторам и проберам (порядок). - Дашборд закрыт логином и паролем (один администратор; пароль и ключ сессии — из env). Сессия — подписанная cookie (
HttpOnly,SameSite=Strict, без состояния на сервере), CSRF-защита поOrigin, 5 неудачных входов за 10 минут с одного IP →429. Без заданных логина/пароля дашборд открыт (с предупреждением в логе). Дашборд ходит в API с токеном администратора. Подробности — docs/DASHBOARD.md. - Учётные данные OpenStack передаются только через переменные окружения процесса (
EnvironmentFile=в systemd,OS_*в Docker) и не попадают в YAML; режимpasswordперевыпускает токен сам, режимtoken— нет. - Компоненты работают по pull-модели: на валидаторах и площадках не нужно открывать входящие порты для
control-api. - Бинарники статические, без
cgo; целостность проверяетсяsha256sum -c bin/SHA256SUMS.
Публикация и эксплуатация
- Бинарники. Готовые linux/amd64 лежат в
bin/и не обновляются автоматически: после правок кода пересоберите их и обновитеSHA256SUMS(команды — docs/SETUP.md); Dockerfile копируют именноbin/*. - systemd. Юниты в
deploy/systemd/; уcontrol-api—EnvironmentFileс учётными данными OpenStack. - Docker.
deploy/docker/docker-compose.yml+docker-compose.override.yml(dev, mock) илиdocker-compose.prod.yml(без публикации портов,restart: unless-stopped). Какие сервисы поднимаются на хосте, задаётCOMPOSE_PROFILES:control-plane,dashboard,prober,validator. БД — volumecloud-ip-validator-db. - Реальный стенд.
rxprod-compose/— compose с готовыми образами, собственнымcontrol-api.yamlи каталогом БДcapi-db/;.envс учётными данными в репозиторий не входит. - Миграции применяются при старте
control-api; версия схемы —PRAGMA user_version. Начальная загрузка (validators,sites,targets,check_types,inbound_checks) выполняется только в пустые таблицы. - Остановка (
SIGTERM) корректно завершает HTTP-сервер и фоновые циклы. Состояние автоцикла и очереди сохраняется в БД.
Интерфейс
Страницы admin-dashboard (подробно — docs/DASHBOARD.md):
| Страница | Назначение |
|---|---|
/overview |
Счётчики и прогресс («Готово D из T», оценка времени), «в работе», «в очереди: Q», «последние завершённые», поиск по IP и фильтр по статусу, индикатор скана и автоцикла; работает на счётчиках и ограниченных списках, поэтому быстрый и при тысячах адресов |
/ips, /ips/{ip} |
Очередь постранично с поиском и фильтром на сервере: добавление адресов, «Сканировать Floating IP» (панель прогресса) и «Пробное сканирование», перепроверка, отмена, удаление (страница или «все N по фильтру», «Очистить всё»); детали и события адреса |
/registry, /registry/{ip} |
Реестр всех адресов (постранично) и полная история проверок адреса; поиск, фильтр и страница сохраняются в адресной строке |
/validators, /sites, /targets, /check-types |
Управление валидаторами, внешними площадками, группами целей и типами проверок |
/settings |
Панель «Автоматический цикл», пауза перед self-check, глубина истории, TCP-порты и ICMP для inbound-проверок |
- Порядок блоков на
/overviewфиксирован: статистика → фильтр → таблицы; поллится только блок таблиц, поэтому набранный в фильтре текст не сбрасывается. - Ошибки control-api показываются баннером; при недоступном API страница остаётся рабочей.
- Тёмная и светлая темы, переключатель в шапке.
Тесты
go build ./... && go vet ./... && go test ./... # юнит-тесты: db, orchestrator, httpapi, dashboard, agentcore, probercore, checkrunner, openstack
go test -race ./internal/orchestrator ./internal/httpapi ./internal/dashboard ./internal/db
scripts/run-local-e2e.sh # сквозной прогон: lease-reclaim, перепроверка, автоматический цикл
Юнит-тесты используют временную SQLite и MockClient, внешних ресурсов не требуют. E2E поднимает все компоненты локальными процессами с включёнными токенами (проверяет 401/открытые маршруты и работу агента и пробера с токеном) и завершается ненулевым кодом при провале проверок автоцикла — docs/LOCAL_E2E.md.
Документация
| Документ | Для чего |
|---|---|
| docs/SETUP.md | Развёртывание с нуля: бинарники или сборка из исходников, конфигурация, systemd и Docker/docker-compose — пошагово |
| docs/USAGE.md | Повседневная работа: очередь, сканирование Floating IP, автоматический цикл, статус, реестр и история, разбор результатов |
| docs/API.md | Спецификация HTTP API control-api и примеры запросов (curl) |
| docs/DASHBOARD.md | Устройство admin-dashboard: страницы, поиск и фильтр, обработка ошибок |
| docs/DIAGRAMS.md | Диаграммы потоков данных: control plane, egress-проверка, телеметрия |
| docs/LOCAL_E2E.md | Полностью офлайн-прогон всей системы одним скриптом |
| docs/changes/ | Планы доработок и отчёты ревью с отметкой времени в имени файла (последняя: скан при тысячах адресов) |
| docs/CONTROL_DATA_PLANE.html | Презентационные схемы control/data plane для docker-compose-деплоя — открыть в браузере |
История изменений
Дизайн крупных доработок зафиксирован в планах docs/PLAN_*.md; остальное — в истории git (git log).
| Изменение | Документ |
|---|---|
Веб-панель администратора admin-dashboard |
план · описание |
| Динамическое управление конфигурацией и очередью через API | план · API |
| Удаление адресов: точечное, массовое, «очистить всё» | план |
Пауза перед self-check после привязки Floating IP (fip_settle_seconds) |
план · USAGE |
| Механизм аутентификации OpenStack-клиента (token / password) | план |
| Дата | Веха | Документ |
|---|---|---|
| 2026-10-01 | Скан Floating IP при тысячах адресов: фоновый постраничный скан с прогрессом, фаза scanning в автоцикле, постраничные /ips и /registry, «Обзор» на счётчиках |
план · ревью и тесты · USAGE · API |
| 2026-10-01 | Аутентификация: токены администратора и агентов для API, логин и пароль для дашборда | план · ревью и тесты · API |
| 2026-10-01 | Автоматический цикл проверок по сценарию: очистка → скан FIP → проверка → пауза | USAGE · API |
| 2026-09-23 | Сканирование Floating IP и устойчивый реестр адресов с настраиваемой глубиной истории | USAGE |
| 2026-09-23 | Поиск по IP и фильтр по статусу на «Обзоре» и «Реестре» | DASHBOARD |
| 2026-09-23 | Пошаговое руководство по развёртыванию в Docker | SETUP |
| 2026-09-18 | Массовая перепроверка в дашборде; compose реального стенда rxprod-compose |
DASHBOARD |
| 2026-09-13 | Состояние occupied: пропуск цикла для уже занятого Floating IP; схемы control/data plane |
DIAGRAMS |
| 2026-08-26 | Управление внешними площадками и heartbeat пробера, новый UI дашборда, переключатель темы | DASHBOARD |