diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 842ef2f..5538184 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -15,28 +15,21 @@ jobs: DOMAINS_FILE: domains.txt # Временные конфиги (куда пишутся generate-configs.sh) - NFTSET_CONF: /tmp/90-nftset.conf + IPSET_CONF: /tmp/91-ipset-bbrkn.conf RESOLVE_CONF: /tmp/92-resolve-bbrkn.conf - # Резолвер, которому pihole делегирует bbrkn-домены (host dnsmasq :5350). - # Кавычки обязательны — '#' иначе может трактоваться как комментарий. - DNS_SERVER: "127.0.0.1#5350" + # DNS-сервер для резолвинга + DNS_SERVER: 8.8.8.8 # Gekata CHROME_SERVER: http://10.77.1.2:3000 - # nftset-таргет: host-инстанс dnsmasq (:5350) грузит /etc/dnsmasq.d - NFTSET_TARGET_DIR: /etc/dnsmasq.d - - # resolve-таргет: pihole (docker) делегирует bbrkn-домены на :5350 + # Директория конфигов на шлюзе TARGET_DIR: /opt/appdata/pihole/etc/dnsmasq.d # Имя контейнера Pi-hole DOCKER_CONTAINER: pihole - # systemd-сервис host-инстанса dnsmasq - HOST_DNSMASQ_SVC: dnsmasq - IGNORE_PARTS: "doubleclick yandex mail.ru bigsv.ru mts.ru rambler.ru yadro.ru aif.ru vkvideo.ru tns-counter.ru st.vk.com vk.com" # Кэш API-ответов (self-hosted runner сохраняет workspace между запусками) @@ -58,6 +51,6 @@ jobs: with: name: dnsmasq-configs path: | - ${{ env.NFTSET_CONF }} + ${{ env.IPSET_CONF }} ${{ env.RESOLVE_CONF }} ${{ env.DEBUG_LOG }} diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 424ec83..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,53 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What this is - -Generates **Pi-hole / dnsmasq** configs that route blocked domains through a WireGuard VPN via **nftables sets**. Input is `domains.txt`; output is two dnsmasq files deployed to **two different dnsmasq instances** on the Archie gateway (see Architecture). README.md (Russian) is the authoritative reference — read it for the classification table, env-var descriptions, and report format. - -> **Routing model (Archie gateway):** client → pihole (docker, `:53`) delegates bbrkn domains via `92-resolve-bbrkn.conf` (`server=/domain/127.0.0.1#5350`) to a **host dnsmasq on `:5350`**, which resolves via VPN and applies `90-nftset.conf` (`nftset=…#bbrkn_v4,…#bbrkn_v6`) to populate nft sets `inet filter bbrkn_v4`/`bbrkn_v6`. An nft mangle chain marks packets to those set IPs (`0x1`/`0x2`) and policy-routing sends them out a WireGuard exit. Dual-exit DPI-bypass failover (`10.77.1.2`/`10.77.2.2`) is handled by `wg-ha.service`, **external to bbrkn** — bbrkn only maintains domain→set membership. - -## Commands - -```bash -make all # generate + deploy (full cycle) -make generate # generate configs into /tmp only -make deploy # deploy existing configs to Pi-hole -make check # validate domains.txt syntax (regex per line) -make clean # remove generated /tmp configs -make cache-clean # wipe .cache/api/ - -./scripts/generate-configs.sh --dry-run # generate, print report, write nothing -DEBUG=1 make generate # verbose log to $DEBUG_LOG -STRICT_MODE=1 make generate # exit 1 if any domain's API query failed -CACHE_TTL_DAYS=0 make generate # bypass API cache for one run -``` - -No test suite. Verification = `--dry-run` + reading the DEBUG REPORT (see README) and diffing generated `/tmp` configs. - -## Architecture - -Two bash scripts, driven by env vars (defaults in each script's header, overridden by Makefile and CI): - -**`scripts/generate-configs.sh`** — classifies each domain by querying a Chromium headless API (`$CHROME_SERVER/domains?domain=`), builds the ordered domain list, writes both config files in one batch. Key logic: - -- **Classification** (drives whether subdomains are added): API JSON with `relatedDomains` → *site* (base + related subdomains); `ERR_NAME_NOT_RESOLVED`/`Timeout` → *dead*, skipped entirely; any other API error → *service* (base only, no subdomains); API fully unreachable → *fallback* (base only, recorded under `API FAILURES`). **Invariant: every domain in `domains.txt` reaches the output unless it's dead.** -- **State is held in bash associative arrays** (`DOM_ROLE`, `EXPANDED`, `SOURCES`, `SITE_RELATED`, …). The main loop deliberately avoids subshells so counters survive — do not wrap counter-mutating code in `$(...)` or pipes. -- **Caching**: successful API responses cached to `$CACHE_DIR/.json`, reused while newer than `CACHE_TTL_DAYS`; corrupt or expired entries refetch. `find -mtime +N` decides freshness. -- **Ordering pass** emits each base domain, a `# domain — N subdomains` comment, then its not-yet-written related domains; a related domain is attributed to whichever base comes first alphabetically. A final uniqueness check `exit 5`s (config NOT written) if any domain would appear twice — treat that as a real bug in the ordering logic, not a data issue. -- Output lines: `nftset=//$NFTSET_SPEC` (NFTSET_CONF → `90-nftset.conf`; default `NFTSET_SPEC=4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6`) and `server=//$DNS_SERVER` (RESOLVE_CONF → `92-resolve-bbrkn.conf`; `DNS_SERVER` default `127.0.0.1#5350`, the host resolver pihole delegates to). - -**`scripts/deploy-to-gateway.sh`** — deploys to **two targets**: `90-nftset.conf` → host `$NFTSET_TARGET_DIR` (`/etc/dnsmasq.d`, the `:5350` instance) and `92-resolve-bbrkn.conf` → pihole `$RESOLVE_TARGET_DIR` (`/opt/appdata/pihole/etc/dnsmasq.d`). Flow: timestamped-backup both → copy → **`systemctl restart $HOST_DNSMASQ_SVC` + `docker restart $DOCKER_CONTAINER`** (a full restart is required — dnsmasq **SIGHUP does not re-read `nftset=`/`server=` directives**, only hosts files) → wait for `:5350` listening + pihole `running` → `nft flush set` on `bbrkn_v4`/`bbrkn_v6` → DNS health check via pihole `:53` → end-to-end nftset-capture check (resolve a bbrkn domain via `:5350`, assert the set populated). **On any failure it calls `rollback()`**, restoring both backups and restarting both instances. Edits must preserve that rollback path — a broken deploy can break DNS for every client on the gateway. - -## Deployment / CI - -`.forgejo/workflows/deploy.yaml` runs on push to `main` (and `workflow_dispatch`) on a **self-hosted `forgejo-runner` on the gateway itself** — no SSH, deploy is local. Note: the runner is **ephemeral** (no persisted `.cache/api` was found on the host despite the older "persists workspace" claim), so a fresh CI run performs a **cold gekata crawl of every domain** (~30-70s each, serialized) — the first run after a cache reset is slow and heavy on gekata. CI runs `make clean && make all` and uploads the generated configs + debug log as artifacts even on failure. CI-only values (`NFTSET_TARGET_DIR`, `TARGET_DIR`, `CHROME_SERVER`, `DOCKER_CONTAINER`, `HOST_DNSMASQ_SVC`, `DNS_SERVER`, `IGNORE_PARTS`) live in the workflow `env:` block, not the scripts. - -## Gotchas - -- `domains.txt` is the primary data file; commits are typically domain additions. Run `make check` after editing it. -- `IGNORE_PARTS` is space-separated substrings; any domain (base or related) containing one is dropped. -- **`#` gotcha**: `DNS_SERVER=127.0.0.1#5350` — `#` starts a comment in Makefiles (escape as `127.0.0.1\#5350`) and can start one in YAML (quote it). The scripts themselves take the plain value. -- The nft sets (`bbrkn_v4`/`bbrkn_v6`, `inet filter`) and mangle/routing rules live on the gateway (`/etc/nftables.d/`, Ansible-managed) — bbrkn only fills the sets via dnsmasq, it does not define them. -- Requires `bash` 4.2+ (associative arrays, `${var,,}`), `curl`, `jq`, `docker`, `nft`, `systemctl`; `dig`/`nslookup` optional (health checks skipped if absent). diff --git a/Makefile b/Makefile index 8758a4d..5106c8e 100644 --- a/Makefile +++ b/Makefile @@ -3,19 +3,16 @@ # =============================== # --- Переменные окружения (с дефолтами для локального запуска) --- -NFTSET_CONF ?= /tmp/90-nftset.conf -RESOLVE_CONF ?= /tmp/92-resolve-bbrkn.conf -CHROME_SERVER ?= http://10.77.1.2:3000 -DOMAINS_FILE ?= domains.txt -# Резолвер, которому pihole делегирует bbrkn-домены (host dnsmasq :5350). -# ВНИМАНИЕ: '#' в Makefile — начало комментария, экранируем как '\#'. -DNS_SERVER ?= 127.0.0.1\#5350 -NFTSET_TARGET_DIR ?= /etc/dnsmasq.d -CACHE_DIR ?= .cache/api -CACHE_TTL_DAYS ?= 15 +IPSET_CONF ?= /tmp/91-ipset-bbrkn.conf +RESOLVE_CONF ?= /tmp/92-resolve-bbrkn.conf +CHROME_SERVER ?= http://10.100.1.2:3000 +DOMAINS_FILE ?= domains.txt +DNS_SERVER ?= 8.8.8.8 +CACHE_DIR ?= .cache/api +CACHE_TTL_DAYS ?= 15 # Экспортируем переменные, чтобы они были доступны внутри shell-скриптов -export NFTSET_CONF RESOLVE_CONF CHROME_SERVER DOMAINS_FILE DNS_SERVER NFTSET_TARGET_DIR CACHE_DIR CACHE_TTL_DAYS +export IPSET_CONF RESOLVE_CONF CHROME_SERVER DOMAINS_FILE DNS_SERVER CACHE_DIR CACHE_TTL_DAYS # --- Основные цели --- .PHONY: all clean cache-clean check generate deploy @@ -25,7 +22,7 @@ all: generate deploy clean: @echo "🧹 Очистка временных файлов" - @rm -f $(NFTSET_CONF) $(RESOLVE_CONF) + @rm -f $(IPSET_CONF) $(RESOLVE_CONF) cache-clean: @echo "🗑 Очистка кэша API ($(CACHE_DIR))" diff --git a/README.md b/README.md index d63fb2b..2b5d65e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bbrkn — DNS Bypass Config Generator -Автоматизирует генерацию конфигураций **Pi-hole / dnsmasq** для маршрутизации трафика заблокированных доменов через VPN-туннель (WireGuard) с помощью **nftables-сетов**. +Автоматизирует генерацию конфигураций **Pi-hole / dnsmasq** для маршрутизации трафика заблокированных доменов через VPN-туннель (WireGuard). ## Как это работает @@ -8,29 +8,25 @@ flowchart TD A[domains.txt] --> B[generate-configs.sh] B -->|кэш .cache/api| B - B -->|MISS / EXPIRED| C[gekata: Chromium headless API] + B -->|MISS / EXPIRED| C[Chromium headless API] C --> B - B --> D[90-nftset.conf] + B --> D[91-ipset-bbrkn.conf] B --> E[92-resolve-bbrkn.conf] D --> F[deploy-to-gateway.sh] E --> F - F -->|backup + copy + systemctl restart| G[host dnsmasq :5350
/etc/dnsmasq.d/] - F -->|backup + copy + docker restart| H[Pi-hole :53] - F -->|health check + nftset check| H - F -.->|rollback если упал| G + F -->|backup + copy| G[Pi-hole dnsmasq.d/] + F -->|docker restart| H[Pi-hole container] + F -->|DNS health check| H + H -->|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
10.77.1.2 / 10.77.2.2] + H -->|домен в ipset → mark| I[iptables] + I --> J[WireGuard VPN] 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` на шлюзе. - --- ## Классификация доменов @@ -50,26 +46,15 @@ flowchart TD Домены группируются по базовому домену с комментарием: -`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 +ipset=/example.com/bbrkn +ipset=/static.example.com/bbrkn +ipset=/api.example.com/bbrkn ... # 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 -... +ipset=/some-service.com/bbrkn ``` --- @@ -108,11 +93,10 @@ make check # проверить синтаксис domains.txt | Переменная | По умолчанию | Описание | |---|---|---| | `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) | +| `IPSET_CONF` | `/tmp/91-ipset-bbrkn.conf` | Выходной файл правил ipset | +| `RESOLVE_CONF` | `/tmp/92-resolve-bbrkn.conf` | Выходной файл правил server= | +| `CHROME_SERVER` | `http://127.0.0.1:3000` | Адрес Chromium headless API | +| `DNS_SERVER` | `8.8.8.8` | DNS-сервер для резолвинга через VPN | | `IGNORE_PARTS` | _(пусто)_ | Подстроки через пробел — домены с совпадением игнорируются | | `CACHE_DIR` | `.cache/api` | Директория кэша API-ответов | | `CACHE_TTL_DAYS` | `15` | Время жизни кэша в днях; `0` — отключить кэш | @@ -124,17 +108,12 @@ make check # проверить синтаксис domains.txt | Переменная | По умолчанию | Описание | |---|---|---| -| `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`) | +| `TARGET_DIR` | `/opt/appdata/pihole/etc/dnsmasq.d` | Директория dnsmasq в Pi-hole | +| `IPSET_CONF` | `/tmp/91-ipset-bbrkn.conf` | Сгенерированный файл ipset | +| `RESOLVE_CONF` | `/tmp/92-resolve-bbrkn.conf` | Сгенерированный файл server= | | `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-инстанса для проверки захвата | +| `DNS_LISTEN_ADDR` | `127.0.0.1` | Адрес, на котором Pi-hole слушает DNS | +| `DNS_CHECK_DOMAIN` | `google.com` | Домен для DNS health check после рестарта | --- @@ -146,7 +125,7 @@ make check # проверить синтаксис domains.txt - Если файл **старше** TTL → перезапрашивается и кэш обновляется. - Если кэш **повреждён** (невалидный JSON) → перезапрашивается автоматически. -> **Внимание:** forgejo-runner на шлюзе **эфемерный** — workspace (и `.cache/api`) между запусками не сохраняется. Поэтому первый CI-прогон после сброса кэша выполняет **холодный обход всех доменов через gekata** (~30-70 с на домен, последовательно) и работает долго. Для быстрых повторных прогонов держите тёплый `.cache/api` в постоянной директории (`CACHE_DIR`). +На self-hosted runner workspace сохраняется между запусками, поэтому повторные CI-запуски значительно быстрее. ```bash # Сбросить кэш и запросить всё заново @@ -160,16 +139,15 @@ CACHE_TTL_DAYS=0 make generate ## Деплой и автооткат -`deploy-to-gateway.sh` деплоит в **два инстанса dnsmasq** и выполняет шаги: +`deploy-to-gateway.sh` выполняет следующие шаги: -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 — **автоматически откатывается**: восстанавливает оба бэкапа и перезапускает оба инстанса со старой конфигурацией. +1. Проверяет наличие сгенерированных конфигов. +2. Создаёт timestamped-бэкапы текущих конфигов в `TARGET_DIR`. +3. Копирует новые конфиги в `TARGET_DIR`. +4. Перезапускает контейнер Pi-hole. +5. Ждёт запуска (до 30 секунд, polling через `docker inspect`). +6. Выполняет DNS health check через `dig` / `nslookup`. +7. При ошибке на шагах 5 или 6 — **автоматически откатывается**: восстанавливает бэкапы и перезапускает контейнер со старой конфигурацией. --- @@ -248,11 +226,10 @@ some-domain.com → api_failed_added_without_subdomains | Инструмент | Где используется | |---|---| | `bash` 4.2+ | оба скрипта | -| `curl` | запросы к gekata API | +| `curl` | запросы к Chromium API | | `jq` | парсинг JSON-ответов | | `docker` | перезапуск Pi-hole | -| `nft` | flush nft-сетов + проверка захвата после деплоя | -| `systemctl` | рестарт host-инстанса dnsmasq | +| `ipset` | сброс маршрутов после деплоя | | `dig` или `nslookup` | DNS health check (опционально) | --- diff --git a/scripts/deploy-to-gateway.sh b/scripts/deploy-to-gateway.sh index beadb0f..257df44 100644 --- a/scripts/deploy-to-gateway.sh +++ b/scripts/deploy-to-gateway.sh @@ -4,64 +4,46 @@ set -euo pipefail # ============================== # Конфигурация через переменные окружения # ============================== -# Сгенерированные конфиги (из generate-configs.sh) -NFTSET_CONF="${NFTSET_CONF:-/tmp/90-nftset.conf}" +TARGET_DIR="${TARGET_DIR:-/opt/appdata/pihole/etc/dnsmasq.d}" +IPSET_CONF="${IPSET_CONF:-/tmp/91-ipset-bbrkn.conf}" RESOLVE_CONF="${RESOLVE_CONF:-/tmp/92-resolve-bbrkn.conf}" -# Два таргета — два инстанса dnsmasq: -# NFTSET_CONF → host dnsmasq (:5350), грузит /etc/dnsmasq.d, наполняет nft-сеты -# RESOLVE_CONF → pihole (docker), делегирует bbrkn-домены инстансу :5350 -NFTSET_TARGET_DIR="${NFTSET_TARGET_DIR:-/etc/dnsmasq.d}" -RESOLVE_TARGET_DIR="${RESOLVE_TARGET_DIR:-${TARGET_DIR:-/opt/appdata/pihole/etc/dnsmasq.d}}" - DOCKER_CONTAINER="${DOCKER_CONTAINER:-pihole}" -# systemd-сервис host-инстанса dnsmasq (:5350) -HOST_DNSMASQ_SVC="${HOST_DNSMASQ_SVC:-dnsmasq}" -# nft-сеты для сброса после деплоя (иначе висят до timeout 24h). -# Формат: "family table set" — перечисляются через ';' в NFT_SETS. -NFT_SETS="${NFT_SETS:-inet filter bbrkn_v4;inet filter bbrkn_v6}" - -# --- Health check --- -DNS_LISTEN_ADDR="${DNS_LISTEN_ADDR:-127.0.0.1}" # pihole :53 (клиентский) +# Адрес, на котором слушает Pi-hole DNS (для health check) +DNS_LISTEN_ADDR="${DNS_LISTEN_ADDR:-127.0.0.1}" +# Домен для проверки DNS после рестарта DNS_CHECK_DOMAIN="${DNS_CHECK_DOMAIN:-google.com}" -# Домен, который ЕСТЬ в bbrkn-сете — для end-to-end проверки nftset-захвата -NFTSET_CHECK_DOMAIN="${NFTSET_CHECK_DOMAIN:-chatgpt.com}" -RESOLVER_ADDR="${RESOLVER_ADDR:-127.0.0.1}" # host dnsmasq (:5350) -RESOLVER_PORT="${RESOLVER_PORT:-5350}" -NFT_CHECK_SET="${NFT_CHECK_SET:-inet filter bbrkn_v4}" -# Имена/пути назначения (параметрическая подстановка, без basename) -nftset_name="${NFTSET_CONF##*/}" +# Имена назначения (без basename — используем параметрическую подстановку) +ipset_name="${IPSET_CONF##*/}" resolve_name="${RESOLVE_CONF##*/}" -nftset_dest="$NFTSET_TARGET_DIR/$nftset_name" -resolve_dest="$RESOLVE_TARGET_DIR/$resolve_name" +ipset_dest="$TARGET_DIR/$ipset_name" +resolve_dest="$TARGET_DIR/$resolve_name" -# Пути к бэкапам (для rollback) -NFTSET_BACKUP="" +# Пути к бэкапам (заполняются ниже, нужны для rollback) +IPSET_BACKUP="" RESOLVE_BACKUP="" -echo "Deploying nftset config → $nftset_dest (host dnsmasq :$RESOLVER_PORT)" -echo "Deploying resolve config → $resolve_dest ($DOCKER_CONTAINER)" +echo "Deploying configuration files to $TARGET_DIR" # ============================== # Проверка входных файлов # ============================== -if [ ! -f "$NFTSET_CONF" ] || [ ! -f "$RESOLVE_CONF" ]; then - echo "Error: one or both generated config files not found!" - echo " NFTSET_CONF=$NFTSET_CONF RESOLVE_CONF=$RESOLVE_CONF" +if [ ! -f "$IPSET_CONF" ] || [ ! -f "$RESOLVE_CONF" ]; then + echo "Error: one or both config files not found!" exit 1 fi # ============================== -# Функция отката: восстанавливает бэкапы и перезапускает оба инстанса +# Функция отката: восстанавливает бэкапы и перезапускает контейнер # ============================== rollback() { echo "[ROLLBACK] Restoring previous configuration..." local restored=false - if [ -n "$NFTSET_BACKUP" ] && [ -f "$NFTSET_BACKUP" ]; then - cp "$NFTSET_BACKUP" "$nftset_dest" - echo "[ROLLBACK] Restored $nftset_dest" + if [ -n "$IPSET_BACKUP" ] && [ -f "$IPSET_BACKUP" ]; then + cp "$IPSET_BACKUP" "$ipset_dest" + echo "[ROLLBACK] Restored $ipset_dest" restored=true fi if [ -n "$RESOLVE_BACKUP" ] && [ -f "$RESOLVE_BACKUP" ]; then @@ -70,8 +52,7 @@ rollback() { restored=true fi if $restored; then - echo "[ROLLBACK] Restarting host dnsmasq ($HOST_DNSMASQ_SVC) and $DOCKER_CONTAINER..." - systemctl restart "$HOST_DNSMASQ_SVC" 2>/dev/null || true + echo "[ROLLBACK] Restarting $DOCKER_CONTAINER with previous config..." docker restart "$DOCKER_CONTAINER" 2>/dev/null || true else echo "[ROLLBACK] No backups found, cannot restore" @@ -83,10 +64,10 @@ rollback() { # Бэкап существующих файлов # ============================== ts=$(date +%Y%m%d-%H%M%S) -if [ -f "$nftset_dest" ]; then - NFTSET_BACKUP="${nftset_dest}.backup.${ts}" - cp "$nftset_dest" "$NFTSET_BACKUP" - echo "Backed up $nftset_dest → $NFTSET_BACKUP" +if [ -f "$ipset_dest" ]; then + IPSET_BACKUP="${ipset_dest}.backup.${ts}" + cp "$ipset_dest" "$IPSET_BACKUP" + echo "Backed up $ipset_dest → $IPSET_BACKUP" fi if [ -f "$resolve_dest" ]; then RESOLVE_BACKUP="${resolve_dest}.backup.${ts}" @@ -97,38 +78,16 @@ fi # ============================== # Копирование новых файлов # ============================== -cp "$NFTSET_CONF" "$nftset_dest" -cp "$RESOLVE_CONF" "$resolve_dest" +cp "$IPSET_CONF" "$TARGET_DIR/" +cp "$RESOLVE_CONF" "$TARGET_DIR/" # ============================== -# Перезапуск обоих инстансов. -# ВАЖНО: dnsmasq по SIGHUP НЕ перечитывает директивы конфига (nftset=/server=), -# только hosts-файлы — новые домены подхватываются лишь полным рестартом. +# Перезапуск контейнера # ============================== -echo "Restarting host dnsmasq ($HOST_DNSMASQ_SVC)..." -systemctl restart "$HOST_DNSMASQ_SVC" - echo "Restarting $DOCKER_CONTAINER container..." docker restart "$DOCKER_CONTAINER" -# --- Ждём host dnsmasq (:5350) --- -echo "Waiting for host dnsmasq on :$RESOLVER_PORT (up to 15s)..." -host_ready=false -for i in $(seq 1 15); do - if ss -uln 2>/dev/null | grep -q ":$RESOLVER_PORT "; then - host_ready=true - break - fi - sleep 1 -done -if ! $host_ready; then - echo "Error: host dnsmasq not listening on :$RESOLVER_PORT after 15s" - rollback - exit 1 -fi -echo "host dnsmasq is listening on :$RESOLVER_PORT" - -# --- Ждём pihole --- +# Ждём запуска — polling через docker inspect вместо sleep + docker ps echo "Waiting for $DOCKER_CONTAINER to be ready (up to 30s)..." ready=false for i in $(seq 1 30); do @@ -139,79 +98,52 @@ for i in $(seq 1 30); do fi sleep 1 done + if ! $ready; then - echo "Error: $DOCKER_CONTAINER failed to start after 30s (status: ${status:-unknown})" + echo "Error: $DOCKER_CONTAINER failed to start after 30s (status: $status)" rollback exit 1 fi echo "$DOCKER_CONTAINER is running" # ============================== -# Сброс nft-сетов (чистим устаревшие записи удалённых доменов; timeout 24h иначе) +# DNS health check # ============================== -echo "Flushing nft sets..." -IFS=';' read -ra _sets <<< "$NFT_SETS" -for s in "${_sets[@]}"; do - [ -z "$s" ] && continue - # shellcheck disable=SC2086 - if nft flush set $s 2>/dev/null; then - echo " flushed set $s" - else - echo " WARN: could not flush set $s" - fi -done - -# ============================== -# Health check 1: базовый DNS через pihole :53 -# ============================== -dns_query() { # - local addr="$1" port="$2" dom="$3" +dns_health_check() { if command -v dig >/dev/null 2>&1; then - dig +short +time=3 +tries=2 -p "$port" "@$addr" "$dom" 2>/dev/null + dig +short +time=3 +tries=2 "@$DNS_LISTEN_ADDR" "$DNS_CHECK_DOMAIN" >/dev/null 2>&1 elif command -v nslookup >/dev/null 2>&1; then - nslookup -port="$port" "$dom" "$addr" 2>/dev/null | awk '/^Address: /{print $2}' + nslookup "$DNS_CHECK_DOMAIN" "$DNS_LISTEN_ADDR" >/dev/null 2>&1 + else + echo "Warning: neither 'dig' nor 'nslookup' found, skipping DNS health check" + return 0 fi } -if ! command -v dig >/dev/null 2>&1 && ! command -v nslookup >/dev/null 2>&1; then - echo "Warning: neither 'dig' nor 'nslookup' found, skipping DNS health checks" -else - echo "DNS health check: $DNS_CHECK_DOMAIN via $DNS_LISTEN_ADDR:53 (up to 15 attempts)..." - dns_ok=false - for i in $(seq 1 15); do - if [ -n "$(dns_query "$DNS_LISTEN_ADDR" 53 "$DNS_CHECK_DOMAIN")" ]; then - dns_ok=true - echo " passed (attempt $i)" - break - fi - echo " attempt $i failed, retry in 1s..." - sleep 1 - done - if ! $dns_ok; then - echo "Error: DNS health check failed — pihole not resolving on $DNS_LISTEN_ADDR:53" - rollback - exit 1 +echo "Running DNS health check ($DNS_CHECK_DOMAIN via $DNS_LISTEN_ADDR, up to 15 attempts)..." +dns_ok=false +for i in $(seq 1 15); do + if dns_health_check; then + dns_ok=true + echo "DNS health check passed (attempt $i)" + break fi - - # ============================== - # Health check 2: end-to-end nftset-захват. - # Резолвим bbrkn-домен через :5350 и убеждаемся, что nft-сет наполнился. - # ============================== - echo "nftset capture check: $NFTSET_CHECK_DOMAIN via $RESOLVER_ADDR:$RESOLVER_PORT..." - dns_query "$RESOLVER_ADDR" "$RESOLVER_PORT" "$NFTSET_CHECK_DOMAIN" >/dev/null + echo " attempt $i failed, retrying in 1s..." sleep 1 - # shellcheck disable=SC2086 - set_count=$(nft list set $NFT_CHECK_SET 2>/dev/null | grep -cE '([0-9]{1,3}\.){3}[0-9]{1,3}' || echo 0) - if [ "${set_count:-0}" -gt 0 ]; then - echo " passed — set '$NFT_CHECK_SET' populated ($set_count element line(s))" - else - echo "Error: nftset '$NFT_CHECK_SET' still empty after resolving $NFTSET_CHECK_DOMAIN" - echo " host dnsmasq likely not loading $nftset_dest" - rollback - exit 1 - fi +done + +if ! $dns_ok; then + echo "Error: DNS health check failed after 15 attempts — Pi-hole not responding on $DNS_LISTEN_ADDR" + rollback + exit 1 fi +# ============================== +# Очистка IPSET +# ============================== +echo "Flushing ipset bbrkn..." +ipset flush bbrkn + echo "Deployment completed successfully" -DOMAIN_COUNT=$(grep -c '^nftset=' "$nftset_dest" || echo "0") -echo "Applied nftset configuration for $DOMAIN_COUNT domain directives" +DOMAIN_COUNT=$(grep -c '^ipset=' "$ipset_dest" || echo "0") +echo "Applied configuration for $DOMAIN_COUNT domains" diff --git a/scripts/generate-configs.sh b/scripts/generate-configs.sh index fc0d0f5..d8aece1 100644 --- a/scripts/generate-configs.sh +++ b/scripts/generate-configs.sh @@ -6,17 +6,11 @@ set -euo pipefail # ============================== INPUT_FILE="${DOMAINS_FILE:-domains.txt}" -NFTSET_CONF="${NFTSET_CONF:-/tmp/90-nftset.conf}" +IPSET_CONF="${IPSET_CONF:-/tmp/91-ipset-bbrkn.conf}" RESOLVE_CONF="${RESOLVE_CONF:-/tmp/92-resolve-bbrkn.conf}" -# nftset-цель для dnsmasq: <4|6>###[,...] -# 4# → A-записи в bbrkn_v4; 6# → AAAA в bbrkn_v6 (таблица: inet filter) -NFTSET_SPEC="${NFTSET_SPEC:-4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6}" - API_URL="${CHROME_SERVER:-http://127.0.0.1:3000}/domains?domain=" -# Куда pihole делегирует резолвинг bbrkn-доменов: инстанс dnsmasq :5350, который -# резолвит через VPN и наполняет nft-сеты. Формат dnsmasq server=: ip[#port]. -DNS_SERVER="${DNS_SERVER:-127.0.0.1#5350}" +DNS_SERVER="${DNS_SERVER:-8.8.8.8}" # Игнорируемые части доменов (разделитель — пробел) IGNORE_PARTS="${IGNORE_PARTS:-}" @@ -49,7 +43,7 @@ if ! command -v jq >/dev/null 2>&1; then err "jq is required"; exit 2; fi if [ "$DEBUG" != "0" ]; then : > "$DEBUG_LOG"; dbg "Debugging enabled"; fi log "Starting generate-configs.sh" -dbg "ENV: INPUT_FILE=$INPUT_FILE NFTSET_CONF=$NFTSET_CONF NFTSET_SPEC=$NFTSET_SPEC RESOLVE_CONF=$RESOLVE_CONF API_URL=$API_URL DNS_SERVER=$DNS_SERVER IGNORE_PARTS='$IGNORE_PARTS' DRY_RUN=$DRY_RUN CACHE_DIR=$CACHE_DIR CACHE_TTL_DAYS=$CACHE_TTL_DAYS STRICT_MODE=$STRICT_MODE" +dbg "ENV: INPUT_FILE=$INPUT_FILE IPSET_CONF=$IPSET_CONF RESOLVE_CONF=$RESOLVE_CONF API_URL=$API_URL DNS_SERVER=$DNS_SERVER IGNORE_PARTS='$IGNORE_PARTS' DRY_RUN=$DRY_RUN CACHE_DIR=$CACHE_DIR CACHE_TTL_DAYS=$CACHE_TTL_DAYS STRICT_MODE=$STRICT_MODE" if [ ! -f "$INPUT_FILE" ]; then err "Input file not found: $INPUT_FILE" @@ -319,9 +313,9 @@ if ! $DRY_RUN; then { for d in "${ORDERED_DOMS[@]}"; do [[ -n "${OUTPUT_COMMENT[$d]:-}" ]] && printf '%s\n' "${OUTPUT_COMMENT[$d]}" - printf 'nftset=/%s/%s\n' "$d" "$NFTSET_SPEC" + printf 'ipset=/%s/bbrkn\n' "$d" done - } > "$NFTSET_CONF" + } > "$IPSET_CONF" { for d in "${ORDERED_DOMS[@]}"; do [[ -n "${OUTPUT_COMMENT[$d]:-}" ]] && printf '%s\n' "${OUTPUT_COMMENT[$d]}"