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

I have been attempting to follow this on a recent pihole v6 setup. I think I have it working but have hit a few gotchas not covered by the initial post. Thanks to flathead of course for working out the difficult bits.

I thought I'd capture the changes I've made and post these as an example to help anyone else.
No guarantees as every redundant pihole setup is different, so your milage will definately vary.

  • Other tools (orbital sync/nebula-sync) are for one main system duplicating their settings to others, this isn't what I want as this then duplicates DHCP and DNS tables.
  • This is for 2 redundant v6 piholes running gravity-sync to synchronise the black/white lists. They are running as DHCP servers with unbound as the local DNS servers.
  • The DHCP response gives the two pihole units as equal dns servers. DHCP address lists are not overlapping but work on the same subnet.
  • The newer Debian bookworm based systems won't allow root ssh connections, so ssh connections are using a normal user account with sudo controls.
  • The script still reads the /etc/pihole/dhcp.leases file and writes to the secondary pihole /etc/dnsmasq.d/ directory. This requires the settings -> All settings -> misc.etc_dnsmasq_d flag set to true on the web control or the [misc] section's etc_dnsmasq_d parameter set to true in the /etc/pihole/pihole.toml file.
  • ssh keys must be used for passwordless ssh connections between the root of one raspberry pi feeding the data to a user account on the other raspberry pi. Since these are both cross-coupled then the user accounts are easier if they are the same. The ssh keys are different. The access uses a ~/.ssh/config file to manage the key use.
  • inotify is still used via inotify-tools installation.
  • The same script architecture is used. I tested the script as a user and needed to add sudo commands before the inotifywait, cksum and awk commands. Since the systemd source script runs under root control, these are not necessary in the final script.
  • The pihole commands have been changed to work with the new pihole v6 command syntax.
  • DNS settings are updated with the host-record= configuration command as specified in the dnsmasq.d command's man pages. FTL is similar so they will hopefully continue to work across subsequent pihole releases. The generated records do not appear in the pihole web page and shouldn't affect any local DNS records set (I don't use this myself).

I would not like to guarantee the security of systems that are remotely accessible from the internet. If you have this configured then you will need to be happy with the set up.

Configuration (steps must be duplicated on both piholes):

  1. Configure both piholes with non-overlapping DHCP address pools.
  2. Copy the files to the given directory and set permissions (.sh must be chmod'd 744)
    * sudo nano /lib/systemd/system/SyncDHCP.service
    * sudo nano /usr/bin/SyncDHCP.sh
    * sudo chmod 744 /usr/bin/SyncDHCP.sh
  3. Establish ssh public key authentication between root on both systems - latest systems won't allow root ssh connections, so must use a user account (apt install openssh-server if necessary). user@hostipaddress is for the other pihole system. Leave the passphrase empty.
    * ssh-keygen
    save generated file <private keygen file> within the ~/.ssh directory
    * ssh-copy-id -i ~/.ssh/<private keygen file>.pub user@hostipaddress
  4. Add the SSH key to your list of SSH identities - permanently
    * nano ~/.ssh/config
    to add the lines (change as necessary)
        Host <ip address>
          HostName <ip address>
          User <user account>
          IdentityFile ~/.ssh/<private keygen file>
  1. Connect to all piholes as the user to set up authorized_keys files in the .ssh directory and check that all is working
  2. Copy all .ssh directory to root's (removing all other settings, so beware. This is a bit of a hack but worked for me) via
    * sudo rm /root/.ssh/*
    * sudo cp ~/.ssh/* /root/.ssh/
    * I changed the /root/.ssh/config file's IdentityFile path to be absolute (/root/.ssh/<private keygen file>) but this could be left as an absolute path to the user account's <private keygen file>.
    * test with the command sudo ssh <user>@<hostipaddress> and check that this connects and doesn't ask for a password.
  3. Install inotify-tools (sudo apt install inotify-tools)
  4. Enable the service (sudo systemctl enable SyncDHCP.service)
  5. Start the service (sudo systemctl start SyncDHCP.service)
  6. The output of the script is shown in the response to the command systemctl status SyncDHCP.service

This hopefully makes sense.

Nothing will happen immediately as the /etc/pihole/dhcp.leases file won't be changing.

The /lib/systemd/system/SyncDHCP.service file is unchanged from above. Repeated here for clarity and as an atomic record.

[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

Some settings need changing in the /usr/bin/SyncDHCP.sh file. The hostnames and associated IP addresses should be set. The username is for the other pihole's user account (one in the earlier ssh key generation). The domain is the local domain suffix set in the pihole's dns.domain configuration. These should all be set at the top of the file to make the customisation easier to adapt to other networks.

The code sudo pihole reloadlists;sleep 5;sudo pihole reloaddns is quite nasty, but the only way I could get it to work reliably with my boards. If anyone can improve this, then please do so.

The /usr/bin/SyncDHCP.sh file is tweaked to read:

#! /usr/bin/bash
Flag="/tmp/SyncDHCP.running"
Src="/etc/pihole/dhcp.leases"
Myhost1="hostname1"
Mydnsip1="xxx.xxx.xxx.xx1"
Myhost2="hostname2"
Mydnsip2="xxx.xxx.xxx.xx2"
Myuser="localuser"
Mydomain="localdomain"

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) == $Myhost1 ]
   then
     Target=$Mydnsip2
   else
     Target=$Mydnsip1
   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 -v hostdomain="$Mydomain" '$4!="*"&&$1!="duid"{printf "host-record=%s.%s,%s\n",$4,hostdomain,$3}' $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 $Myuser@$Target:/tmp/newcustom.list > /dev/null && \
               ssh $Myuser@$Target "sudo chown root:root /tmp/newcustom.list" && \
               ssh $Myuser@$Target "sudo chmod 644 /tmp/newcustom.list" && \
               ssh $Myuser@$Target "sudo mv -f /tmp/newcustom.list /etc/dnsmasq.d/50-localdns.conf" && \
               ssh $Myuser@$Target "sudo pihole reloadlists;sleep 5;sudo pihole reloaddns" && \
               cp $Tmp $Dst && \
               Msg "Updated 50-localdns.conf 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

I am glad you are getting some utility out of this.

I think you will find root logins are disabled by default in OpenSSH Server but can be conditionally enabled in /etc/ssh/sshd_config by appending:

Match Address
PermitRootLogin yes

and restarting the service.

As to restarting FTL, did "pihole restartdns reload" not work in v6?

No (Remove the restartdns functionality and promote the reloaddns functions by PromoFaux · Pull Request #5780 · pi-hole/pi-hole · GitHub).

To restart Pi-hole, you can run sudo systemctl restart pihole-FTL.service.

Hi flathead.

Yes, thanks to you it's working really well. The solution has the right level of automation and the immediate response with a low processing overhead is ideal. It also doesn't get into any nasty loops. Expansion to 3 or more piholes is possible too by altering the script destination files.

I was wondering about allowing the root ssh access by changing the OpenSSH config, but I didn't really want to allow this and since my testing went down the certificate route instead I already had this method working. Yes, there are many ways to achieve the same ends and some are better than others. In the above messages, I just captured what worked for me. Hopefully others will be able to come up with some better ways or corrections if I've made any bad mistakes.

As rdwebdesign mentions, the pihole command has changed slightly for the v6 release, and the restartdns command has been replaced. The changes are described in the pihole command help. Therefore, my changes only apply to the v6 code (and hopefully later).

Cheers.

How do you configure separate DHCP ranges for different lans? My pihole install works much better when it's handling the DHCP.

Yes, I agree. The piholes do an excellent job managing DHCP/DNS. But the web UI doesn't yet support multiple LANs. No matter!

Additional subnets can be defined using /etc/dnsmasq.d files. Simply create a /etc/dnsmasq.d/99-additional-dhcp-scopes.conf file on each server. Again, keeping the pools non-overlapping on each. Heres an example for one:

log-dhcp

# Comment out primary subnet - managed via web UI
#dhcp-range=set:homenet,10.2.2.100,10.2.2.149,255.255.255.0,1h
#dhcp-option=tag:homenet,option:router,10.2.2.254
#dhcp-option=tag:homenet,option:dns-server,8.8.8.8,8.8.4.4
#dhcp-option=tag:homenet,option:domain-name,zork.local

dhcp-range=set:wifinet,10.2.3.100,10.2.3.149,255.255.255.0,1h
dhcp-option=tag:wifinet,option:router,10.2.3.254
dhcp-option=tag:wifinet,option:dns-server,8.8.8.8,8.8.4.4
dhcp-option=tag:wifinet,option:domain-name,zork.local

See the dnsmasq documentation for full details.