warm: self-heal wedged dnsmasq, run after deploy, shared state dir
All checks were successful
Deploy DNS Configuration / deploy (push) Successful in 1h15m4s
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
This commit is contained in:
parent
a5ee057c59
commit
8598a1a48f
4 changed files with 107 additions and 18 deletions
|
|
@ -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 с момента <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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue