Dynamic update of DNS with DHCP lease info between dual redundant pihole servers

I have just completed deploying redundant pi-hole DNS/DHCP servers in our environment and they are working quite well.

So that both servers will be able to respond to DNS requests for all clients with DHCP leases, I have scripts on each that regenerates /etc/pihole/custom.list from the other servers /etc/pihole/dhcp.leases. But this is being accomplished using cron which is a bit clunky and incurs some latency.

I was wondering if there was a "hook" in pihole I could take advantage of when the dhcp leases change rather than poling with cron every 5 minutes.

Came up with a satisfactory solution using systemd and inotifywait to monitor the /etc/pihole/dhcp.leases file. When it is modified (but not zero length), I munge it into "hosts" format using awk/sort. I compare the resulting file to the last one generated - simple renewals, which are the bulk of changes to the dhcp.leases file don't change resulting custom.list file. But if it has, I copy it to the other servers /etc/pihole/custom.list and reload piehole. The second server, of course, has a reciprocating service.

Updates are pretty nearly instantaneous. As soon as a client has a lease, it can be resolved on either server. Code follows:

# /lib/systemd/system/SyncDHCP.service
[Unit]
Description=Updates DHCP DNS between two pihole servers

[Service]
ExecStart=/usr/bin/SyncDHCP.sh

[Install]
WantedBy=multi-user.target

#! /usr/bin/bash
# /usr/bin/SyncDHCP.sh
Target=dns1 # Points at the server recieving custom.list
Src=/etc/pihole/dhcp.leases
Dst=$(mktemp)
Tmp=$(mktemp)
Msg(){
printf "%s - %s\n" $(date +%u%m%d%H%M%S) "$"
}
inotifywait -m $Src -e modify |
while read -r f a
do
size=$(cksum $Src | awk '{print $2}')
if [ "$size" -ne 0 ]
then
awk '$4!="
"{printf "%s %s\n",$3,$4}' $Src | sort -k3 > $Tmp
if ! cmp -s $Dst $Tmp
then
diff $Dst $Tmp | awk '$1~/<|>/{
if ($1~/</)printf "released %s %s\n",$2,$3;
if ($1~/>/)printf "assigned %s %s\n",$2,$3;
}'
scp $Tmp root@$Target:/etc/pihole/custom.list > /dev/null &&
ssh root@$Target "pihole restartdns reload" &&
cp $Tmp $Dst &&
Msg "Updated custom.list on $Target"
fi
fi
done

2 Likes

Made some improvements to this service to allow stopping/restarting cleanly. Be sure to modify the "Target" section with your hostnames. And keep in mind, you need to install inotifywait and set up public key authentication for the root accounts between the two systems.

/lib/systemd/system/SyncDHCP.service

[Unit]
Description=Updates DHCP DNS between two pihole servers
After=network.target

[Service]
ExecStart=/usr/bin/SyncDHCP.sh Start
ExecStop=/usr/bin/SyncDHCP.sh Stop
ExecRestart=/usr/bin/SyncDHCP.sh Restart

[Install]
WantedBy=multi-user.target

/usr/bin/SyncDHCP.sh

#! /usr/bin/bash
Flag="/tmp/SyncDHCP.running"
Src="/etc/pihole/dhcp.leases"

function Msg {
   printf "%s - %s\n" $(date +%u%m%d%H%M%S) "$*" 
}

function Stop {
   Msg "Stopping SyncDHCP service..."
   rm -f $Flag
   proc=$(ps -aux | awk '/inotifywait.*dhcp\.leases/{print $2}')
   [ -n "$proc" ] && kill -HUP $proc
   sleep 3     # Wait for filesystem cleanup
}

function Start {
   Msg "Starting SyncDHCP service..."
   Dst=$(mktemp)
   Tmp=$(mktemp)

   # Modify these target names to conform to your deployment
   if [ $(hostname) == "dns1" ]
   then
      Target=dns2
   else
      Target=dns1
   fi

   # Create the "run flag file"
   touch $Flag
   while [ -e $Flag ]
   do
      inotifywait -e modify $Src | \
      while read -r f a
      do
      size=$(cksum $Src | awk '{print $2}')
      if [ "$size" -ne 0 ]
      then
            awk '$4!="*"{printf "%s %s\n",$3,$4}' $Src | sort -k3 > $Tmp
            if ! cmp -s $Dst $Tmp
            then
                  diff $Dst $Tmp | awk '$1~/<|>/{
                     if ($1~/</)printf "released %s %s\n",$2,$3;
                     if ($1~/>/)printf "assigned %s %s\n",$2,$3;
                  }'
               scp $Tmp root@$Target:/etc/pihole/custom.list > /dev/null && \
               ssh root@$Target "chmod 744 /etc/pihole/custom.list;pihole restartdns reload" && \
               cp $Tmp $Dst && \
               Msg "Updated custom.list on $Target" 
            fi
      fi
      done
   done
   rm -f $Dst 
   rm -f $Tmp 
}

function Restart {
   Stop
   Start
}

case "$1" in
   Start)
      Start
      ;;
   Stop)
      Stop
      ;;
   Restart)
      Restart
      ;;
   Status)
      systemctl status SyncDHCP.service
      ;;
   *)
      echo "Usage: $0 {Start|Stop|Restart|Status}"
      ;;
esac

2 Likes

This could be tackled by DNS means, by simply configuring your two Pi-hole's DHCP and DNS options.

Configure your DHCP servers for a different Pi-hole domain name and a distinctive Range of IP addresses to hand out via Settings | DHCP, e.g. lan1 and lan2.

Then head to the bottom of Settings | DNS and enable Pi-hole's Conditional Forwarding and point requests for lan1 to your second Pi-hole and vice versa.

Hmm, multiple local domain names is not desirable in our case. Are you certain that is required?

Pi-hole will forward the DNS request based on that domain name.

If you'd use the same local domain on both Pi-holes, you'd have configured a partial DNS loop for unknown hostnames.

You may be able to short-circuit that loop by defining a client-specific blocking rule on one Pi-hole for local hostname requests from the respective other.

Try adding the following rule as RegEx filters:

^[^\.]+(\.local\.domain)?$

where local.domain is your local domain name.

In addition, you'd have to create a Group (e.g. 'prevent local loops'), then add your respective other Pi-hole as its sole Client and attach it to your 'prevent local loops' group.

Yes, I thought I had tried this and wound up with DNS loops.

Our environment is a little involved: 6 local subnets, DHCP forwarding to the piholes is handle by our Cisco router, each subnet requires separate (but single) domain name AND search domains list for each lease. To top it off, we have a parent company to which I must forward requests for the corporate domains.

Our current method is working great and it is fast!

Even if get conditional forwarding working, aren't I going to have an extra forward for (potentially) half the systems on our local subnets?