#!/bin/bash set -euo pipefail # ============================== # Конфигурация через переменные окружения # ============================== # Сгенерированные конфиги (из generate-configs.sh) NFTSET_CONF="${NFTSET_CONF:-/tmp/90-nftset.conf}" RESOLVE_CONF="${RESOLVE_CONF:-/tmp/92-resolve-bbrkn.conf}" # Два таргета — два инстанса dnsmasq: # NFTSET_CONF → host dnsmasq (:5350), грузит /etc/dnsmasq.d, наполняет nft-сеты # RESOLVE_CONF → pihole (docker), делегирует bbrkn-домены инстансу :5350 NFTSET_TARGET_DIR="${NFTSET_TARGET_DIR:-/etc/dnsmasq.d}" RESOLVE_TARGET_DIR="${RESOLVE_TARGET_DIR:-${TARGET_DIR:-/opt/appdata/pihole/etc/dnsmasq.d}}" DOCKER_CONTAINER="${DOCKER_CONTAINER:-pihole}" # systemd-сервис host-инстанса dnsmasq (:5350) HOST_DNSMASQ_SVC="${HOST_DNSMASQ_SVC:-dnsmasq}" # nft-сеты для сброса после деплоя (иначе висят до timeout 24h). # Формат: "family table set" — перечисляются через ';' в NFT_SETS. NFT_SETS="${NFT_SETS:-inet filter bbrkn_v4;inet filter bbrkn_v6}" # --- Health check --- DNS_LISTEN_ADDR="${DNS_LISTEN_ADDR:-127.0.0.1}" # pihole :53 (клиентский) DNS_CHECK_DOMAIN="${DNS_CHECK_DOMAIN:-google.com}" # Домен, который ЕСТЬ в bbrkn-сете — для end-to-end проверки nftset-захвата NFTSET_CHECK_DOMAIN="${NFTSET_CHECK_DOMAIN:-chatgpt.com}" RESOLVER_ADDR="${RESOLVER_ADDR:-127.0.0.1}" # host dnsmasq (:5350) RESOLVER_PORT="${RESOLVER_PORT:-5350}" NFT_CHECK_SET="${NFT_CHECK_SET:-inet filter bbrkn_v4}" # Имена/пути назначения (параметрическая подстановка, без basename) nftset_name="${NFTSET_CONF##*/}" resolve_name="${RESOLVE_CONF##*/}" nftset_dest="$NFTSET_TARGET_DIR/$nftset_name" resolve_dest="$RESOLVE_TARGET_DIR/$resolve_name" # Пути к бэкапам (для rollback) NFTSET_BACKUP="" RESOLVE_BACKUP="" echo "Deploying nftset config → $nftset_dest (host dnsmasq :$RESOLVER_PORT)" echo "Deploying resolve config → $resolve_dest ($DOCKER_CONTAINER)" # ============================== # Проверка входных файлов # ============================== if [ ! -f "$NFTSET_CONF" ] || [ ! -f "$RESOLVE_CONF" ]; then echo "Error: one or both generated config files not found!" echo " NFTSET_CONF=$NFTSET_CONF RESOLVE_CONF=$RESOLVE_CONF" exit 1 fi # ============================== # Функция отката: восстанавливает бэкапы и перезапускает оба инстанса # ============================== rollback() { echo "[ROLLBACK] Restoring previous configuration..." local restored=false if [ -n "$NFTSET_BACKUP" ] && [ -f "$NFTSET_BACKUP" ]; then cp "$NFTSET_BACKUP" "$nftset_dest" echo "[ROLLBACK] Restored $nftset_dest" restored=true fi if [ -n "$RESOLVE_BACKUP" ] && [ -f "$RESOLVE_BACKUP" ]; then cp "$RESOLVE_BACKUP" "$resolve_dest" echo "[ROLLBACK] Restored $resolve_dest" restored=true fi if $restored; then echo "[ROLLBACK] Restarting host dnsmasq ($HOST_DNSMASQ_SVC) and $DOCKER_CONTAINER..." systemctl restart "$HOST_DNSMASQ_SVC" 2>/dev/null || true docker restart "$DOCKER_CONTAINER" 2>/dev/null || true else echo "[ROLLBACK] No backups found, cannot restore" fi echo "[ROLLBACK] Done" } # ============================== # Бэкап существующих файлов # ============================== ts=$(date +%Y%m%d-%H%M%S) if [ -f "$nftset_dest" ]; then NFTSET_BACKUP="${nftset_dest}.backup.${ts}" cp "$nftset_dest" "$NFTSET_BACKUP" echo "Backed up $nftset_dest → $NFTSET_BACKUP" fi if [ -f "$resolve_dest" ]; then RESOLVE_BACKUP="${resolve_dest}.backup.${ts}" cp "$resolve_dest" "$RESOLVE_BACKUP" echo "Backed up $resolve_dest → $RESOLVE_BACKUP" fi # ============================== # Копирование новых файлов # ============================== cp "$NFTSET_CONF" "$nftset_dest" cp "$RESOLVE_CONF" "$resolve_dest" # ============================== # Перезапуск обоих инстансов. # ВАЖНО: dnsmasq по SIGHUP НЕ перечитывает директивы конфига (nftset=/server=), # только hosts-файлы — новые домены подхватываются лишь полным рестартом. # ============================== echo "Restarting host dnsmasq ($HOST_DNSMASQ_SVC)..." systemctl restart "$HOST_DNSMASQ_SVC" echo "Restarting $DOCKER_CONTAINER container..." docker restart "$DOCKER_CONTAINER" # --- Ждём host dnsmasq (:5350) --- echo "Waiting for host dnsmasq on :$RESOLVER_PORT (up to 15s)..." host_ready=false for i in $(seq 1 15); do if ss -uln 2>/dev/null | grep -q ":$RESOLVER_PORT "; then host_ready=true break fi sleep 1 done if ! $host_ready; then echo "Error: host dnsmasq not listening on :$RESOLVER_PORT after 15s" rollback exit 1 fi echo "host dnsmasq is listening on :$RESOLVER_PORT" # --- Ждём pihole --- echo "Waiting for $DOCKER_CONTAINER to be ready (up to 30s)..." ready=false for i in $(seq 1 30); do status=$(docker inspect "$DOCKER_CONTAINER" --format '{{.State.Status}}' 2>/dev/null || echo "missing") if [ "$status" = "running" ]; then ready=true break fi sleep 1 done if ! $ready; then echo "Error: $DOCKER_CONTAINER failed to start after 30s (status: ${status:-unknown})" rollback exit 1 fi echo "$DOCKER_CONTAINER is running" # ============================== # Сброс nft-сетов (чистим устаревшие записи удалённых доменов; timeout 24h иначе) # ============================== echo "Flushing nft sets..." IFS=';' read -ra _sets <<< "$NFT_SETS" for s in "${_sets[@]}"; do [ -z "$s" ] && continue # shellcheck disable=SC2086 if nft flush set $s 2>/dev/null; then echo " flushed set $s" else echo " WARN: could not flush set $s" fi done # ============================== # Health check 1: базовый DNS через pihole :53 # ============================== dns_query() { # local addr="$1" port="$2" dom="$3" if command -v dig >/dev/null 2>&1; then dig +short +time=3 +tries=2 -p "$port" "@$addr" "$dom" 2>/dev/null elif command -v nslookup >/dev/null 2>&1; then nslookup -port="$port" "$dom" "$addr" 2>/dev/null | awk '/^Address: /{print $2}' fi } if ! command -v dig >/dev/null 2>&1 && ! command -v nslookup >/dev/null 2>&1; then echo "Warning: neither 'dig' nor 'nslookup' found, skipping DNS health checks" else echo "DNS health check: $DNS_CHECK_DOMAIN via $DNS_LISTEN_ADDR:53 (up to 15 attempts)..." dns_ok=false for i in $(seq 1 15); do if [ -n "$(dns_query "$DNS_LISTEN_ADDR" 53 "$DNS_CHECK_DOMAIN")" ]; then dns_ok=true echo " passed (attempt $i)" break fi echo " attempt $i failed, retry in 1s..." sleep 1 done if ! $dns_ok; then echo "Error: DNS health check failed — pihole not resolving on $DNS_LISTEN_ADDR:53" rollback exit 1 fi # ============================== # Health check 2: end-to-end nftset-захват. # Резолвим bbrkn-домен через :5350 и убеждаемся, что nft-сет наполнился. # ============================== echo "nftset capture check: $NFTSET_CHECK_DOMAIN via $RESOLVER_ADDR:$RESOLVER_PORT..." dns_query "$RESOLVER_ADDR" "$RESOLVER_PORT" "$NFTSET_CHECK_DOMAIN" >/dev/null sleep 1 # shellcheck disable=SC2086 set_count=$(nft list set $NFT_CHECK_SET 2>/dev/null | grep -cE '([0-9]{1,3}\.){3}[0-9]{1,3}' || echo 0) if [ "${set_count:-0}" -gt 0 ]; then echo " passed — set '$NFT_CHECK_SET' populated ($set_count element line(s))" else echo "Error: nftset '$NFT_CHECK_SET' still empty after resolving $NFTSET_CHECK_DOMAIN" echo " host dnsmasq likely not loading $nftset_dest" rollback exit 1 fi fi echo "Deployment completed successfully" DOMAIN_COUNT=$(grep -c '^nftset=' "$nftset_dest" || echo "0") echo "Applied nftset configuration for $DOMAIN_COUNT domain directives"