All checks were successful
Deploy DNS Configuration / deploy (push) Successful in 14m41s
Elements in bbrkn_v4/bbrkn_v6 carry a 1d timeout and are only refreshed when a query reaches the host dnsmasq on :5350, where the nftset= directives are applied. Idle domains — or ones answered from the pihole FTL cache — age out of the sets and their traffic silently falls back to the plain WAN route instead of the tunnel. warm-nftset.sh re-resolves every domain from the deployed 90-nftset.conf (base + related subdomains) directly against 127.0.0.1:5350, bypassing the FTL cache. Sourcing the domain list from the deployed config means there is no second list to keep in sync. Ping is off by default: packets originating on the gateway itself go through OUTPUT, never prerouting_mangle, so they leave unmarked and almost all time out — a misleading signal, not a real failure. Run on the gateway via `make warm` or hourly cron. Measured: 1059 domains in ~1m45s at PARALLEL=8, v4 526->2203, v6 305->1440 elements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
312 lines
17 KiB
Markdown
312 lines
17 KiB
Markdown
# bbrkn — DNS Bypass Config Generator
|
||
|
||
Автоматизирует генерацию конфигураций **Pi-hole / dnsmasq** для маршрутизации трафика заблокированных доменов через VPN-туннель (WireGuard) с помощью **nftables-сетов**.
|
||
|
||
## Как это работает
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[domains.txt] --> B[generate-configs.sh]
|
||
B -->|кэш .cache/api| B
|
||
B -->|MISS / EXPIRED| C[gekata: Chromium headless API]
|
||
C --> B
|
||
B --> D[90-nftset.conf]
|
||
B --> E[92-resolve-bbrkn.conf]
|
||
D --> F[deploy-to-gateway.sh]
|
||
E --> F
|
||
F -->|backup + copy + systemctl restart| G[host dnsmasq :5350<br/>/etc/dnsmasq.d/]
|
||
F -->|backup + copy + docker restart| H[Pi-hole :53]
|
||
F -->|health check + nftset check| H
|
||
F -.->|rollback если упал| G
|
||
|
||
subgraph "Трафик клиента"
|
||
K[Клиент] -->|DNS запрос| H
|
||
H -->|"server= → 127.0.0.1#5350"| G
|
||
G -->|"резолв через VPN + nftset="| N["nft set bbrkn_v4 / bbrkn_v6"]
|
||
N -->|ip daddr @bbrkn → mark| I[nft mangle]
|
||
I -->|policy routing| J[WireGuard exit<br/>10.77.1.2 / 10.77.2.2]
|
||
J --> L[Интернет]
|
||
end
|
||
```
|
||
|
||
> **Два инстанса dnsmasq.** Pi-hole (:53) отдаёт клиентов и делегирует bbrkn-домены host-инстансу на :5350 (`92-resolve`). Инстанс :5350 резолвит их через VPN и по директиве `nftset=` (`90-nftset.conf`) кладёт полученные IP в nft-сеты `bbrkn_v4`/`bbrkn_v6`. nft-mangle маркирует пакеты к этим IP, policy-routing уводит их в WireGuard. **Выбор из двух exit-нод (DPI-обход РКН) и failover — вне bbrkn**, ими управляет `wg-ha.service` на шлюзе.
|
||
|
||
---
|
||
|
||
## Классификация доменов
|
||
|
||
Для каждого домена из `domains.txt` скрипт обращается к Chromium headless API и классифицирует результат:
|
||
|
||
| Результат API | Действие |
|
||
|---|---|
|
||
| JSON с `relatedDomains` | **Сайт** — добавляется домен + все связанные субдомены |
|
||
| JSON с `error: ERR_NAME_NOT_RESOLVED` или `Timeout` | **Мёртвый домен** — пропускается, в конфиг не попадает |
|
||
| JSON с любой другой ошибкой (`ERR_CERT_*`, `ERR_CONNECTION_REFUSED` и т.д.) | **Сервис** — добавляется только сам домен без субдоменов (не HTTP-сервис: SMTP, VPN, игровой сервер и т.п.) |
|
||
| API недоступен (не 200 / не JSON после всех попыток) | **Fallback** — домен всё равно добавляется без субдоменов; фиксируется в разделе отчёта `API FAILURES` |
|
||
|
||
> Любой домен из `domains.txt` гарантированно попадает в финальный конфиг, даже если API недоступен.
|
||
|
||
### Формат выходных файлов
|
||
|
||
Домены группируются по базовому домену с комментарием:
|
||
|
||
`90-nftset.conf` (наполняет nft-сеты):
|
||
|
||
```
|
||
# example.com — 4 subdomains
|
||
nftset=/example.com/4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6
|
||
nftset=/static.example.com/4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6
|
||
nftset=/api.example.com/4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6
|
||
...
|
||
|
||
# some-service.com — 0 subdomains
|
||
nftset=/some-service.com/4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6
|
||
```
|
||
|
||
`92-resolve-bbrkn.conf` (делегирует те же домены host-инстансу :5350):
|
||
|
||
```
|
||
# example.com — 4 subdomains
|
||
server=/example.com/127.0.0.1#5350
|
||
server=/static.example.com/127.0.0.1#5350
|
||
...
|
||
```
|
||
|
||
---
|
||
|
||
## Структура проекта
|
||
|
||
```
|
||
bbrkn/
|
||
├── domains.txt # список доменов для обхода
|
||
├── Makefile # управление сборкой и деплоем
|
||
├── scripts/
|
||
│ ├── generate-configs.sh # генерация dnsmasq-конфигов
|
||
│ ├── deploy-to-gateway.sh # деплой в Pi-hole + rollback
|
||
│ └── warm-nftset.sh # прогрев nft-сетов по cron (на шлюзе)
|
||
└── .forgejo/workflows/deploy.yaml # CI/CD на self-hosted runner
|
||
```
|
||
|
||
---
|
||
|
||
## Makefile
|
||
|
||
```bash
|
||
make all # generate + deploy (полный цикл)
|
||
make generate # только сгенерировать конфиги
|
||
make deploy # только задеплоить в Pi-hole
|
||
make clean # удалить сгенерированные конфиги из /tmp
|
||
make cache-clean # очистить кэш API-ответов (.cache/api/)
|
||
make check # проверить синтаксис domains.txt
|
||
make warm # прогреть nft-сеты (запускать на шлюзе)
|
||
```
|
||
|
||
---
|
||
|
||
## Переменные окружения
|
||
|
||
### generate-configs.sh
|
||
|
||
| Переменная | По умолчанию | Описание |
|
||
|---|---|---|
|
||
| `DOMAINS_FILE` | `domains.txt` | Входной файл со списком доменов |
|
||
| `NFTSET_CONF` | `/tmp/90-nftset.conf` | Выходной файл директив `nftset=` |
|
||
| `NFTSET_SPEC` | `4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6` | Цель nftset: `<4\|6>#family#table#set` |
|
||
| `RESOLVE_CONF` | `/tmp/92-resolve-bbrkn.conf` | Выходной файл директив `server=` |
|
||
| `CHROME_SERVER` | `http://127.0.0.1:3000` | Адрес gekata (Chromium headless API) |
|
||
| `DNS_SERVER` | `127.0.0.1#5350` | Резолвер, которому pihole делегирует домены (host dnsmasq :5350) |
|
||
| `IGNORE_PARTS` | _(пусто)_ | Подстроки через пробел — домены с совпадением игнорируются |
|
||
| `CACHE_DIR` | `.cache/api` | Директория кэша API-ответов |
|
||
| `CACHE_TTL_DAYS` | `15` | Время жизни кэша в днях; `0` — отключить кэш |
|
||
| `STRICT_MODE` | `0` | `1` — завершать с кодом 1 при любых ошибках API |
|
||
| `DEBUG` | `0` | `1` — включить подробный лог |
|
||
| `DEBUG_LOG` | `/tmp/generate-configs.debug.log` | Путь к файлу отладочного лога |
|
||
|
||
### deploy-to-gateway.sh
|
||
|
||
| Переменная | По умолчанию | Описание |
|
||
|---|---|---|
|
||
| `NFTSET_CONF` | `/tmp/90-nftset.conf` | Сгенерированный файл `nftset=` |
|
||
| `NFTSET_TARGET_DIR` | `/etc/dnsmasq.d` | Директория host-инстанса dnsmasq (:5350) |
|
||
| `RESOLVE_CONF` | `/tmp/92-resolve-bbrkn.conf` | Сгенерированный файл `server=` |
|
||
| `TARGET_DIR` | `/opt/appdata/pihole/etc/dnsmasq.d` | Директория dnsmasq в Pi-hole (для `92-resolve`) |
|
||
| `DOCKER_CONTAINER` | `pihole` | Имя Docker-контейнера Pi-hole |
|
||
| `HOST_DNSMASQ_SVC` | `dnsmasq` | systemd-сервис host-инстанса dnsmasq |
|
||
| `NFT_SETS` | `inet filter bbrkn_v4;inet filter bbrkn_v6` | Сеты для flush после деплоя (через `;`) |
|
||
| `DNS_LISTEN_ADDR` | `127.0.0.1` | Адрес, на котором Pi-hole слушает DNS (:53) |
|
||
| `DNS_CHECK_DOMAIN` | `google.com` | Домен базового DNS health check |
|
||
| `NFTSET_CHECK_DOMAIN` | `chatgpt.com` | bbrkn-домен для end-to-end проверки nftset-захвата |
|
||
| `RESOLVER_ADDR` / `RESOLVER_PORT` | `127.0.0.1` / `5350` | Адрес host-инстанса для проверки захвата |
|
||
|
||
---
|
||
|
||
## Кэш API-ответов
|
||
|
||
При каждом запуске ответы Chromium API сохраняются в `$CACHE_DIR/<domain>.json`. При повторном запуске:
|
||
|
||
- Если файл кэша **свежее** `CACHE_TTL_DAYS` дней → ответ берётся из кэша, Chromium не запускается.
|
||
- Если файл **старше** TTL → перезапрашивается и кэш обновляется.
|
||
- Если кэш **повреждён** (невалидный JSON) → перезапрашивается автоматически.
|
||
|
||
> **Внимание:** forgejo-runner на шлюзе **эфемерный** — workspace (и `.cache/api`) между запусками не сохраняется. Поэтому первый CI-прогон после сброса кэша выполняет **холодный обход всех доменов через gekata** (~30-70 с на домен, последовательно) и работает долго. Для быстрых повторных прогонов держите тёплый `.cache/api` в постоянной директории (`CACHE_DIR`).
|
||
|
||
```bash
|
||
# Сбросить кэш и запросить всё заново
|
||
make cache-clean && make generate
|
||
|
||
# Отключить кэш для одного запуска
|
||
CACHE_TTL_DAYS=0 make generate
|
||
```
|
||
|
||
---
|
||
|
||
## Деплой и автооткат
|
||
|
||
`deploy-to-gateway.sh` деплоит в **два инстанса dnsmasq** и выполняет шаги:
|
||
|
||
1. Проверяет наличие обоих сгенерированных конфигов.
|
||
2. Создаёт timestamped-бэкапы `90-nftset.conf` (host) и `92-resolve-bbrkn.conf` (pihole).
|
||
3. Копирует `90-nftset.conf` → `NFTSET_TARGET_DIR`, `92-resolve-bbrkn.conf` → `TARGET_DIR`.
|
||
4. Перезапускает **оба** инстанса: `systemctl restart $HOST_DNSMASQ_SVC` + `docker restart $DOCKER_CONTAINER`. Полный рестарт обязателен — dnsmasq по **SIGHUP не перечитывает** директивы `nftset=`/`server=`.
|
||
5. Ждёт: host-инстанс слушает `:5350` (до 15 с) и pihole в состоянии `running` (до 30 с).
|
||
6. Сбрасывает nft-сеты `bbrkn_v4`/`bbrkn_v6` (устаревшие IP иначе висят до timeout 24 ч).
|
||
7. Health check: базовый DNS через pihole `:53` + end-to-end проверка nftset-захвата (резолв bbrkn-домена через `:5350` → сет наполнился).
|
||
8. При ошибке на шагах 5–7 — **автоматически откатывается**: восстанавливает оба бэкапа и перезапускает оба инстанса со старой конфигурацией.
|
||
|
||
---
|
||
|
||
## Прогрев nft-сетов (`warm-nftset.sh`)
|
||
|
||
Элементы сетов `bbrkn_v4`/`bbrkn_v6` живут по `timeout` (сейчас `1d`) и обновляются **только** когда запрос доходит до host-инстанса `:5350` — именно там применяются директивы `nftset=`. Если домен долго никто не открывал (или ответ отдал кэш pihole FTL), элемент истекает, и трафик к этому домену уходит мимо туннеля напрямую в WAN.
|
||
|
||
`scripts/warm-nftset.sh` закрывает эту дыру: заново резолвит **все** домены из задеплоенного `90-nftset.conf` (база + связанные субдомены) **напрямую** через `dig @127.0.0.1 -p 5350`, минуя кэш FTL, — каждый запрос гарантированно доходит до тегирующего инстанса.
|
||
|
||
> **Запускать только на шлюзе.** Инстанс `:5350` слушает `listen-address=127.0.0.1` и с других хостов LAN недоступен. Список доменов берётся из задеплоенного конфига, так что скрипт всегда работает с актуальным после последнего деплоя списком.
|
||
|
||
```bash
|
||
# разово, вручную
|
||
make warm
|
||
./scripts/warm-nftset.sh
|
||
|
||
# ежечасно по cron (пользователь на шлюзе, sudo без пароля для nft)
|
||
crontab -e
|
||
17 * * * * /path/to/bbrkn/scripts/warm-nftset.sh >/dev/null 2>&1
|
||
```
|
||
|
||
### Переменные окружения
|
||
|
||
| Переменная | По умолчанию | Описание |
|
||
|---|---|---|
|
||
| `NFTSET_CONF` | `/etc/dnsmasq.d/90-nftset.conf` | Задеплоенный конфиг — источник списка доменов |
|
||
| `RESOLVER` / `RESOLVER_PORT` | `127.0.0.1` / `5350` | Host-инстанс dnsmasq, применяющий `nftset=` |
|
||
| `PARALLEL` | `8` | Одновременных запросов к `:5350` |
|
||
| `DIG_TIMEOUT` / `DIG_TRIES` | `3` / `2` | Таймаут и число попыток на домен |
|
||
| `WARM_PING` | `0` | `1` — дополнительно пинговать первый A-адрес (только для отладки) |
|
||
| `STATE_DIR` | `~/.local/state/bbrkn` | Лог `warm.log` и lock-файл |
|
||
| `LOG_MAX_BYTES` | `4194304` | Порог ротации лога в `warm.log.1` |
|
||
|
||
Параллельные запуски блокируются через `flock` — медленный прогон не наложится на следующий по cron.
|
||
|
||
### Лог
|
||
|
||
```
|
||
2026-08-05T20:46:57+03:00 START domains=1059 conf=/etc/dnsmasq.d/90-nftset.conf v4=526 v6=305
|
||
2026-08-05T20:48:44+03:00 DONE ok=1036 noping=0 dnsfail=23 v4=526->2203 v6=305->1440
|
||
2026-08-05T20:48:44+03:00 dnsfail: azureedge.net cdn-telegram.org kinozal.tv ...
|
||
```
|
||
|
||
`v4=X->Y` — число элементов в сете до и после прогона (считается через `sudo -n nft list set`; без прав sudo вместо числа будет `?`, на сам прогрев это не влияет). Элементов больше, чем доменов — у CDN на один домен приходится несколько адресов. Домены в `dnsfail` не резолвятся вообще (мёртвые записи, NXDOMAIN) — кандидаты на удаление из `domains.txt`.
|
||
|
||
> `WARM_PING=1` на шлюзе почти всегда даёт `noping`: пакеты, рождённые на самом шлюзе, идут через `OUTPUT` и не проходят цепочку `prerouting_mangle`, то есть уходят в WAN без метки. Это диагностический режим, не показатель проблемы.
|
||
|
||
Полный прогон 1059 доменов при `PARALLEL=8` занимает ~1 мин 45 с.
|
||
|
||
---
|
||
|
||
## Запуск вручную
|
||
|
||
```bash
|
||
# Полный цикл
|
||
make all
|
||
|
||
# Только генерация (без деплоя)
|
||
make generate
|
||
|
||
# Dry-run — проверить что будет сгенерировано, ничего не записывать
|
||
./scripts/generate-configs.sh --dry-run
|
||
|
||
# Отладочный режим
|
||
DEBUG=1 make generate
|
||
|
||
# Строгий режим — завершить с ошибкой если часть доменов не опросилась
|
||
STRICT_MODE=1 make generate
|
||
```
|
||
|
||
---
|
||
|
||
## CI/CD
|
||
|
||
Workflow запускается при push в `main`, а также **вручную из UI** (`workflow_dispatch`).
|
||
|
||
```
|
||
push → main
|
||
└─ make clean
|
||
└─ make all
|
||
├─ generate-configs.sh (генерация с кэшем)
|
||
└─ deploy-to-gateway.sh (деплой + rollback)
|
||
└─ upload artifacts (конфиги + debug-лог, даже при падении)
|
||
```
|
||
|
||
Self-hosted runner работает непосредственно на шлюзе, поэтому деплой происходит локально без SSH.
|
||
|
||
---
|
||
|
||
## Пример отчёта
|
||
|
||
```
|
||
===== DEBUG REPORT =====
|
||
Input file: domains.txt
|
||
Raw input lines: 170
|
||
Processed lines: 170
|
||
Normalized OK: 165
|
||
Normalized skipped: 5
|
||
Ignored (IGNORE_PARTS): 12
|
||
|
||
Cache hits: 148 (TTL: 15d, dir: .cache/api)
|
||
Cache expired/refetched: 3
|
||
API success (sites): 11
|
||
API error/failed: 3
|
||
Related domains added: 87
|
||
Final unique domains: 256
|
||
|
||
---- VALID BASE SITES ----
|
||
example.com
|
||
another-site.org
|
||
...
|
||
|
||
---- API FAILURES (added without subdomains) ----
|
||
some-domain.com → api_failed_added_without_subdomains
|
||
...
|
||
|
||
===== END DEBUG REPORT =====
|
||
```
|
||
|
||
---
|
||
|
||
## Зависимости
|
||
|
||
| Инструмент | Где используется |
|
||
|---|---|
|
||
| `bash` 4.2+ | оба скрипта |
|
||
| `curl` | запросы к gekata API |
|
||
| `jq` | парсинг JSON-ответов |
|
||
| `docker` | перезапуск Pi-hole |
|
||
| `nft` | flush nft-сетов + проверка захвата после деплоя |
|
||
| `systemctl` | рестарт host-инстанса dnsmasq |
|
||
| `dig` или `nslookup` | DNS health check (опционально) |
|
||
|
||
---
|
||
|
||
## Лицензия
|
||
|
||
MIT
|