#!/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