|
All checks were successful
Deploy DNS Configuration / deploy (push) Successful in 18m57s
|
||
|---|---|---|
| .forgejo/workflows | ||
| scripts | ||
| .gitignore | ||
| CLAUDE.md | ||
| domains.txt | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
bbrkn — DNS Bypass Config Generator
Автоматизирует генерацию конфигураций Pi-hole / dnsmasq для маршрутизации трафика заблокированных доменов через VPN-туннель (WireGuard) с помощью nftables-сетов.
Как это работает
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
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).
# Сбросить кэш и запросить всё заново
make cache-clean && make generate
# Отключить кэш для одного запуска
CACHE_TTL_DAYS=0 make generate
Деплой и автооткат
deploy-to-gateway.sh деплоит в два инстанса dnsmasq и выполняет шаги:
- Проверяет наличие обоих сгенерированных конфигов.
- Создаёт timestamped-бэкапы
90-nftset.conf(host) и92-resolve-bbrkn.conf(pihole). - Копирует
90-nftset.conf→NFTSET_TARGET_DIR,92-resolve-bbrkn.conf→TARGET_DIR. - Перезапускает оба инстанса:
systemctl restart $HOST_DNSMASQ_SVC+docker restart $DOCKER_CONTAINER. Полный рестарт обязателен — dnsmasq по SIGHUP не перечитывает директивыnftset=/server=. - Ждёт: host-инстанс слушает
:5350(до 15 с) и pihole в состоянииrunning(до 30 с). - Сбрасывает nft-сеты
bbrkn_v4/bbrkn_v6(устаревшие IP иначе висят до timeout 24 ч). - Health check: базовый DNS через pihole
:53+ end-to-end проверка nftset-захвата (резолв bbrkn-домена через:5350→ сет наполнился). - При ошибке на шагах 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 недоступен. Список доменов берётся из задеплоенного конфига, так что скрипт всегда работает с актуальным после последнего деплоя списком.
# разово, вручную
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 с.
Запуск вручную
# Полный цикл
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