All checks were successful
Deploy DNS Configuration / deploy (push) Successful in 1h15m4s
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DR7zpSsdX28T4Kn6v4jPSS
188 lines
8.6 KiB
Bash
Executable file
188 lines
8.6 KiB
Bash
Executable file
#!/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 с момента <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
|
||
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
|