From a5ee057c593937022d777538f8153a4c8bd6b16b Mon Sep 17 00:00:00 2001 From: goodvin Date: Tue, 13 Oct 2026 20:54:19 +0300 Subject: [PATCH 1/2] deploy: keep config backups out of dnsmasq conf dirs, cap at 10 Backups were written as .backup. next to the live file. Both dnsmasq instances load every file in their conf dir (the host excludes only .dpkg-*), so each backup was parsed as live config, resurrecting removed domains and piling up one stale copy per deploy. Write them to BACKUP_DIR (/var/backups/bbrkn) instead and keep the newest BACKUP_KEEP (10) per file. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01DR7zpSsdX28T4Kn6v4jPSS --- README.md | 2 ++ scripts/deploy-to-gateway.sh | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c08766..a5cb7db 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ 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 | diff --git a/scripts/deploy-to-gateway.sh b/scripts/deploy-to-gateway.sh index beadb0f..89763c7 100644 --- a/scripts/deploy-to-gateway.sh +++ b/scripts/deploy-to-gateway.sh @@ -14,6 +14,11 @@ 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}" @@ -82,16 +87,31 @@ 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="${nftset_dest}.backup.${ts}" + NFTSET_BACKUP="$BACKUP_DIR/${nftset_name}.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="${resolve_dest}.backup.${ts}" + RESOLVE_BACKUP="$BACKUP_DIR/${resolve_name}.backup.${ts}" cp "$resolve_dest" "$RESOLVE_BACKUP" echo "Backed up $resolve_dest → $RESOLVE_BACKUP" + prune_backups "$resolve_name" fi # ============================== From 8598a1a48f920005cf9a5d6583fb93d72ea70cd0 Mon Sep 17 00:00:00 2001 From: goodvin Date: Tue, 13 Oct 2026 21:26:49 +0300 Subject: [PATCH 2/2] warm: self-heal wedged dnsmasq, run after deploy, shared state dir dnsmasq adds IPs to an nft set only on a fresh upstream reply; a cache hit never refreshes the set. On Archie the :5350 instance also wedged: TCP child processes first, then the main process, logged 'nftset ... netlink: Error: cache initialization failed: Protocol error' on every add and stopped filling bbrkn_v4/v6 until restarted. warm-nftset.sh now counts those errors for the main PID during the run (journalctl _PID=) and, if any, restarts the service and warms again; exit 1 if it still fails. The state dir defaults to /var/lib/bbrkn so the root cron job and the deploy share one lock (LOCK_WAIT lets deploy wait). deploy-to-gateway.sh runs the warmer right after flushing the sets, since clients keep old IPs cached and would bypass the tunnel until the next fresh resolve. A warm failure only warns; it does not roll back. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01DR7zpSsdX28T4Kn6v4jPSS --- CLAUDE.md | 3 +- README.md | 23 +++++++--- scripts/deploy-to-gateway.sh | 17 ++++++++ scripts/warm-nftset.sh | 82 +++++++++++++++++++++++++++++++----- 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 424ec83..9854d86 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). **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). 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. ## Deployment / CI @@ -46,6 +46,7 @@ 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/README.md b/README.md index a5cb7db..57c09d7 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ 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`). --- @@ -183,6 +184,10 @@ CACHE_TTL_DAYS=0 make generate `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 @@ -190,11 +195,14 @@ CACHE_TTL_DAYS=0 make generate make warm ./scripts/warm-nftset.sh -# ежечасно по cron (пользователь на шлюзе, sudo без пароля для nft) -crontab -e -17 * * * * /path/to/bbrkn/scripts/warm-nftset.sh >/dev/null 2>&1 +# ежечасно по 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 тоже общий. + ### Переменные окружения | Переменная | По умолчанию | Описание | @@ -204,8 +212,11 @@ crontab -e | `PARALLEL` | `8` | Одновременных запросов к `:5350` | | `DIG_TIMEOUT` / `DIG_TRIES` | `3` / `2` | Таймаут и число попыток на домен | | `WARM_PING` | `0` | `1` — дополнительно пинговать первый A-адрес (только для отладки) | -| `STATE_DIR` | `~/.local/state/bbrkn` | Лог `warm.log` и lock-файл | +| `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. @@ -213,11 +224,11 @@ crontab -e ``` 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 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` — число элементов в сете до и после прогона (считается через `sudo -n nft list set`; без прав sudo вместо числа будет `?`, на сам прогрев это не влияет). Элементов больше, чем доменов — у CDN на один домен приходится несколько адресов. Домены в `dnsfail` не резолвятся вообще (мёртвые записи, NXDOMAIN) — кандидаты на удаление из `domains.txt`. +`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 без метки. Это диагностический режим, не показатель проблемы. diff --git a/scripts/deploy-to-gateway.sh b/scripts/deploy-to-gateway.sh index 89763c7..ca82ac9 100644 --- a/scripts/deploy-to-gateway.sh +++ b/scripts/deploy-to-gateway.sh @@ -232,6 +232,23 @@ 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 index 52295c6..8e26669 100755 --- a/scripts/warm-nftset.sh +++ b/scripts/warm-nftset.sh @@ -13,6 +13,13 @@ set -uo pipefail # Источник списка доменов — сам задеплоенный конфиг, поэтому отдельный список # синхронизировать не нужно: скрипт всегда работает с тем, что реально # загружен в dnsmasq после последнего успешного деплоя. +# +# Самовосстановление: dnsmasq добавляет IP в сет только при свежем ответе +# upstream и иногда "клинит" — основной процесс начинает отвечать на каждую +# попытку записи "nftset ... netlink: Error: cache initialization failed: +# Protocol error" и больше сеты не наполняет до рестарта. Если за прогон в +# журнале основного процесса появилась такая ошибка — рестартуем dnsmasq и +# прогреваем ещё раз. # --- Конфигурация через переменные окружения --- # Задеплоенный конфиг с директивами nftset= (источник списка доменов) @@ -21,11 +28,12 @@ NFTSET_CONF="${NFTSET_CONF:-/etc/dnsmasq.d/90-nftset.conf}" RESOLVER="${RESOLVER:-127.0.0.1}" RESOLVER_PORT="${RESOLVER_PORT:-5350}" -# Состояние и лог -STATE_DIR="${STATE_DIR:-${HOME}/.local/state/bbrkn}" +# Состояние и лог. Общий каталог для 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 @@ -40,11 +48,19 @@ 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 -n 9; then +if ! flock -w "$LOCK_WAIT" 9; then echo "$(date -Is) SKIP предыдущий запуск ещё выполняется" >>"$LOG" exit 0 fi @@ -55,7 +71,19 @@ log() { echo "$(date -Is) $*" >>"$LOG"; } # "expires" встречается ровно один раз на элемент. Ошибка здесь не фатальна — # это только метрика прогресса. set_count() { - sudo -n "$NFT" list set inet filter "$1" 2>/dev/null | grep -c 'expires' || echo "?" + $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 @@ -71,10 +99,6 @@ if [ "${#HOSTS[@]}" -eq 0 ]; then exit 1 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_one() { local host="$1" a aaaa a=$(dig +short +timeout="$DIG_TIMEOUT" +tries="$DIG_TRIES" \ @@ -104,8 +128,37 @@ warm_one() { export -f warm_one export RESOLVER RESOLVER_PORT DIG_TIMEOUT DIG_TRIES WARM_PING PING_TIMEOUT -RESULTS="$(printf '%s\n' "${HOSTS[@]}" \ - | xargs -r -P "$PARALLEL" -I{} bash -c 'warm_one "$@"' _ {})" +# Один прогон по всем доменам. Заполняет 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) @@ -113,16 +166,23 @@ 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 v4=${BEFORE_V4}->${AFTER_V4} v6=${BEFORE_V6}->${AFTER_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