Files
cloudflare_balancer/README.md
T

310 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <container_id>
```
В логах используются **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` отдельно.