#!/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