Compare commits
2 commits
1dff606865
...
8598a1a48f
| Author | SHA1 | Date | |
|---|---|---|---|
| 8598a1a48f | |||
| a5ee057c59 |
4 changed files with 131 additions and 20 deletions
|
|
@ -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.
|
- **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=/<domain>/$NFTSET_SPEC` (NFTSET_CONF → `90-nftset.conf`; default `NFTSET_SPEC=4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6`) and `server=/<domain>/$DNS_SERVER` (RESOLVE_CONF → `92-resolve-bbrkn.conf`; `DNS_SERVER` default `127.0.0.1#5350`, the host resolver pihole delegates to).
|
- Output lines: `nftset=/<domain>/$NFTSET_SPEC` (NFTSET_CONF → `90-nftset.conf`; default `NFTSET_SPEC=4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6`) and `server=/<domain>/$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
|
## Deployment / CI
|
||||||
|
|
||||||
|
|
@ -46,6 +46,7 @@ Two bash scripts, driven by env vars (defaults in each script's header, overridd
|
||||||
|
|
||||||
## Gotchas
|
## 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.
|
- `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.
|
- `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.
|
- **`#` 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.
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -132,6 +132,8 @@ make warm # прогреть nft-сеты (запускать на шл
|
||||||
| `TARGET_DIR` | `/opt/appdata/pihole/etc/dnsmasq.d` | Директория dnsmasq в Pi-hole (для `92-resolve`) |
|
| `TARGET_DIR` | `/opt/appdata/pihole/etc/dnsmasq.d` | Директория dnsmasq в Pi-hole (для `92-resolve`) |
|
||||||
| `DOCKER_CONTAINER` | `pihole` | Имя Docker-контейнера Pi-hole |
|
| `DOCKER_CONTAINER` | `pihole` | Имя Docker-контейнера Pi-hole |
|
||||||
| `HOST_DNSMASQ_SVC` | `dnsmasq` | systemd-сервис host-инстанса dnsmasq |
|
| `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 после деплоя (через `;`) |
|
| `NFT_SETS` | `inet filter bbrkn_v4;inet filter bbrkn_v6` | Сеты для flush после деплоя (через `;`) |
|
||||||
| `DNS_LISTEN_ADDR` | `127.0.0.1` | Адрес, на котором Pi-hole слушает DNS (:53) |
|
| `DNS_LISTEN_ADDR` | `127.0.0.1` | Адрес, на котором Pi-hole слушает DNS (:53) |
|
||||||
| `DNS_CHECK_DOMAIN` | `google.com` | Домен базового DNS health check |
|
| `DNS_CHECK_DOMAIN` | `google.com` | Домен базового DNS health check |
|
||||||
|
|
@ -172,6 +174,7 @@ CACHE_TTL_DAYS=0 make generate
|
||||||
6. Сбрасывает nft-сеты `bbrkn_v4`/`bbrkn_v6` (устаревшие IP иначе висят до timeout 24 ч).
|
6. Сбрасывает nft-сеты `bbrkn_v4`/`bbrkn_v6` (устаревшие IP иначе висят до timeout 24 ч).
|
||||||
7. Health check: базовый DNS через pihole `:53` + end-to-end проверка nftset-захвата (резолв bbrkn-домена через `:5350` → сет наполнился).
|
7. Health check: базовый DNS через pihole `:53` + end-to-end проверка nftset-захвата (резолв bbrkn-домена через `:5350` → сет наполнился).
|
||||||
8. При ошибке на шагах 5–7 — **автоматически откатывается**: восстанавливает оба бэкапа и перезапускает оба инстанса со старой конфигурацией.
|
8. При ошибке на шагах 5–7 — **автоматически откатывается**: восстанавливает оба бэкапа и перезапускает оба инстанса со старой конфигурацией.
|
||||||
|
9. После успешного деплоя запускает `warm-nftset.sh` — сеты только что очищены, а клиенты держат старые IP в кэше. Ошибка прогрева деплой не откатывает (только `WARN`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -181,6 +184,10 @@ CACHE_TTL_DAYS=0 make generate
|
||||||
|
|
||||||
`scripts/warm-nftset.sh` закрывает эту дыру: заново резолвит **все** домены из задеплоенного `90-nftset.conf` (база + связанные субдомены) **напрямую** через `dig @127.0.0.1 -p 5350`, минуя кэш FTL, — каждый запрос гарантированно доходит до тегирующего инстанса.
|
`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 недоступен. Список доменов берётся из задеплоенного конфига, так что скрипт всегда работает с актуальным после последнего деплоя списком.
|
> **Запускать только на шлюзе.** Инстанс `:5350` слушает `listen-address=127.0.0.1` и с других хостов LAN недоступен. Список доменов берётся из задеплоенного конфига, так что скрипт всегда работает с актуальным после последнего деплоя списком.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -188,11 +195,14 @@ CACHE_TTL_DAYS=0 make generate
|
||||||
make warm
|
make warm
|
||||||
./scripts/warm-nftset.sh
|
./scripts/warm-nftset.sh
|
||||||
|
|
||||||
# ежечасно по cron (пользователь на шлюзе, sudo без пароля для nft)
|
# ежечасно по cron — так стоит на Archie:
|
||||||
crontab -e
|
install -m 0755 scripts/warm-nftset.sh /usr/local/sbin/bbrkn-warm
|
||||||
17 * * * * /path/to/bbrkn/scripts/warm-nftset.sh >/dev/null 2>&1
|
cat /etc/cron.d/bbrkn-warm
|
||||||
|
17 * * * * root /usr/local/sbin/bbrkn-warm >/dev/null 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Копия в `/usr/local/sbin` **не обновляется деплоем** — после правок скрипта переустановить вручную. Деплой (CI) запускает скрипт из репо, `STATE_DIR` у них общий, поэтому lock тоже общий.
|
||||||
|
|
||||||
### Переменные окружения
|
### Переменные окружения
|
||||||
|
|
||||||
| Переменная | По умолчанию | Описание |
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|
@ -202,8 +212,11 @@ crontab -e
|
||||||
| `PARALLEL` | `8` | Одновременных запросов к `:5350` |
|
| `PARALLEL` | `8` | Одновременных запросов к `:5350` |
|
||||||
| `DIG_TIMEOUT` / `DIG_TRIES` | `3` / `2` | Таймаут и число попыток на домен |
|
| `DIG_TIMEOUT` / `DIG_TRIES` | `3` / `2` | Таймаут и число попыток на домен |
|
||||||
| `WARM_PING` | `0` | `1` — дополнительно пинговать первый A-адрес (только для отладки) |
|
| `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` |
|
| `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.
|
Параллельные запуски блокируются через `flock` — медленный прогон не наложится на следующий по cron.
|
||||||
|
|
||||||
|
|
@ -211,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: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 ...
|
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 без метки. Это диагностический режим, не показатель проблемы.
|
> `WARM_PING=1` на шлюзе почти всегда даёт `noping`: пакеты, рождённые на самом шлюзе, идут через `OUTPUT` и не проходят цепочку `prerouting_mangle`, то есть уходят в WAN без метки. Это диагностический режим, не показатель проблемы.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ RESOLVE_CONF="${RESOLVE_CONF:-/tmp/92-resolve-bbrkn.conf}"
|
||||||
NFTSET_TARGET_DIR="${NFTSET_TARGET_DIR:-/etc/dnsmasq.d}"
|
NFTSET_TARGET_DIR="${NFTSET_TARGET_DIR:-/etc/dnsmasq.d}"
|
||||||
RESOLVE_TARGET_DIR="${RESOLVE_TARGET_DIR:-${TARGET_DIR:-/opt/appdata/pihole/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}"
|
DOCKER_CONTAINER="${DOCKER_CONTAINER:-pihole}"
|
||||||
# systemd-сервис host-инстанса dnsmasq (:5350)
|
# systemd-сервис host-инстанса dnsmasq (:5350)
|
||||||
HOST_DNSMASQ_SVC="${HOST_DNSMASQ_SVC:-dnsmasq}"
|
HOST_DNSMASQ_SVC="${HOST_DNSMASQ_SVC:-dnsmasq}"
|
||||||
|
|
@ -82,16 +87,31 @@ rollback() {
|
||||||
# ==============================
|
# ==============================
|
||||||
# Бэкап существующих файлов
|
# Бэкап существующих файлов
|
||||||
# ==============================
|
# ==============================
|
||||||
|
# Оставляет BACKUP_KEEP самых свежих копий <name>.backup.<ts>
|
||||||
|
# (ts вида YYYYmmdd-HHMMSS сортируется лексикографически).
|
||||||
|
prune_backups() { # <name>
|
||||||
|
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)
|
ts=$(date +%Y%m%d-%H%M%S)
|
||||||
if [ -f "$nftset_dest" ]; then
|
if [ -f "$nftset_dest" ]; then
|
||||||
NFTSET_BACKUP="${nftset_dest}.backup.${ts}"
|
NFTSET_BACKUP="$BACKUP_DIR/${nftset_name}.backup.${ts}"
|
||||||
cp "$nftset_dest" "$NFTSET_BACKUP"
|
cp "$nftset_dest" "$NFTSET_BACKUP"
|
||||||
echo "Backed up $nftset_dest → $NFTSET_BACKUP"
|
echo "Backed up $nftset_dest → $NFTSET_BACKUP"
|
||||||
|
prune_backups "$nftset_name"
|
||||||
fi
|
fi
|
||||||
if [ -f "$resolve_dest" ]; then
|
if [ -f "$resolve_dest" ]; then
|
||||||
RESOLVE_BACKUP="${resolve_dest}.backup.${ts}"
|
RESOLVE_BACKUP="$BACKUP_DIR/${resolve_name}.backup.${ts}"
|
||||||
cp "$resolve_dest" "$RESOLVE_BACKUP"
|
cp "$resolve_dest" "$RESOLVE_BACKUP"
|
||||||
echo "Backed up $resolve_dest → $RESOLVE_BACKUP"
|
echo "Backed up $resolve_dest → $RESOLVE_BACKUP"
|
||||||
|
prune_backups "$resolve_name"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|
@ -212,6 +232,23 @@ else
|
||||||
fi
|
fi
|
||||||
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"
|
echo "Deployment completed successfully"
|
||||||
DOMAIN_COUNT=$(grep -c '^nftset=' "$nftset_dest" || echo "0")
|
DOMAIN_COUNT=$(grep -c '^nftset=' "$nftset_dest" || echo "0")
|
||||||
echo "Applied nftset configuration for $DOMAIN_COUNT domain directives"
|
echo "Applied nftset configuration for $DOMAIN_COUNT domain directives"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@ set -uo pipefail
|
||||||
# Источник списка доменов — сам задеплоенный конфиг, поэтому отдельный список
|
# Источник списка доменов — сам задеплоенный конфиг, поэтому отдельный список
|
||||||
# синхронизировать не нужно: скрипт всегда работает с тем, что реально
|
# синхронизировать не нужно: скрипт всегда работает с тем, что реально
|
||||||
# загружен в dnsmasq после последнего успешного деплоя.
|
# загружен в dnsmasq после последнего успешного деплоя.
|
||||||
|
#
|
||||||
|
# Самовосстановление: dnsmasq добавляет IP в сет только при свежем ответе
|
||||||
|
# upstream и иногда "клинит" — основной процесс начинает отвечать на каждую
|
||||||
|
# попытку записи "nftset ... netlink: Error: cache initialization failed:
|
||||||
|
# Protocol error" и больше сеты не наполняет до рестарта. Если за прогон в
|
||||||
|
# журнале основного процесса появилась такая ошибка — рестартуем dnsmasq и
|
||||||
|
# прогреваем ещё раз.
|
||||||
|
|
||||||
# --- Конфигурация через переменные окружения ---
|
# --- Конфигурация через переменные окружения ---
|
||||||
# Задеплоенный конфиг с директивами nftset= (источник списка доменов)
|
# Задеплоенный конфиг с директивами nftset= (источник списка доменов)
|
||||||
|
|
@ -21,11 +28,12 @@ NFTSET_CONF="${NFTSET_CONF:-/etc/dnsmasq.d/90-nftset.conf}"
|
||||||
RESOLVER="${RESOLVER:-127.0.0.1}"
|
RESOLVER="${RESOLVER:-127.0.0.1}"
|
||||||
RESOLVER_PORT="${RESOLVER_PORT:-5350}"
|
RESOLVER_PORT="${RESOLVER_PORT:-5350}"
|
||||||
|
|
||||||
# Состояние и лог
|
# Состояние и лог. Общий каталог для cron и деплоя — lock у них один.
|
||||||
STATE_DIR="${STATE_DIR:-${HOME}/.local/state/bbrkn}"
|
STATE_DIR="${STATE_DIR:-/var/lib/bbrkn}"
|
||||||
LOG="${LOG:-${STATE_DIR}/warm.log}"
|
LOG="${LOG:-${STATE_DIR}/warm.log}"
|
||||||
LOCK="${LOCK:-${STATE_DIR}/warm.lock}"
|
LOCK="${LOCK:-${STATE_DIR}/warm.lock}"
|
||||||
LOG_MAX_BYTES="${LOG_MAX_BYTES:-4194304}" # ротация лога, 4 МиБ
|
LOG_MAX_BYTES="${LOG_MAX_BYTES:-4194304}" # ротация лога, 4 МиБ
|
||||||
|
LOCK_WAIT="${LOCK_WAIT:-0}" # секунд ждать чужой прогон (0 — сразу выйти)
|
||||||
|
|
||||||
# Параметры опроса
|
# Параметры опроса
|
||||||
PARALLEL="${PARALLEL:-8}" # одновременных запросов к :5350
|
PARALLEL="${PARALLEL:-8}" # одновременных запросов к :5350
|
||||||
|
|
@ -40,11 +48,19 @@ PING_TIMEOUT="${PING_TIMEOUT:-2}"
|
||||||
|
|
||||||
NFT="${NFT:-/usr/sbin/nft}"
|
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"
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
# Один инстанс: медленный прогон не должен наложиться на следующий по cron.
|
# Один инстанс: медленный прогон не должен наложиться на следующий по cron.
|
||||||
exec 9>"$LOCK"
|
exec 9>"$LOCK"
|
||||||
if ! flock -n 9; then
|
if ! flock -w "$LOCK_WAIT" 9; then
|
||||||
echo "$(date -Is) SKIP предыдущий запуск ещё выполняется" >>"$LOG"
|
echo "$(date -Is) SKIP предыдущий запуск ещё выполняется" >>"$LOG"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -55,7 +71,19 @@ log() { echo "$(date -Is) $*" >>"$LOG"; }
|
||||||
# "expires" встречается ровно один раз на элемент. Ошибка здесь не фатальна —
|
# "expires" встречается ровно один раз на элемент. Ошибка здесь не фатальна —
|
||||||
# это только метрика прогресса.
|
# это только метрика прогресса.
|
||||||
set_count() {
|
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 с момента <since>.
|
||||||
|
# Ошибки дочерних (TCP) процессов не считаем — они разовые; заклинивание
|
||||||
|
# видно именно по основному.
|
||||||
|
nftset_errors() { # <pid> <since>
|
||||||
|
$SUDO journalctl -q --no-pager _PID="$1" --since "$2" 2>/dev/null \
|
||||||
|
| grep -c 'nftset.*Protocol error' || true
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ ! -r "$NFTSET_CONF" ]; then
|
if [ ! -r "$NFTSET_CONF" ]; then
|
||||||
|
|
@ -71,10 +99,6 @@ if [ "${#HOSTS[@]}" -eq 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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() {
|
warm_one() {
|
||||||
local host="$1" a aaaa
|
local host="$1" a aaaa
|
||||||
a=$(dig +short +timeout="$DIG_TIMEOUT" +tries="$DIG_TRIES" \
|
a=$(dig +short +timeout="$DIG_TIMEOUT" +tries="$DIG_TRIES" \
|
||||||
|
|
@ -104,8 +128,37 @@ warm_one() {
|
||||||
export -f warm_one
|
export -f warm_one
|
||||||
export RESOLVER RESOLVER_PORT DIG_TIMEOUT DIG_TRIES WARM_PING PING_TIMEOUT
|
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[@]}" \
|
RESULTS="$(printf '%s\n' "${HOSTS[@]}" \
|
||||||
| xargs -r -P "$PARALLEL" -I{} bash -c 'warm_one "$@"' _ {})"
|
| 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)
|
OK=$(grep -c '^ok ' <<<"$RESULTS" || true)
|
||||||
NOPING=$(grep -c '^noping ' <<<"$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_V4=$(set_count bbrkn_v4)
|
||||||
AFTER_V6=$(set_count bbrkn_v6)
|
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
|
if [ "$DNSFAIL" -gt 0 ]; then
|
||||||
log "dnsfail: $(grep '^dnsfail ' <<<"$RESULTS" | cut -d' ' -f2 | tr '\n' ' ')"
|
log "dnsfail: $(grep '^dnsfail ' <<<"$RESULTS" | cut -d' ' -f2 | tr '\n' ' ')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✔ Прогрето доменов: $OK, не резолвится: $DNSFAIL; bbrkn_v4 ${BEFORE_V4}->${AFTER_V4}, bbrkn_v6 ${BEFORE_V6}->${AFTER_V6}"
|
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
|
if [ -f "$LOG" ] && [ "$(stat -c%s "$LOG")" -gt "$LOG_MAX_BYTES" ]; then
|
||||||
mv -f "$LOG" "${LOG}.1"
|
mv -f "$LOG" "${LOG}.1"
|
||||||
fi
|
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
|
exit 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue