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>
This commit is contained in:
parent
ef30817cf8
commit
d81a4ab3b2
6 changed files with 271 additions and 111 deletions
|
|
@ -4,46 +4,64 @@ set -euo pipefail
|
|||
# ==============================
|
||||
# Конфигурация через переменные окружения
|
||||
# ==============================
|
||||
TARGET_DIR="${TARGET_DIR:-/opt/appdata/pihole/etc/dnsmasq.d}"
|
||||
IPSET_CONF="${IPSET_CONF:-/tmp/91-ipset-bbrkn.conf}"
|
||||
# Сгенерированные конфиги (из 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}"
|
||||
|
||||
# Адрес, на котором слушает Pi-hole DNS (для health check)
|
||||
DNS_LISTEN_ADDR="${DNS_LISTEN_ADDR:-127.0.0.1}"
|
||||
# Домен для проверки DNS после рестарта
|
||||
# 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 — используем параметрическую подстановку)
|
||||
ipset_name="${IPSET_CONF##*/}"
|
||||
# Имена/пути назначения (параметрическая подстановка, без basename)
|
||||
nftset_name="${NFTSET_CONF##*/}"
|
||||
resolve_name="${RESOLVE_CONF##*/}"
|
||||
ipset_dest="$TARGET_DIR/$ipset_name"
|
||||
resolve_dest="$TARGET_DIR/$resolve_name"
|
||||
nftset_dest="$NFTSET_TARGET_DIR/$nftset_name"
|
||||
resolve_dest="$RESOLVE_TARGET_DIR/$resolve_name"
|
||||
|
||||
# Пути к бэкапам (заполняются ниже, нужны для rollback)
|
||||
IPSET_BACKUP=""
|
||||
# Пути к бэкапам (для rollback)
|
||||
NFTSET_BACKUP=""
|
||||
RESOLVE_BACKUP=""
|
||||
|
||||
echo "Deploying configuration files to $TARGET_DIR"
|
||||
echo "Deploying nftset config → $nftset_dest (host dnsmasq :$RESOLVER_PORT)"
|
||||
echo "Deploying resolve config → $resolve_dest ($DOCKER_CONTAINER)"
|
||||
|
||||
# ==============================
|
||||
# Проверка входных файлов
|
||||
# ==============================
|
||||
if [ ! -f "$IPSET_CONF" ] || [ ! -f "$RESOLVE_CONF" ]; then
|
||||
echo "Error: one or both config files not found!"
|
||||
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 "$IPSET_BACKUP" ] && [ -f "$IPSET_BACKUP" ]; then
|
||||
cp "$IPSET_BACKUP" "$ipset_dest"
|
||||
echo "[ROLLBACK] Restored $ipset_dest"
|
||||
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
|
||||
|
|
@ -52,7 +70,8 @@ rollback() {
|
|||
restored=true
|
||||
fi
|
||||
if $restored; then
|
||||
echo "[ROLLBACK] Restarting $DOCKER_CONTAINER with previous config..."
|
||||
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"
|
||||
|
|
@ -64,10 +83,10 @@ rollback() {
|
|||
# Бэкап существующих файлов
|
||||
# ==============================
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
if [ -f "$ipset_dest" ]; then
|
||||
IPSET_BACKUP="${ipset_dest}.backup.${ts}"
|
||||
cp "$ipset_dest" "$IPSET_BACKUP"
|
||||
echo "Backed up $ipset_dest → $IPSET_BACKUP"
|
||||
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}"
|
||||
|
|
@ -78,16 +97,38 @@ fi
|
|||
# ==============================
|
||||
# Копирование новых файлов
|
||||
# ==============================
|
||||
cp "$IPSET_CONF" "$TARGET_DIR/"
|
||||
cp "$RESOLVE_CONF" "$TARGET_DIR/"
|
||||
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"
|
||||
|
||||
# Ждём запуска — polling через docker inspect вместо sleep + docker ps
|
||||
# --- Ждём 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
|
||||
|
|
@ -98,52 +139,79 @@ for i in $(seq 1 30); do
|
|||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! $ready; then
|
||||
echo "Error: $DOCKER_CONTAINER failed to start after 30s (status: $status)"
|
||||
echo "Error: $DOCKER_CONTAINER failed to start after 30s (status: ${status:-unknown})"
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
echo "$DOCKER_CONTAINER is running"
|
||||
|
||||
# ==============================
|
||||
# DNS health check
|
||||
# Сброс nft-сетов (чистим устаревшие записи удалённых доменов; timeout 24h иначе)
|
||||
# ==============================
|
||||
dns_health_check() {
|
||||
if command -v dig >/dev/null 2>&1; then
|
||||
dig +short +time=3 +tries=2 "@$DNS_LISTEN_ADDR" "$DNS_CHECK_DOMAIN" >/dev/null 2>&1
|
||||
elif command -v nslookup >/dev/null 2>&1; then
|
||||
nslookup "$DNS_CHECK_DOMAIN" "$DNS_LISTEN_ADDR" >/dev/null 2>&1
|
||||
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 "Warning: neither 'dig' nor 'nslookup' found, skipping DNS health check"
|
||||
return 0
|
||||
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
|
||||
}
|
||||
|
||||
echo "Running DNS health check ($DNS_CHECK_DOMAIN via $DNS_LISTEN_ADDR, up to 15 attempts)..."
|
||||
dns_ok=false
|
||||
for i in $(seq 1 15); do
|
||||
if dns_health_check; then
|
||||
dns_ok=true
|
||||
echo "DNS health check passed (attempt $i)"
|
||||
break
|
||||
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
|
||||
echo " attempt $i failed, retrying in 1s..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! $dns_ok; then
|
||||
echo "Error: DNS health check failed after 15 attempts — Pi-hole not responding on $DNS_LISTEN_ADDR"
|
||||
rollback
|
||||
exit 1
|
||||
# ==============================
|
||||
# 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
|
||||
|
||||
# ==============================
|
||||
# Очистка IPSET
|
||||
# ==============================
|
||||
echo "Flushing ipset bbrkn..."
|
||||
ipset flush bbrkn
|
||||
|
||||
echo "Deployment completed successfully"
|
||||
DOMAIN_COUNT=$(grep -c '^ipset=' "$ipset_dest" || echo "0")
|
||||
echo "Applied configuration for $DOMAIN_COUNT domains"
|
||||
DOMAIN_COUNT=$(grep -c '^nftset=' "$nftset_dest" || echo "0")
|
||||
echo "Applied nftset configuration for $DOMAIN_COUNT domain directives"
|
||||
|
|
|
|||
|
|
@ -6,11 +6,17 @@ set -euo pipefail
|
|||
# ==============================
|
||||
INPUT_FILE="${DOMAINS_FILE:-domains.txt}"
|
||||
|
||||
IPSET_CONF="${IPSET_CONF:-/tmp/91-ipset-bbrkn.conf}"
|
||||
NFTSET_CONF="${NFTSET_CONF:-/tmp/90-nftset.conf}"
|
||||
RESOLVE_CONF="${RESOLVE_CONF:-/tmp/92-resolve-bbrkn.conf}"
|
||||
|
||||
# nftset-цель для dnsmasq: <4|6>#<family>#<table>#<set>[,...]
|
||||
# 4# → A-записи в bbrkn_v4; 6# → AAAA в bbrkn_v6 (таблица: inet filter)
|
||||
NFTSET_SPEC="${NFTSET_SPEC:-4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6}"
|
||||
|
||||
API_URL="${CHROME_SERVER:-http://127.0.0.1:3000}/domains?domain="
|
||||
DNS_SERVER="${DNS_SERVER:-8.8.8.8}"
|
||||
# Куда pihole делегирует резолвинг bbrkn-доменов: инстанс dnsmasq :5350, который
|
||||
# резолвит через VPN и наполняет nft-сеты. Формат dnsmasq server=: ip[#port].
|
||||
DNS_SERVER="${DNS_SERVER:-127.0.0.1#5350}"
|
||||
|
||||
# Игнорируемые части доменов (разделитель — пробел)
|
||||
IGNORE_PARTS="${IGNORE_PARTS:-}"
|
||||
|
|
@ -43,7 +49,7 @@ if ! command -v jq >/dev/null 2>&1; then err "jq is required"; exit 2; fi
|
|||
if [ "$DEBUG" != "0" ]; then : > "$DEBUG_LOG"; dbg "Debugging enabled"; fi
|
||||
|
||||
log "Starting generate-configs.sh"
|
||||
dbg "ENV: INPUT_FILE=$INPUT_FILE IPSET_CONF=$IPSET_CONF RESOLVE_CONF=$RESOLVE_CONF API_URL=$API_URL DNS_SERVER=$DNS_SERVER IGNORE_PARTS='$IGNORE_PARTS' DRY_RUN=$DRY_RUN CACHE_DIR=$CACHE_DIR CACHE_TTL_DAYS=$CACHE_TTL_DAYS STRICT_MODE=$STRICT_MODE"
|
||||
dbg "ENV: INPUT_FILE=$INPUT_FILE NFTSET_CONF=$NFTSET_CONF NFTSET_SPEC=$NFTSET_SPEC RESOLVE_CONF=$RESOLVE_CONF API_URL=$API_URL DNS_SERVER=$DNS_SERVER IGNORE_PARTS='$IGNORE_PARTS' DRY_RUN=$DRY_RUN CACHE_DIR=$CACHE_DIR CACHE_TTL_DAYS=$CACHE_TTL_DAYS STRICT_MODE=$STRICT_MODE"
|
||||
|
||||
if [ ! -f "$INPUT_FILE" ]; then
|
||||
err "Input file not found: $INPUT_FILE"
|
||||
|
|
@ -313,9 +319,9 @@ if ! $DRY_RUN; then
|
|||
{
|
||||
for d in "${ORDERED_DOMS[@]}"; do
|
||||
[[ -n "${OUTPUT_COMMENT[$d]:-}" ]] && printf '%s\n' "${OUTPUT_COMMENT[$d]}"
|
||||
printf 'ipset=/%s/bbrkn\n' "$d"
|
||||
printf 'nftset=/%s/%s\n' "$d" "$NFTSET_SPEC"
|
||||
done
|
||||
} > "$IPSET_CONF"
|
||||
} > "$NFTSET_CONF"
|
||||
{
|
||||
for d in "${ORDERED_DOMS[@]}"; do
|
||||
[[ -n "${OUTPUT_COMMENT[$d]:-}" ]] && printf '%s\n' "${OUTPUT_COMMENT[$d]}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue