bbrkn/scripts/deploy-to-gateway.sh
goodvin d81a4ab3b2 Migrate bbrkn from legacy ipset to nftables sets
The gateway (Archie) routes bbrkn domains via nft sets bbrkn_v4/bbrkn_v6
(inet filter), not the legacy ipset. bbrkn was still emitting dead
`ipset=/domain/bbrkn` directives (no such ipset exists) plus a 92-resolve
pointing at 8.8.8.8, both overridden by hand-maintained files on the host.
This makes bbrkn the generator of record for the real scheme.

generate-configs.sh:
- emit `nftset=/domain/$NFTSET_SPEC` (default 4#inet#filter#bbrkn_v4,
  6#inet#filter#bbrkn_v6) into 90-nftset.conf instead of ipset= into
  91-ipset-bbrkn.conf
- DNS_SERVER default 8.8.8.8 -> 127.0.0.1#5350 (host dnsmasq pihole
  delegates bbrkn domains to for VPN resolution + nftset capture)

deploy-to-gateway.sh: two targets, two instances
- 90-nftset.conf -> host /etc/dnsmasq.d (:5350), 92-resolve -> pihole
- full restart of both (dnsmasq SIGHUP does NOT re-read nftset=/server=)
- flush nft sets bbrkn_v4/bbrkn_v6 instead of `ipset flush bbrkn`
- add end-to-end nftset-capture health check via :5350
- rollback restores both files and restarts both instances

Makefile/workflow: rename IPSET_CONF->NFTSET_CONF, add NFTSET_TARGET_DIR
and HOST_DNSMASQ_SVC, DNS_SERVER=127.0.0.1#5350 (escaped `\#` in Make,
quoted in YAML), note runner is ephemeral (cold gekata crawl).

Docs: README + new CLAUDE.md describe the two-dnsmasq / nft-set model;
exit-node DPI failover (10.77.1.2/10.77.2.2) documented as external
(wg-ha.service), not owned by bbrkn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:55:16 +03:00

217 lines
8.1 KiB
Bash
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 -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() { # <addr> <port> <domain>
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"