claude-home/server-configs/networking/scripts/npm-pihole-sync.sh
Cal Corum 6c8d199359 Add Pi-hole HA documentation and networking updates
Add dual Pi-hole high availability setup guide, deployment notes, and
disk optimization docs. Update NPM + Pi-hole sync script and docs.
Add UniFi DNS firewall troubleshooting and networking scripts CONTEXT.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:19:56 -06:00

164 lines
5.2 KiB
Bash
Executable File

#!/bin/bash
# NPM to Pi-hole DNS Sync (Pi-hole v6 compatible)
# Syncs Nginx Proxy Manager proxy hosts to Pi-hole local DNS
# All domains point to NPM's IP, not the forward destination
# Supports dual Pi-hole deployment for high availability
#
# Pi-hole v6 changes:
# - Updates /etc/pihole/pihole.toml (dns.hosts array)
# - Updates /etc/pihole/hosts/custom.list (traditional format)
set -e
DRY_RUN=false
if [[ "$1" == "--dry-run" ]]; then
DRY_RUN=true
fi
# NPM's IP address (where all domains should point)
NPM_IP="10.10.0.16"
# Pi-hole instances
PRIMARY_PIHOLE="10.10.0.16"
SECONDARY_PIHOLE="10.10.0.226"
echo "NPM → Pi-hole DNS Sync (v6 compatible)"
echo "============================================================"
# Query NPM database for all enabled proxy hosts
DOMAINS=$(docker exec nginx-proxy-manager_app_1 python3 -c '
import sqlite3
import json
conn = sqlite3.connect("/data/database.sqlite")
cursor = conn.cursor()
cursor.execute("SELECT domain_names FROM proxy_host WHERE enabled = 1")
domains = []
for (domain_names,) in cursor.fetchall():
for domain in json.loads(domain_names or "[]"):
domains.append(domain)
for domain in sorted(domains):
print(domain)
conn.close()
')
# Count records
RECORD_COUNT=$(echo "$DOMAINS" | wc -l)
echo "Found $RECORD_COUNT enabled proxy hosts"
echo ""
echo "All domains will point to NPM at: $NPM_IP"
echo ""
echo "Domains to sync:"
echo "$DOMAINS" | awk -v ip="$NPM_IP" '{printf " %-15s %s\n", ip, $0}'
if [ "$DRY_RUN" = true ]; then
echo ""
echo "[DRY RUN] Not applying changes"
exit 0
fi
# Build new custom.list (traditional format)
NEW_DNS="# Pi-hole Local DNS Records
# Auto-synced from Nginx Proxy Manager
# All domains point to NPM at $NPM_IP
"
while IFS= read -r domain; do
NEW_DNS+="$NPM_IP $domain"$'\n'
done <<< "$DOMAINS"
# Build TOML hosts array
TOML_HOSTS=""
while IFS= read -r domain; do
TOML_HOSTS+=" \"$NPM_IP $domain\","$'\n'
done <<< "$DOMAINS"
# Remove trailing comma from last entry
TOML_HOSTS=$(echo "$TOML_HOSTS" | sed '$ s/,$//')
echo ""
echo "Syncing to Pi-hole instances..."
echo "============================================================"
# Function to update Pi-hole v6 configuration
update_pihole_v6() {
local pihole_container="$1"
local ssh_prefix="$2" # Empty for local, "ssh user@host" for remote
# Write to /etc/pihole/hosts/custom.list
if [ -z "$ssh_prefix" ]; then
echo "$NEW_DNS" | docker exec -i "$pihole_container" tee /etc/pihole/hosts/custom.list > /dev/null 2>&1
else
$ssh_prefix "echo '${NEW_DNS}' | docker exec -i $pihole_container tee /etc/pihole/hosts/custom.list > /dev/null" 2>&1
fi
# Update pihole.toml using perl for reliable multi-line regex replacement
# Escape special characters for perl
local toml_hosts_perl=$(echo "$TOML_HOSTS" | sed 's/\\/\\\\/g; s/"/\\"/g')
local perl_cmd="perl -i.bak -0pe 's/hosts\\s*=\\s*\\[[^\\]]*\\]/hosts = [\\n${toml_hosts_perl}\\n ]/s' /etc/pihole/pihole.toml"
if [ -z "$ssh_prefix" ]; then
docker exec "$pihole_container" sh -c "$perl_cmd"
else
$ssh_prefix "docker exec $pihole_container sh -c \"$perl_cmd\""
fi
}
# Sync to primary Pi-hole (local)
echo "Primary Pi-hole ($PRIMARY_PIHOLE):"
if update_pihole_v6 "pihole" ""; then
if docker exec pihole pihole reloaddns > /dev/null 2>&1; then
echo " ✓ Updated $RECORD_COUNT DNS records"
echo " ✓ Updated /etc/pihole/hosts/custom.list"
echo " ✓ Updated /etc/pihole/pihole.toml"
echo " ✓ Reloaded DNS"
else
echo " ✗ Failed to reload DNS"
PRIMARY_SYNC_FAILED=1
fi
else
echo " ✗ Failed to update Pi-hole configuration"
PRIMARY_SYNC_FAILED=1
fi
# Sync to secondary Pi-hole (remote via SSH using IP)
echo "Secondary Pi-hole ($SECONDARY_PIHOLE):"
if update_pihole_v6 "pihole" "ssh -o StrictHostKeyChecking=no cal@$SECONDARY_PIHOLE"; then
if ssh -o StrictHostKeyChecking=no "cal@$SECONDARY_PIHOLE" "docker exec pihole pihole reloaddns > /dev/null" 2>&1; then
echo " ✓ Updated $RECORD_COUNT DNS records"
echo " ✓ Updated /etc/pihole/hosts/custom.list"
echo " ✓ Updated /etc/pihole/pihole.toml"
echo " ✓ Reloaded DNS"
else
echo " ✗ Failed to reload DNS"
SECONDARY_SYNC_FAILED=1
fi
else
echo " ✗ Failed to update Pi-hole configuration or SSH connection issue"
SECONDARY_SYNC_FAILED=1
fi
echo ""
if [ -z "$PRIMARY_SYNC_FAILED" ] && [ -z "$SECONDARY_SYNC_FAILED" ]; then
echo "✓ Successfully synced to both Pi-hole instances"
echo "✓ All $RECORD_COUNT domains now point to NPM at $NPM_IP"
echo "✓ Updated both pihole.toml and custom.list files"
exit 0
elif [ -z "$PRIMARY_SYNC_FAILED" ] && [ -n "$SECONDARY_SYNC_FAILED" ]; then
echo "⚠ Primary sync successful, but secondary sync failed"
echo " Check SSH connectivity to $SECONDARY_PIHOLE and secondary Pi-hole health"
exit 1
elif [ -n "$PRIMARY_SYNC_FAILED" ] && [ -z "$SECONDARY_SYNC_FAILED" ]; then
echo "⚠ Secondary sync successful, but primary sync failed"
echo " Check primary Pi-hole health"
exit 1
else
echo "✗ Both Pi-hole syncs failed"
echo " Check Pi-hole containers and SSH connectivity"
exit 2
fi