There is a great project known as Pi-hole that enables ad-blocking features (among other things) that can help both speed up your browsing experience by blocking page call-outs to ad-based sites and protect browsing history by blocking user-tracking activities. This article explains how to set up a Pi-hole active/failover pair on Raspberry Pi 3 B+ devices, and configure the devices to function as your home network primary DNS for ad-blocking within a Unifi Dream Machine Pro. It will also explain the details behind using a service such as keepalived to establish a virtual IP that the Pi-hole pair will share and manage if and when your primary Pi-hole instance becomes unavailable, and configuration of gravity-sync to keep the secondary Pi-hole instance in sync with your primary Pi-hole configurations in case such a failover occurs so the secondary can simply pick up where the primary left off, protecting your browsing experience on your home network. Note that this article specifically takes various works of art from other fantastic community contributors (see Credits in article) and combines them into a specific/opinionated architectural solution - no ownership of previous work is claimed or assumed.
Requirements
This tutorial uses 2x Raspberry Pi 3 B+ devices as the primary and secondary Pi-hole instances, but the steps detailed here could be adapted to support other device-specific implementations of Pi-hole primary/secondary assuming the Pi-hole and gravity-sync functionality support it (e.g. running multiple docker containers, multiple VMs, separate hardware, etc.).
Additionally, this tutorial includes details about configuring the Pi-hole setup in your Unifi Dream Machine Pro configuration. However, up to this point in the tutorial, everything is applicable for any network that has the ability to specify a DNS server endpoint and you do not actually need Unifi-specific gear to reap the benefits of this architecture.
Specifically, this tutorial assumes the following architecture:
- Unifi Dream Machine Pro as core router in network with DHCP server issuing IPs and DNS endpoint IPs to clients.
- 2x Raspberry Pi 3 B+ devices installed with the Raspberry Pi operating system, hard-wired to the Dream Machine Pro or similar connected switch.
- Virtual IP
192.168.1.170
as the DNS IP to be used by the DHCP server issuing client IPs, which will fail over between Pi-hole devices as required. - 2x static IP addresses
192.168.1.171
and192.168.1.172
reserved and assigned to the Pi-hole instances for primary and failover, respectively.
It is also assumed that you know how to install the Raspberry Pi OS, enable SSH access to your Raspberry Pi instances, and understand the importance of
following certain provided security tips by the Raspberry Pi documentation (e.g. changing your default pi
user password, etc.). Those items will not
be covered here.
Prerequisites
There are several things you should consider doing and installing on your Raspberry Pi devices prior to proceeding. Run the following steps on both of your primary/secondary Pi-hole devices.
First, upgrade all packages to bring your OS up to date with the latest/greatest:
$ sudo apt-get update
$ sudo apt-get upgrade
Next, install some prerequisite packages that gravity-sync depends on:
$ sudo apt-get install sqlite3 sudo git rsync ssh
Next, a little bit of IP management is in order. On whatever device is serving as your DHCP server (e.g. Dream Machine Pro), reserve the 3x IP addresses
indicated in the Requirements section above (or whatever IPs you wish to use, just ensure you replace the future content of the tutorial with your
respective IPs). Assign 192.168.1.171
to Raspberry Pi Pi-hole 1 (primary), and 192.168.1.172
to Raspberry Pi Pi-hole 2 (secondary). Once done, you
should likely restart your Raspberry Pi instances to both provide a clean boot with upgraded packages and receive the newly-allocated static IP addresses.
$ sudo reboot
Installing and Configuring Pi-hole
At the risk of duplicating installation instructions captured by the official documentation here, the following command should be run to install Pi-hole on both Pi-hole devices (as root):
$ curl -sSL https://install.pi-hole.net | bash
Walk through the installation instructions, ensuring (most importantly) that the static IP address for each of the primary/secondary instances is correct in the configuration when prompted. Once you have completed the setup on each Pi device, take note of the initial login password for the admin interface and log into http://192.168.1.171/admin and http://192.168.1.172/admin web interfaces for the primary and secondary Pi-hole instances, respectively, to ensure you can see the initial configuration and to change the login password to something you’re comfortable with.
You’re free to now attempt using the primary Pi-hole and creating configurations in the web interface. However, ensure you only manipulate things in the primary Pi-hole interface as any secondary instance configurations will be overwritten once we finally get gravity-sync installed and configured.
Installing and Configuring keepalived
Next is our failover functionality for the Pi-hole instances. First, install keepalived
and its dependencies on each of the Pi-hole instances, and enable
keepalived
to start on boot of the device:
$ sudo apt-get -y install keepalived
$ sudo systemctl enable keepalived.service
You’re likely going to want a static /etc/hosts
file on each of the Pi-hole instances to avoid any DNS-related failures causing hiccups in your
failover setup. Edit the /etc/hosts
file on each of the Pi-hole instances to include the 3x IP to host mappings for the Pi-Hole instances and
virtual IP address (should look similar to the following):
192.168.1.170 pihole.localdomain pihole
192.168.1.171 pihole1.localdomain pihole1
192.168.1.172 pihole2.localdomain pihole2
Each of the records should be resolvable on both Pi-hole instances at this point. Next, configure keepalived
on each of the respective hosts by
editing the configuration file /etc/keepalived/keepalived.conf
, replacing the password <SUPERSECRETPASSWORD>
with a password that suits your needs:
# pihole1.localdomain
vrrp_instance ADBLOCK {
state MASTER
interface eth0
virtual_router_id 51
priority 255
advert_int 1
authentication {
auth_type PASS
auth_pass <SUPERSECRETPASSWORD>
}
virtual_ipaddress {
192.168.1.170/24
}
}
# pihole2.localdomain
vrrp_instance ADBLOCK {
state BACKUP
interface eth0
virtual_router_id 51
priority 254
advert_int 1
authentication {
auth_type PASS
auth_pass <SUPERSECRETPASSWORD>
}
virtual_ipaddress {
192.168.1.170/24
}
}
Once the configuration file is in place, restart the keepalived
service on each of the Pi-hole instances:
$ sudo systemctl restart keepalived.service
You can inspect the /var/log/messages
log file on each instance, demonstrating which was elected as MASTER
vs. which obtained BACKUP
status, and
also have a look at the IP configurations on each instance to see that the MASTER
should have the virtual IP allocated while the BACKUP
should not:
# pihole1
$ sudo ip -brief addr show
# pihole2
$ sudo ip -brief addr show
Additionally, if you attempt to visit http://192.168.1.170/admin/ you should receive a response from the MASTER
instance,
or if your curl
library on a device supports specifying --dns-servers
, you can specify using 192.168.1.170
as the DNS server and you should be able
to see the request/response logs in /var/log/pihole.log
on the MASTER
instance.
Testing Failover
Now that we have both Pi-hole instances configured with keepalived
, let’s simulate a failover/fail-back. On the MASTER
Pi-hole instance, run the
following to turn off keepalived
:
$ sudo systemctl stop keepalived.service
On the BACKUP
Pi-hole instance in /var/log/messages
, you should see something similar to ...Entering MASTER STATE
and any requests to the virtual
IP 192.168.1.170
should be responded to by pihole2
. Now let’s simulate a fail-back by re-starting the keepalived
service on the original MASTER
of
pihole1
:
$ sudo systemctl start keepalived
You should similarly see logs indicating that pihole1
took back control of the virtual IP endpoint. Failover is working well!
Gravity Sync
Having a primary and secondary Pi-hole instance is great, except currently (as of Pi-hole version 5.2.1
) there is no native mechanism to automatically sync
configurations between the Pi-hole instances, meaning any block/permit lists would need to be updated in both Pi-hole interfaces, which is cumbersome. Luckily,
a great project named gravity-sync has been developed and provides us the syncing capability we need. Although this
sync functionality does not address things such as DHCP, etc. - we only care about permit/block functionality in this tutorial, so this project will serve us
well.
First, install the gravity-sync
components on the primary Pi-hole instance pihole1
:
$ sudo su -
$ export GS_INSTALL=primary && curl -sSL https://gravity.vmstan.com | bash
Next, install the respective components on the secondary Pi-hole pihole2
:
$ sudo su -
$ export GS_INSTALL=secondary && curl -sSL https://gravity.vmstan.com | bash
Ensure you answer the respective questions in the installer and set up the SSH key functionality you need in order to have gravity-sync successfully copy
configurations from the primary to the secondary. Once complete, on the secondary pihole2
, you can do a check of what might need to be sync’d, perform a
one-time sync, and even schedule routine sync operations via cron. Note that the commands below assume the gravity-sync repository was cloned to
~root/gravity-sync/
- it would be better to have this be more readily available as a callable script in /usr/local/bin
, but that exercise will be
left up to the reader.
# on pihole2:
$ sudo su -
$ cd gravity-sync
# perform a diff check from the primary
$ ./gravity-sync.sh compare
# perform a one-time sync operation
$ ./gravity-sync.sh
# configure scheduled sync operations
$ ./gravity-sync.sh automate
# answer the questions asked
Scheduled sync operations are nice and in this tutorial, we’re going to use 30 minute intervals for sync operations. In addition, following the sync configuration,
you can check the crontab for the root user (crontab -l
) to see that the sync operation is in place.
You now have auto-syncing Pi-hole instances, which means you can edit all rules in your primary pihole1
and only ever interact with a single administrative
interface vs. needing to keep things in sync by visiting and editing both web interfaces!
Unifi Configuration for Pi-hole Setup
If you’ve made it this far, you should now have a 2-node Pi-hole failover cluster interacting with a single virtual IP address that can be used for any DNS
configuration of clients on your network. You can take this virtual IP (192.168.1.170
) and put it into whatever configuration you need based on the vendors and
devices you own, but in our case, we will specifically point out that one way to use this functionality is to insert the virtual IP 192.168.1.170
into the
Settings -> Networks -> LAN -> DHCP Name Server
option in a Unifi Dream Machine Pro controller, updating the Auto
setting to instead be Manual
and
specifying 192.168.1.170
as the DNS server 1
parameter. Saving these settings will ensure that on a DHCP lease renew by a client, that client will then have
its DNS configuration (/etc/resolv.conf
for *nix-based devices, or similar) updated to point to your Pi-hole primary/secondary failover setup!
Happy ad-blocking!
Further Improvements
There’s a lot left to be desired in the above configuration - some topics that might be of use that the reader can implement:
- Monitoring of
keepalived
: If you can configure email or other notification-based alerts (either natively in thekeepalived
configuration or external using a monitoring agent such as monit or similar), you can be alerted to the fact that your primary Pi-hole has gone down/failover has ensued. The risk in a nice failover configuration such as described above without having alerting is you may continue to surf the internet during a failover event and never notice/be notified such that you would then be in a single point of failure if the primary Pi-hole is down. - Adjustment of gravity-sync Functionality: As mentioned previously, shuffling the
gravity-sync.sh
functionality into a morePATH
-based implementation that does not rely on a local git clone directory to function.
Credit
The above tutorial was pieced together with some information from the following sites/resources, among others that were likely missed in this list: