diff --git a/CLAUDE.md b/CLAUDE.md index 9854d86..424ec83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Two bash scripts, driven by env vars (defaults in each script's header, overridd - **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). Then it runs `warm-nftset.sh` (non-fatal) to refill the just-flushed sets. Backups go to `$BACKUP_DIR` (`/var/backups/bbrkn`, newest `BACKUP_KEEP`=10) — never into a dnsmasq conf dir, which would load them as live config. **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. +**`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 @@ -46,7 +46,6 @@ Two bash scripts, driven by env vars (defaults in each script's header, overridd ## Gotchas -- **dnsmasq only adds IPs to an nft set on a fresh upstream reply**, never on a cache hit — hence `cache-size=0` on the `:5350` instance and the hourly warmer (`/etc/cron.d/bbrkn-warm` → `/usr/local/sbin/bbrkn-warm`, a manually installed copy of `scripts/warm-nftset.sh`). The `:5350` main process can also wedge (`nftset … cache initialization failed: Protocol error` on every add until restart); the warmer detects that via `journalctl _PID=` and restarts it. - `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. diff --git a/Makefile b/Makefile index 3d814f7..8758a4d 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ CACHE_TTL_DAYS ?= 15 export NFTSET_CONF RESOLVE_CONF CHROME_SERVER DOMAINS_FILE DNS_SERVER NFTSET_TARGET_DIR CACHE_DIR CACHE_TTL_DAYS # --- Основные цели --- -.PHONY: all clean cache-clean check generate deploy warm +.PHONY: all clean cache-clean check generate deploy all: generate deploy @echo "✅ Конфиги успешно сгенерированы и задеплоены" @@ -55,10 +55,3 @@ deploy: @echo "🚀 Деплой конфигов в систему" @chmod +x scripts/deploy-to-gateway.sh @./scripts/deploy-to-gateway.sh - -# Прогрев nft-сетов. Запускать на шлюзе — читает задеплоенный конфиг и -# ходит в host-инстанс dnsmasq на 127.0.0.1:5350. -warm: - @echo "🔥 Прогрев nft-сетов" - @chmod +x scripts/warm-nftset.sh - @./scripts/warm-nftset.sh diff --git a/README.md b/README.md index 57c09d7..d63fb2b 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,7 @@ bbrkn/ ├── Makefile # управление сборкой и деплоем ├── scripts/ │ ├── generate-configs.sh # генерация dnsmasq-конфигов -│ ├── deploy-to-gateway.sh # деплой в Pi-hole + rollback -│ └── warm-nftset.sh # прогрев nft-сетов по cron (на шлюзе) +│ └── deploy-to-gateway.sh # деплой в Pi-hole + rollback └── .forgejo/workflows/deploy.yaml # CI/CD на self-hosted runner ``` @@ -98,7 +97,6 @@ make deploy # только задеплоить в Pi-hole make clean # удалить сгенерированные конфиги из /tmp make cache-clean # очистить кэш API-ответов (.cache/api/) make check # проверить синтаксис domains.txt -make warm # прогреть nft-сеты (запускать на шлюзе) ``` --- @@ -132,8 +130,6 @@ make warm # прогреть nft-сеты (запускать на шл | `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 | -| `BACKUP_DIR` | `/var/backups/bbrkn` | Куда класть бэкапы конфигов перед деплоем (НЕ в `dnsmasq.d` — оттуда грузится всё) | -| `BACKUP_KEEP` | `10` | Сколько последних бэкапов хранить на каждый файл | | `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 | @@ -174,65 +170,6 @@ CACHE_TTL_DAYS=0 make generate 6. Сбрасывает nft-сеты `bbrkn_v4`/`bbrkn_v6` (устаревшие IP иначе висят до timeout 24 ч). 7. Health check: базовый DNS через pihole `:53` + end-to-end проверка nftset-захвата (резолв bbrkn-домена через `:5350` → сет наполнился). 8. При ошибке на шагах 5–7 — **автоматически откатывается**: восстанавливает оба бэкапа и перезапускает оба инстанса со старой конфигурацией. -9. После успешного деплоя запускает `warm-nftset.sh` — сеты только что очищены, а клиенты держат старые IP в кэше. Ошибка прогрева деплой не откатывает (только `WARN`). - ---- - -## Прогрев 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` должен быть выключен кэш** (`cache-size=0` в `/etc/dnsmasq.conf` на шлюзе). dnsmasq добавляет IP в сет **только при свежем ответе upstream** — ответ из собственного кэша сет не обновляет. Кэширует pihole перед ним. - -**Самовосстановление.** dnsmasq иногда «клинит» на записи в сеты: основной процесс на каждую попытку пишет в журнал `nftset inet filter bbrkn_v4 netlink: Error: cache initialization failed: Protocol error` и больше ничего в сеты не добавляет до рестарта (наблюдалось под нагрузкой; начинается с дочерних TCP-процессов). Скрипт после прогона считает такие ошибки у основного процесса (`journalctl _PID=…`); если они есть — `systemctl restart $HOST_DNSMASQ_SVC` и повторный прогрев. Если не помогло — `exit 1` и `FAIL` в логе. - -> **Запускать только на шлюзе.** Инстанс `:5350` слушает `listen-address=127.0.0.1` и с других хостов LAN недоступен. Список доменов берётся из задеплоенного конфига, так что скрипт всегда работает с актуальным после последнего деплоя списком. - -```bash -# разово, вручную -make warm -./scripts/warm-nftset.sh - -# ежечасно по cron — так стоит на Archie: -install -m 0755 scripts/warm-nftset.sh /usr/local/sbin/bbrkn-warm -cat /etc/cron.d/bbrkn-warm -17 * * * * root /usr/local/sbin/bbrkn-warm >/dev/null 2>&1 -``` - -Копия в `/usr/local/sbin` **не обновляется деплоем** — после правок скрипта переустановить вручную. Деплой (CI) запускает скрипт из репо, `STATE_DIR` у них общий, поэтому lock тоже общий. - -### Переменные окружения - -| Переменная | По умолчанию | Описание | -|---|---|---| -| `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` | `/var/lib/bbrkn` | Лог `warm.log` и lock-файл (общие для cron и деплоя) | -| `LOG_MAX_BYTES` | `4194304` | Порог ротации лога в `warm.log.1` | -| `LOCK_WAIT` | `0` | Секунд ждать идущий прогон (`0` — сразу выйти с `SKIP`; деплой ждёт `WARM_LOCK_WAIT=300`) | -| `HOST_DNSMASQ_SVC` | `dnsmasq` | systemd-сервис `:5350` — для детектора заклинивания и рестарта | -| `SELF_HEAL` | `1` | `0` — только сообщать о заклинивании, не перезапускать dnsmasq | - -Параллельные запуски блокируются через `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 nftset_errors=0 healed=0 v4=526->2203 v6=305->1440 -2026-08-05T20:48:44+03:00 dnsfail: azureedge.net cdn-telegram.org kinozal.tv ... -``` - -`v4=X->Y` — число элементов в сете до и после прогона (считается через `nft list set`; не под root — через `sudo -n`, без прав будет `0`, на сам прогрев это не влияет). `nftset_errors` — ошибки записи в сет у основного процесса dnsmasq, `healed=1` — был рестарт. Элементов больше, чем доменов — у CDN на один домен приходится несколько адресов. Домены в `dnsfail` не резолвятся вообще (мёртвые записи, NXDOMAIN) — кандидаты на удаление из `domains.txt`. - -> `WARM_PING=1` на шлюзе почти всегда даёт `noping`: пакеты, рождённые на самом шлюзе, идут через `OUTPUT` и не проходят цепочку `prerouting_mangle`, то есть уходят в WAN без метки. Это диагностический режим, не показатель проблемы. - -Полный прогон 1059 доменов при `PARALLEL=8` занимает ~1 мин 45 с. --- diff --git a/domains.txt b/domains.txt index bbd336a..62990c6 100644 --- a/domains.txt +++ b/domains.txt @@ -23,6 +23,7 @@ claude.ai dell.com download.freebsd.org duckduckgo.com +echo.msk.ru fast.com flibusta.is forum.xda-developers.com @@ -62,6 +63,15 @@ seasonvar.ru sketchup.com snap.licdn.com sonic-net.github.io +spoti.fi +spotifycharts.com +spotifycodes.com +spotify.com +spotify.design +spotify-everywhere.com +spotifyforbrands.com +spotifyjobs.com +spotify.link static.cdninstagram.com static.rutracker.cc store.akamai.steamstatic.com @@ -183,96 +193,4 @@ netlify.com netlifydns.com netlifyusercontent.com www.ubuntu-touch.io -ubports.com -4pda.to -www.patreon.com -registry-1.docker.io -docker.io -downloads.claude.ai -claude.com -30bf3790.obrut.show -amedia.online -anime1.best -animego.me -animevost.org -api.anilibria.app -api.anilibria.tv -api.apbugall.org -api.bhcesh.me -api.cdnlibs.org -api.filmix.tv -api.g1.plvideo.ru -api.lumex.space -api.luxembd.ws -api.moonanime.art -api.rgshows.me -api.rstprgapipt.com -api.srvkp.com -api.vokino.org -apivb.com -base.ashdi.vip -cdnmovies-stream.online -chaturbate.com -coldcdn.xyz -ee.bongacams.com -embed.su -eneyida.tv -filmixapp.cyou -getstv.com -go.zet-flix.online -hdrezka.me -iframe.video -in.tizam.info -iptv.online -kinobase.org -kinogo.luxury -kinogo.media -kinoukr.com -kinovibe.vip -kodikapi.com -m.hqporner.com -megaoblako.com -moviesapi.club -myfanserial.net -plapi.cdnvideohub.com -player.autoembed.cc -player.smashystream.com -player.videasy.net -portal.lumex.host -redheadsound.studio -vibix.org -videoseed.tv -vidfast.pro -vidlink.pro -vidora.su -vidsrc.cc -gemini.google.com -nakarte.me -a-api.anthropic.com -youtube.com -youtu.be -youtube-nocookie.com -ytimg.com -ggpht.com -googlevideo.com -gvt1.com -gvt2.com -gvt3.com -googleusercontent.com -gstatic.com -jnn-pa.googleapis.com -play.googleapis.com -youtubei.googleapis.com -googleapis.com -printables.com -files.printables.com -api.bambulab.com -bambulab.com -e.bambulab.com -public-cdn.bblmw.com -makerworld.com -makerworld.bblmw.com -bblmw.com -blog.3mdeb.com -3mdeb.com -withpersona.com \ No newline at end of file +ubports.com \ No newline at end of file diff --git a/scripts/deploy-to-gateway.sh b/scripts/deploy-to-gateway.sh index ca82ac9..beadb0f 100644 --- a/scripts/deploy-to-gateway.sh +++ b/scripts/deploy-to-gateway.sh @@ -14,11 +14,6 @@ RESOLVE_CONF="${RESOLVE_CONF:-/tmp/92-resolve-bbrkn.conf}" NFTSET_TARGET_DIR="${NFTSET_TARGET_DIR:-/etc/dnsmasq.d}" RESOLVE_TARGET_DIR="${RESOLVE_TARGET_DIR:-${TARGET_DIR:-/opt/appdata/pihole/etc/dnsmasq.d}}" -# Бэкапы — ВНЕ conf-dir: dnsmasq (и pihole) грузит из dnsmasq.d все файлы, -# кроме исключённых расширений, так что *.backup.* там стал бы живым конфигом. -BACKUP_DIR="${BACKUP_DIR:-/var/backups/bbrkn}" -BACKUP_KEEP="${BACKUP_KEEP:-10}" # сколько последних копий хранить на каждый файл - DOCKER_CONTAINER="${DOCKER_CONTAINER:-pihole}" # systemd-сервис host-инстанса dnsmasq (:5350) HOST_DNSMASQ_SVC="${HOST_DNSMASQ_SVC:-dnsmasq}" @@ -87,31 +82,16 @@ rollback() { # ============================== # Бэкап существующих файлов # ============================== -# Оставляет BACKUP_KEEP самых свежих копий .backup. -# (ts вида YYYYmmdd-HHMMSS сортируется лексикографически). -prune_backups() { # - local name="$1" old - find "$BACKUP_DIR" -maxdepth 1 -type f -name "${name}.backup.*" -printf '%f\n' \ - | sort -r | tail -n +"$((BACKUP_KEEP + 1))" \ - | while read -r old; do - rm -f -- "$BACKUP_DIR/$old" - echo " pruned old backup $old" - done -} - -mkdir -p "$BACKUP_DIR" ts=$(date +%Y%m%d-%H%M%S) if [ -f "$nftset_dest" ]; then - NFTSET_BACKUP="$BACKUP_DIR/${nftset_name}.backup.${ts}" + NFTSET_BACKUP="${nftset_dest}.backup.${ts}" cp "$nftset_dest" "$NFTSET_BACKUP" echo "Backed up $nftset_dest → $NFTSET_BACKUP" - prune_backups "$nftset_name" fi if [ -f "$resolve_dest" ]; then - RESOLVE_BACKUP="$BACKUP_DIR/${resolve_name}.backup.${ts}" + RESOLVE_BACKUP="${resolve_dest}.backup.${ts}" cp "$resolve_dest" "$RESOLVE_BACKUP" echo "Backed up $resolve_dest → $RESOLVE_BACKUP" - prune_backups "$resolve_name" fi # ============================== @@ -232,23 +212,6 @@ else fi fi -# ============================== -# Прогрев nft-сетов: после flush сеты пусты, а у клиентов в кэше (Edge, -# Windows) остаются старые IP — без прогрева их трафик идёт мимо туннеля до -# первого свежего резолва. Ошибка прогрева деплой не откатывает: конфиги уже -# проверены, а cron-прогрев повторит попытку. -# ============================== -WARM_SCRIPT="${WARM_SCRIPT:-$(dirname "$0")/warm-nftset.sh}" -if [ -f "$WARM_SCRIPT" ]; then - echo "Warming nft sets ($WARM_SCRIPT)..." - if ! NFTSET_CONF="$nftset_dest" HOST_DNSMASQ_SVC="$HOST_DNSMASQ_SVC" \ - LOCK_WAIT="${WARM_LOCK_WAIT:-300}" bash "$WARM_SCRIPT"; then - echo " WARN: warm failed — sets will refill on client queries / hourly cron" - fi -else - echo " WARN: $WARM_SCRIPT not found, skipping warm" -fi - echo "Deployment completed successfully" DOMAIN_COUNT=$(grep -c '^nftset=' "$nftset_dest" || echo "0") echo "Applied nftset configuration for $DOMAIN_COUNT domain directives" diff --git a/scripts/warm-nftset.sh b/scripts/warm-nftset.sh deleted file mode 100755 index 8e26669..0000000 --- a/scripts/warm-nftset.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/bin/bash -set -uo pipefail - -# ============================== -# Прогрев nft-сетов (запускать НА ШЛЮЗЕ) -# ============================== -# Заново резолвит все домены, которые dnsmasq помечает директивами nftset=, -# обращаясь напрямую к host-инстансу 127.0.0.1#5350. Прямой запрос обходит -# кэш pihole FTL: запрос, попавший в кэш FTL, до :5350 не доходит и элементы -# в inet filter bbrkn_v4/bbrkn_v6 не обновляет — они молча истекают по timeout -# и домен уходит мимо туннеля. -# -# Источник списка доменов — сам задеплоенный конфиг, поэтому отдельный список -# синхронизировать не нужно: скрипт всегда работает с тем, что реально -# загружен в dnsmasq после последнего успешного деплоя. -# -# Самовосстановление: dnsmasq добавляет IP в сет только при свежем ответе -# upstream и иногда "клинит" — основной процесс начинает отвечать на каждую -# попытку записи "nftset ... netlink: Error: cache initialization failed: -# Protocol error" и больше сеты не наполняет до рестарта. Если за прогон в -# журнале основного процесса появилась такая ошибка — рестартуем dnsmasq и -# прогреваем ещё раз. - -# --- Конфигурация через переменные окружения --- -# Задеплоенный конфиг с директивами nftset= (источник списка доменов) -NFTSET_CONF="${NFTSET_CONF:-/etc/dnsmasq.d/90-nftset.conf}" -# Host-инстанс dnsmasq, который применяет nftset= -RESOLVER="${RESOLVER:-127.0.0.1}" -RESOLVER_PORT="${RESOLVER_PORT:-5350}" - -# Состояние и лог. Общий каталог для cron и деплоя — lock у них один. -STATE_DIR="${STATE_DIR:-/var/lib/bbrkn}" -LOG="${LOG:-${STATE_DIR}/warm.log}" -LOCK="${LOCK:-${STATE_DIR}/warm.lock}" -LOG_MAX_BYTES="${LOG_MAX_BYTES:-4194304}" # ротация лога, 4 МиБ -LOCK_WAIT="${LOCK_WAIT:-0}" # секунд ждать чужой прогон (0 — сразу выйти) - -# Параметры опроса -PARALLEL="${PARALLEL:-8}" # одновременных запросов к :5350 -DIG_TIMEOUT="${DIG_TIMEOUT:-3}" # секунд на попытку -DIG_TRIES="${DIG_TRIES:-2}" - -# Пинг выключен по умолчанию: пакеты, рождённые на самом шлюзе, идут через -# OUTPUT и не проходят цепочку prerouting_mangle — уходят в обычный WAN без -# метки и почти все отваливаются по таймауту. Включать только для отладки. -WARM_PING="${WARM_PING:-0}" -PING_TIMEOUT="${PING_TIMEOUT:-2}" - -NFT="${NFT:-/usr/sbin/nft}" - -# systemd-сервис host-инстанса dnsmasq (:5350) и самовосстановление -HOST_DNSMASQ_SVC="${HOST_DNSMASQ_SVC:-dnsmasq}" -SELF_HEAL="${SELF_HEAL:-1}" - -# Под root (cron.d, CI) sudo не нужен -SUDO="" -[ "$(id -u)" -eq 0 ] || SUDO="sudo -n" - -mkdir -p "$STATE_DIR" - -# Один инстанс: медленный прогон не должен наложиться на следующий по cron. -exec 9>"$LOCK" -if ! flock -w "$LOCK_WAIT" 9; then - echo "$(date -Is) SKIP предыдущий запуск ещё выполняется" >>"$LOG" - exit 0 -fi - -log() { echo "$(date -Is) $*" >>"$LOG"; } - -# Количество элементов в сете. У каждого элемента есть timeout, поэтому -# "expires" встречается ровно один раз на элемент. Ошибка здесь не фатальна — -# это только метрика прогресса. -set_count() { - $SUDO "$NFT" list set inet filter "$1" 2>/dev/null | grep -c 'expires' || true -} - -main_pid() { - systemctl show -p MainPID --value "$HOST_DNSMASQ_SVC" 2>/dev/null -} - -# Ошибки записи в nft-сет у основного процесса dnsmasq с момента . -# Ошибки дочерних (TCP) процессов не считаем — они разовые; заклинивание -# видно именно по основному. -nftset_errors() { # - $SUDO journalctl -q --no-pager _PID="$1" --since "$2" 2>/dev/null \ - | grep -c 'nftset.*Protocol error' || true -} - -if [ ! -r "$NFTSET_CONF" ]; then - log "FATAL не читается $NFTSET_CONF" - echo "Ошибка: не читается $NFTSET_CONF (скрипт запускается на шлюзе)" >&2 - exit 1 -fi - -mapfile -t HOSTS < <(sed -n 's|^nftset=/\([^/]*\)/.*|\1|p' "$NFTSET_CONF" | sort -u) -if [ "${#HOSTS[@]}" -eq 0 ]; then - log "FATAL в $NFTSET_CONF не найдено ни одной директивы nftset=" - echo "Ошибка: в $NFTSET_CONF не найдено ни одной директивы nftset=" >&2 - exit 1 -fi - -warm_one() { - local host="$1" a aaaa - a=$(dig +short +timeout="$DIG_TIMEOUT" +tries="$DIG_TRIES" \ - @"$RESOLVER" -p "$RESOLVER_PORT" A "$host" 2>/dev/null \ - | grep -E '^[0-9]+\.' | head -1) - aaaa=$(dig +short +timeout="$DIG_TIMEOUT" +tries="$DIG_TRIES" \ - @"$RESOLVER" -p "$RESOLVER_PORT" AAAA "$host" 2>/dev/null \ - | grep -E '^[0-9a-fA-F]*:' | head -1) - - if [ -z "$a" ] && [ -z "$aaaa" ]; then - echo "dnsfail $host" - return - fi - - # Пинг только подтверждает проходимость маршрута; в сет IP попал уже на - # шаге резолва выше. - if [ "$WARM_PING" = "1" ] && [ -n "$a" ]; then - if ping -n -q -c1 -W "$PING_TIMEOUT" "$a" >/dev/null 2>&1; then - echo "ok $host $a" - else - echo "noping $host $a" - fi - else - echo "ok $host ${a:-$aaaa}" - fi -} -export -f warm_one -export RESOLVER RESOLVER_PORT DIG_TIMEOUT DIG_TRIES WARM_PING PING_TIMEOUT - -# Один прогон по всем доменам. Заполняет RESULTS и ERRORS (ошибки записи в -# сет у основного процесса dnsmasq за время прогона). -warm_all() { - local since pid - since=$(date '+%Y-%m-%d %H:%M:%S') - pid=$(main_pid) - RESULTS="$(printf '%s\n' "${HOSTS[@]}" \ - | xargs -r -P "$PARALLEL" -I{} bash -c 'warm_one "$@"' _ {})" - sleep 1 # журнал пишется асинхронно - ERRORS=0 - if [ -n "$pid" ] && [ "$pid" != "0" ]; then - ERRORS=$(nftset_errors "$pid" "$since") - fi -} - -BEFORE_V4=$(set_count bbrkn_v4) -BEFORE_V6=$(set_count bbrkn_v6) -log "START domains=${#HOSTS[@]} conf=$NFTSET_CONF v4=$BEFORE_V4 v6=$BEFORE_V6" - -warm_all -HEALED=0 -if [ "$ERRORS" -gt 0 ] && [ "$SELF_HEAL" = "1" ]; then - log "WEDGE $HOST_DNSMASQ_SVC pid=$(main_pid) nftset_errors=$ERRORS — рестарт и повторный прогрев" - if $SUDO systemctl restart "$HOST_DNSMASQ_SVC"; then - sleep 2 - HEALED=1 - warm_all - else - log "FATAL не удалось перезапустить $HOST_DNSMASQ_SVC" - fi -fi - -OK=$(grep -c '^ok ' <<<"$RESULTS" || true) -NOPING=$(grep -c '^noping ' <<<"$RESULTS" || true) -DNSFAIL=$(grep -c '^dnsfail ' <<<"$RESULTS" || true) - -AFTER_V4=$(set_count bbrkn_v4) -AFTER_V6=$(set_count bbrkn_v6) -log "DONE ok=$OK noping=$NOPING dnsfail=$DNSFAIL nftset_errors=$ERRORS healed=$HEALED v4=${BEFORE_V4}->${AFTER_V4} v6=${BEFORE_V6}->${AFTER_V6}" -if [ "$DNSFAIL" -gt 0 ]; then - log "dnsfail: $(grep '^dnsfail ' <<<"$RESULTS" | cut -d' ' -f2 | tr '\n' ' ')" -fi - -echo "✔ Прогрето доменов: $OK, не резолвится: $DNSFAIL; bbrkn_v4 ${BEFORE_V4}->${AFTER_V4}, bbrkn_v6 ${BEFORE_V6}->${AFTER_V6}" -[ "$HEALED" = "1" ] && echo "⚠ dnsmasq заклинило на записи в nft-сет — перезапущен ($HOST_DNSMASQ_SVC)" - -# Ротация лога -if [ -f "$LOG" ] && [ "$(stat -c%s "$LOG")" -gt "$LOG_MAX_BYTES" ]; then - mv -f "$LOG" "${LOG}.1" -fi - -# Не ноль, если сеты так и не наполняются — деплой покажет предупреждение. -if [ "$ERRORS" -gt 0 ]; then - log "FAIL nftset_errors=$ERRORS после прогрева" - echo "Ошибка: dnsmasq не пишет в nft-сеты (nftset_errors=$ERRORS), см. journalctl -u $HOST_DNSMASQ_SVC" >&2 - exit 1 -fi -exit 0