bbrkn/scripts/warm-nftset.sh
goodvin 660a31c35e
All checks were successful
Deploy DNS Configuration / deploy (push) Successful in 14m41s
Add warm-nftset.sh — periodic nftset warmer
Elements in bbrkn_v4/bbrkn_v6 carry a 1d timeout and are only refreshed
when a query reaches the host dnsmasq on :5350, where the nftset=
directives are applied. Idle domains — or ones answered from the pihole
FTL cache — age out of the sets and their traffic silently falls back to
the plain WAN route instead of the tunnel.

warm-nftset.sh re-resolves every domain from the deployed 90-nftset.conf
(base + related subdomains) directly against 127.0.0.1:5350, bypassing
the FTL cache. Sourcing the domain list from the deployed config means
there is no second list to keep in sync.

Ping is off by default: packets originating on the gateway itself go
through OUTPUT, never prerouting_mangle, so they leave unmarked and
almost all time out — a misleading signal, not a real failure.

Run on the gateway via `make warm` or hourly cron. Measured: 1059
domains in ~1m45s at PARALLEL=8, v4 526->2203, v6 305->1440 elements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:58:33 +03:00

128 lines
5.6 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 после последнего успешного деплоя.
# --- Конфигурация через переменные окружения ---
# Задеплоенный конфиг с директивами 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}"
# Состояние и лог
STATE_DIR="${STATE_DIR:-${HOME}/.local/state/bbrkn}"
LOG="${LOG:-${STATE_DIR}/warm.log}"
LOCK="${LOCK:-${STATE_DIR}/warm.lock}"
LOG_MAX_BYTES="${LOG_MAX_BYTES:-4194304}" # ротация лога, 4 МиБ
# Параметры опроса
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}"
mkdir -p "$STATE_DIR"
# Один инстанс: медленный прогон не должен наложиться на следующий по cron.
exec 9>"$LOCK"
if ! flock -n 9; then
echo "$(date -Is) SKIP предыдущий запуск ещё выполняется" >>"$LOG"
exit 0
fi
log() { echo "$(date -Is) $*" >>"$LOG"; }
# Количество элементов в сете. У каждого элемента есть timeout, поэтому
# "expires" встречается ровно один раз на элемент. Ошибка здесь не фатальна —
# это только метрика прогресса.
set_count() {
sudo -n "$NFT" list set inet filter "$1" 2>/dev/null | grep -c 'expires' || echo "?"
}
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
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" \
@"$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="$(printf '%s\n' "${HOSTS[@]}" \
| xargs -r -P "$PARALLEL" -I{} bash -c 'warm_one "$@"' _ {})"
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 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}"
# Ротация лога
if [ -f "$LOG" ] && [ "$(stat -c%s "$LOG")" -gt "$LOG_MAX_BYTES" ]; then
mv -f "$LOG" "${LOG}.1"
fi
exit 0