# Cloudflare DNS pool balancer Лёгкий контейнер на **Alpine**: периодически проверяет доступность бэкендов (**ping** или **HTTP**), синхронизирует записи **A / AAAA** для одного DNS-имени (**пул**) в Cloudflare через **API Token**, пишет цветные логи и опционально отдаёт **HTML-страницу статуса** с ограничением по IP. --- ## Оглавление - [Как это работает](#как-это-работает) - [Требования](#требования) - [Сборка образа (Linux / bash)](#сборка-образа-linux--bash) - [Быстрый старт](#быстрый-старт) - [Переменные окружения](#переменные-окружения) - [Cloudflare: API Token](#cloudflare-api-token) - [Примеры запуска](#примеры-запуска) - [Docker Compose](#docker-compose) - [Веб-интерфейс статуса и whitelist](#веб-интерфейс-статуса-и-whitelist) - [Логи](#логи) - [Ограничения](#ограничения) - [Устранение неполадок](#устранение-неполадок) - [Сборка в CI (Gitea Actions)](#сборка-в-ci-gitea-actions) - [Безопасность](#безопасность) - [Windows и WSL2](#windows-и-wsl2) --- ## Как это работает 1. Для каждой цели из `CHECK_TARGETS` определяется IP (прямой адрес, резолв имени или хоста из URL). 2. Выполняется проверка: **ICMP ping** к IP или **curl** по URL (см. `CHECK_MODE`). 3. IP считается **желательным в пуле**, только если **все** цели, которые резолвятся в этот IP, прошли проверку. 4. Читаются текущие записи **A/AAAA** с именем `POOL_DOMAIN` в Cloudflare. 5. Лишние записи удаляются, недостающие создаются (TTL и proxied из переменных окружения). Два разных смысла **curl**: - **Проверка бэкендов** (`CHECK_MODE=curl`) — запросы к URL из целей / шаблону `CURL_URL_TEMPLATE`. - **Порт `STATUS_HTTP_PORT`** — только встроенная страница статуса внутри контейнера, не health-check. --- ## Требования - **Docker Engine** на Linux (или эквивалент с Linux-контейнерами). - У контейнера должен быть доступ в интернет (Cloudflare API, ping/curl к бэкендам). - Для **ping** из пользователя `nobody` в образе выставлен `cap_net_raw` на `/usr/bin/ping`. Если ping не работает, запускайте с **`--cap-add=NET_RAW`** (в `docker-compose.yml` это уже указано). --- ## Сборка образа (Linux / bash) Из корня репозитория: ```bash cd /path/to/cloudflare_balancer docker build -t cloudflare-balancer:latest . ``` Проверка локально (без реальных секретов контейнер сразу завершится с ошибкой — это нормально): ```bash docker build -t cloudflare-balancer:latest . && echo "сборка OK" ``` --- ## Быстрый старт Минимально нужны: `POOL_DOMAIN`, `CHECK_TARGETS`, `CLOUDFLARE_API_TOKEN`. Рекомендуется передавать секреты через **`--env-file`**: ```bash cp .env.example .env # отредактируйте .env docker run --rm \ --cap-add=NET_RAW \ --env-file .env \ cloudflare-balancer:latest ``` --- ## Переменные окружения | Переменная | Обязательно | По умолчанию | Описание | |------------|-------------|--------------|----------| | `POOL_DOMAIN` | да | — | DNS-имя пула (все **A/AAAA** с этим именем управляются скриптом). | | `CHECK_TARGETS` | да | — | Список целей через **запятую**: IP, hostname или `http(s)://...`. | | `CLOUDFLARE_API_TOKEN` | да* | — | Bearer-токен Cloudflare. | | `CLOUDFLARE_API_KEY` | нет | — | Алиас для токена (если не задан `CLOUDFLARE_API_TOKEN`). | | `CLOUDFLARE_ZONE_ID` | нет | — | ID зоны; если пусто — поиск по имени зоны. | | `CLOUDFLARE_ZONE_NAME` | нет | — | Имя зоны (apex), например `example.com`; если пусто — эвристика: **две последние метки** `POOL_DOMAIN`. | | `CHECK_INTERVAL_SEC` | нет | `30` | Пауза между циклами, сек. | | `CHECK_MODE` | нет | `ping` | `ping` или `curl`. | | `PING_COUNT` | нет | `2` | Число ICMP-запросов. | | `PING_TIMEOUT_SEC` | нет | `2` | Таймаут ping (см. `iputils-ping`). | | `CURL_URL_TEMPLATE` | нет | `http://%s/` | Для не-URL целей: один плейсхолдер `%s` заменяется на цель. | | `CURL_MAX_TIME_SEC` | нет | `5` | `--max-time` для curl. | | `CURL_CONNECT_TIMEOUT_SEC` | нет | `3` | `--connect-timeout`. | | `CURL_OK_MIN` | нет | `200` | Минимальный допустимый HTTP-код. | | `CURL_OK_MAX` | нет | `399` | Максимальный допустимый HTTP-код. | | `DEFAULT_TTL` | нет | `300` | TTL при создании записи (для **прокси**-записей Cloudflare сам использует авто-TTL). | | `DEFAULT_PROXIED` | нет | `false` | `true` / `false` — оранжевое облако. | | `ENABLE_STATUS_HTTP` | нет | `0` | `1` — включить HTTP-страницу статуса. | | `STATUS_HTTP_PORT` | нет | `8080` | Порт **внутри контейнера** для страницы статуса. | | `STATUS_HTTP_ALLOW_IPS` | нет | *(пусто)* | Whitelist: IP и IPv4 **CIDR** через запятую. Пусто = слушать только **127.0.0.1**. | \* Обязателен токен или алиас. > **Важно:** эвристика зоны по двум последним меткам не подходит для всех публичных суффиксов (например некоторые зоны второго уровня). В сомнениях задайте `CLOUDFLARE_ZONE_ID` или `CLOUDFLARE_ZONE_NAME`. --- ## Cloudflare: API Token 1. Cloudflare Dashboard → **My Profile** → **API Tokens** → **Create Token**. 2. Шаблон **Edit zone DNS** или кастомный минимум: - **Zone** — **DNS** — **Edit** - **Zone** — **Zone** — **Read** - Ограничить **конкретной зоной** (рекомендуется). 3. Скопируйте токен в `.env` как `CLOUDFLARE_API_TOKEN`. Официальная документация API: [Cloudflare API](https://developers.cloudflare.com/api/). --- ## Примеры запуска ### Только ping, без UI ```bash docker run --rm --cap-add=NET_RAW \ -e POOL_DOMAIN=app.example.com \ -e CHECK_TARGETS='10.0.0.1,10.0.0.2' \ -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ -e CHECK_INTERVAL_SEC=20 \ cloudflare-balancer:latest ``` ### Режим curl и шаблон URL ```bash docker run --rm --cap-add=NET_RAW \ -e POOL_DOMAIN=app.example.com \ -e CHECK_TARGETS='backend1.internal,backend2.internal' \ -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ -e CHECK_MODE=curl \ -e CURL_URL_TEMPLATE='http://%s:8080/health' \ cloudflare-balancer:latest ``` ### Полный URL как цель ```bash docker run --rm --cap-add=NET_RAW \ -e POOL_DOMAIN=app.example.com \ -e CHECK_TARGETS='https://node1.example.com/health' \ -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ -e CHECK_MODE=curl \ cloudflare-balancer:latest ``` ### Веб-статус + проброс порта + whitelist При доступе **с хоста Docker** клиентский IP в контейнере часто совпадает с адресом **шлюза bridge** (часто `172.17.0.1` или подсеть `172.17.0.0/16`). Добавьте её или конкретный IP в whitelist. ```bash docker run --rm --cap-add=NET_RAW \ -p 8080:8080 \ -e POOL_DOMAIN=app.example.com \ -e CHECK_TARGETS='10.0.0.1' \ -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ -e ENABLE_STATUS_HTTP=1 \ -e STATUS_HTTP_PORT=8080 \ -e STATUS_HTTP_ALLOW_IPS='127.0.0.1,172.17.0.0/16,10.0.0.0/8' \ cloudflare-balancer:latest ``` Откройте в браузере: `http://127.0.0.1:8080/` (если whitelist это разрешает). --- ## Docker Compose ```bash cp .env.example .env # заполните CLOUDFLARE_API_TOKEN и остальное docker compose build docker compose up -d docker compose logs -f ``` Файл [docker-compose.yml](docker-compose.yml) подключает `.env` и добавляет **`NET_RAW`** для ping. --- ## Веб-интерфейс статуса и whitelist - **`STATUS_HTTP_PORT`** — порт HTTP-сервера **статуса**, не имеет отношения к `CHECK_MODE=curl`. - Если **`STATUS_HTTP_ALLOW_IPS` пустой** — `socat` слушает **127.0.0.1** (доступ с хоста через `-p` без NAT к loopback контейнера обычно **не** попадёт на 127.0.0.1 внутри контейнера). Для просмотра со стороны хоста задайте whitelist и `0.0.0.0`-bind (это делается автоматически при непустом whitelist). - В whitelist поддерживаются **точные IPv4** и **IPv4 CIDR**; для IPv6 — в основном точное совпадение (CIDR для v6 в обработчике минимальный). --- ## Логи ```bash docker logs -f ``` В логах используются **ANSI-цвета** (удобно в терминале). Блоки: заголовок цикла, строки **OK/FAIL** по целям, действия **DELETE/POST** в API. --- ## Ограничения - Одна строка в `CHECK_TARGETS` с запятой внутри URL может сломать разбор — избегайте запятых в целях или используйте только один хост на цель. - Несколько имён, резолвящихся в **один IP**, считаются одним бэкендом: IP остаётся в пуле только если **все** такие проверки успешны. - Записи **A/AAAA** для `POOL_DOMAIN`, которые **не соответствуют** IP из текущего резолва целей, **не удаляются** (в лог и HTML выводится предупреждение «вне списка»). --- ## Устранение неполадок | Симптом | Что проверить | |--------|----------------| | Ошибка авторизации Cloudflare | Токен, срок, зона в области действия токена. | | «Зона не найдена» | Задайте `CLOUDFLARE_ZONE_ID` или корректный `CLOUDFLARE_ZONE_NAME`. | | Все ping FAIL | `NET_RAW` / `cap_add`; маршрутизация сети контейнера до бэкендов; ICMP может быть запрещён файрволом. | | curl всегда FAIL | URL, TLS, коды ответа (`CURL_OK_MIN` / `CURL_OK_MAX`), таймауты. | | UI отдаёт 403 | Whitelist: добавьте IP шлюза Docker или вашу подсеть. | | После старта пул «пустой» | При всех FAIL записи удаляются — проверьте доступность целей и корректность IP. | --- ## Сборка в CI (Gitea Actions) 1. На сервере Gitea включите **Actions** и подключите **runner** ([документация](https://docs.gitea.com/usage/actions/overview)). 2. Workflow: [.gitea/workflows/docker.yml](.gitea/workflows/docker.yml). 3. По умолчанию выполняется **`docker build`**. 4. Опциональная публикация в **Container Registry** Gitea: задайте секреты репозитория: - `REGISTRY_URL` — хост (можно с `https://`, скрипт обрежет протокол для `docker login`). - `REGISTRY_USER` — логин пользователя Gitea. - **`PACKAGE_TOKEN`** — токен Gitea с правом публиковать пакеты (используется как **пароль** в `docker login`; рекомендуемый способ). - `REGISTRY_PASSWORD` — опционально, только если не задан `PACKAGE_TOKEN` (устаревший/альтернативный секрет). Имя образа в registry: `//cloudflare-balancer:`. > В Gitea создайте **Personal Access Token** (или токен с нужным scope) с доступом к **пакетам** / записи в Container Registry и сохраните его в секрете **`PACKAGE_TOKEN`**. > **Почему шаги не «Skipped»:** в workflow нет условия `if: secrets.… != ''` — в Gitea Actions (как и в GitHub) такие проверки часто **всегда ложны**, и публикация молча пропускается. Вместо этого один шаг всегда выполняется: при отсутствии секретов он выводит причину и завершается с кодом 0. На **pull request** push в registry намеренно не делается (только `docker build`). --- ## Безопасность - Не коммитьте `.env` и реальные токены. - Минимизируйте права API Token одной зоной. - Не публикуйте порт статуса в интернет без **whitelist** и без понимания сетевой модели Docker. --- ## Windows и WSL2 Основные команды из этой инструкции рассчитаны на **bash под Linux**. На Windows удобнее запускать Docker **внутри WSL2** и выполнять те же `docker build` / `docker run` из Ubuntu (или другого дистрибутива в WSL). --- ## Лицензия Проект в репозитории пользователя — при необходимости добавьте файл `LICENSE` отдельно.