# 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. ### CHECK_TARGETS: не только «локальные» адреса В инструкции в примерах часто встречаются частные IP — это лишь **пример**. Формат **тот же** для любых целей, которые контейнер реально может проверить: | Тип | Примеры | |-----|---------| | Частные IP | `10.0.0.1`, `192.168.1.10`, `172.16.0.5` | | Публичные (глобальные) IP | `203.0.113.50`, адреса VPS/анонсеров | | Внутренние FQDN | `backend.prod.local`, `node1.dc.company.internal` | | Публичные домены | `api.example.com`, `origin.example.org` | | URL (режим `curl`) | `https://api.example.com/health`, `http://203.0.113.1:8080/ping` | Имя резолвится через **DNS из контейнера** (`dig`); для сопоставления с пулом Cloudflare используется полученный **A/AAAA**. Проверка — обычный **ping** к IP или **curl** к URL. Единственное условие: **из сети контейнера** до цели должен доходить трафик (маршрутизация, файрвол, `docker network`, VPN и т.д.). Публичные хосты в интернете обычно доступны из стандартного bridge; частные адреса за NAT без проброса/маршрута — нет, пока не настроите сеть. --- ## Требования - **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**, внутренние или **глобальные** имена, либо `http(s)://...` (подробнее — подраздел **«CHECK_TARGETS: не только „локальные“ адреса»** выше). | | `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 ``` ### Смешанные цели: частные IP, публичный домен, глобальный IP ```bash docker run --rm --cap-add=NET_RAW \ -e POOL_DOMAIN=app.example.com \ -e CHECK_TARGETS='10.0.0.1,origin.example.com,203.0.113.10' \ -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ cloudflare-balancer:latest ``` *(Замените `203.0.113.10` на реальный публичный IP бэкенда; `origin.example.com` — на любой FQDN, который резолвится и отвечает на ping из контейнера.)* ### Веб-статус + проброс порта + 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) — **Buildx**, `docker/login-action`, `docker/build-push-action`; пути и метаданные из контекста **`gitea.*`** (`repository`, `actor`, `sha`, `ref_name`, `server_url`). **Авторизация в registry** - Отдельный **`ACTIONS_PAT` не нужен**: в `docker login` используется встроенный **`gitea.token`** (токен текущего запуска workflow) и **`gitea.actor`**. У job задано **`permissions: packages: write`** — этого достаточно для push в Container Registry на том же Gitea (при несовместимости вашей версии см. [документацию Gitea Actions](https://docs.gitea.com/usage/actions/overview)). **Секрет для обновления контейнера (опционально)** - **`CONTAINER_UPDATE_URL`** — полный URL webhook после успешного push (например ссылка **Redeploy** в Portainer, другой оркестратор). Workflow делает **`curl` (GET)** по этому URL; если секрет пустой, шаг только пишет в лог и выходит. При необходимости другого метода (POST) измените шаг в `docker.yml`. **Переменные в workflow** - **`env.REGISTRY`** в начале `docker.yml` — хост registry **без** схемы (`https://`), как для `docker pull` (в файле задан пример `git.shts.su`; замените при переносе на другой инстанс). - **`IMAGE_REPO`:** `${{ gitea.repository }}` — полный путь образа: `${REGISTRY}/${{ gitea.repository }}`. **Триггеры:** push по всем веткам и тегам, **`workflow_dispatch`**. **Теги:** `latest`, «безопасное» имя ветки/тега (`/` → `-`), `sha-<12 hex>`. **Job:** `permissions: contents: read`, `packages: write`. --- ## Безопасность - Не коммитьте `.env` и реальные токены. - Минимизируйте права API Token одной зоной. - Не публикуйте порт статуса в интернет без **whitelist** и без понимания сетевой модели Docker. --- ## Windows и WSL2 Основные команды из этой инструкции рассчитаны на **bash под Linux**. На Windows удобнее запускать Docker **внутри WSL2** и выполнять те же `docker build` / `docker run` из Ubuntu (или другого дистрибутива в WSL). --- ## Лицензия Проект в репозитории пользователя — при необходимости добавьте файл `LICENSE` отдельно.