bbrkn/README.md
goodvin d81a4ab3b2 Migrate bbrkn from legacy ipset to nftables sets
The gateway (Archie) routes bbrkn domains via nft sets bbrkn_v4/bbrkn_v6
(inet filter), not the legacy ipset. bbrkn was still emitting dead
`ipset=/domain/bbrkn` directives (no such ipset exists) plus a 92-resolve
pointing at 8.8.8.8, both overridden by hand-maintained files on the host.
This makes bbrkn the generator of record for the real scheme.

generate-configs.sh:
- emit `nftset=/domain/$NFTSET_SPEC` (default 4#inet#filter#bbrkn_v4,
  6#inet#filter#bbrkn_v6) into 90-nftset.conf instead of ipset= into
  91-ipset-bbrkn.conf
- DNS_SERVER default 8.8.8.8 -> 127.0.0.1#5350 (host dnsmasq pihole
  delegates bbrkn domains to for VPN resolution + nftset capture)

deploy-to-gateway.sh: two targets, two instances
- 90-nftset.conf -> host /etc/dnsmasq.d (:5350), 92-resolve -> pihole
- full restart of both (dnsmasq SIGHUP does NOT re-read nftset=/server=)
- flush nft sets bbrkn_v4/bbrkn_v6 instead of `ipset flush bbrkn`
- add end-to-end nftset-capture health check via :5350
- rollback restores both files and restarts both instances

Makefile/workflow: rename IPSET_CONF->NFTSET_CONF, add NFTSET_TARGET_DIR
and HOST_DNSMASQ_SVC, DNS_SERVER=127.0.0.1#5350 (escaped `\#` in Make,
quoted in YAML), note runner is ephemeral (cold gekata crawl).

Docs: README + new CLAUDE.md describe the two-dnsmasq / nft-set model;
exit-node DPI failover (10.77.1.2/10.77.2.2) documented as external
(wg-ha.service), not owned by bbrkn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:55:16 +03:00

12 KiB
Raw Blame History

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
└── .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

Переменные окружения

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 и выполняет шаги:

  1. Проверяет наличие обоих сгенерированных конфигов.
  2. Создаёт timestamped-бэкапы 90-nftset.conf (host) и 92-resolve-bbrkn.conf (pihole).
  3. Копирует 90-nftset.confNFTSET_TARGET_DIR, 92-resolve-bbrkn.confTARGET_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. При ошибке на шагах 57 — автоматически откатывается: восстанавливает оба бэкапа и перезапускает оба инстанса со старой конфигурацией.

Запуск вручную

# Полный цикл
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