DNS load balancing for Pi-hole with Dnsdist
Introduction
I thought it would be beneficial to implement a load balancer with health check functionality to distribute DNS traffic from devices across multiple Pi-hole instances (pointing to Unbound). After some extensive searching, I found this post. Since then, I’ve been exploring dnsdist, an impressive piece of software.
Setup
Dnsdist configuration (dnsdist.conf) and explanation
After conducting some research and reviewing these examples, I used the following configuration for dnsdist:
-- To create this script use your text editor application, for example NanosetLocal('0.0.0.0:53')addLocal('[::]:53')
-- pc = newPacketCache(10000, {})
pool_backend = 'backend'
newServer({ name = 'pihole-host1', address = '<IP address>:53', pool = pool_backend, useClientSubnet=true, healthCheckMode='lazy', checkInterval=30, lazyHealthCheckFailedInterval=30, rise=2, maxCheckFailures=3, lazyHealthCheckThreshold=20, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckUseExponentialBackOff=true, lazyHealthCheckMode='TimeoutOnly',})newServer({ name = 'pihole-host2', address = '<IP address>:53', pool = pool_backend, useClientSubnet=true, healthCheckMode='lazy', checkInterval=30, lazyHealthCheckFailedInterval=30, rise=2, maxCheckFailures=3, lazyHealthCheckThreshold=20, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckUseExponentialBackOff=true, lazyHealthCheckMode='TimeoutOnly',})
setPoolServerPolicy(roundrobin, pool_backend)setECSSourcePrefixV4(32)
-- getPool(pool_backend):setCache(pc)
addAction( AllRule(), PoolAction(pool_backend))
setSecurityPollSuffix('')
controlSocket('127.0.0.1:5199')setKey('pVC5gO/HECwOfgFzQDjAy6v5mWYmpwcj2h546GjqDgg=')-- IMPORTANT: Please read the explanation below
Explanation:
setLocal('0.0.0.0:53')
andaddLocal('[::]:53')
: Udp/tcp dns listening, bind on ip anypc = newPacketCache(10000, {})
: It is good to know that it is possible to use caching, I have disabled this because that’s what I use Pi-hole forpool_backend = 'backend'
: Servers can be part of any number of pools. Servers that are not assigned to a specific pool get assigned to the default pool that is always present. I chose to create my own backend poolnewServer
optionsname = 'pihole-host1'
,address = '192.168.xx.xxx:53'
,pool = pool_backend
anduseClientSubnet=true
: Create a server for each Pi-hole instance. Fill in the name and ip address with port number. To make sure that the source address of devices is passed from dnsdist to Pi-hole, useuseClientSubnet=true
, otherwise you will only see the name of the host on which dnsdist is installed in the query log of Pi-hole. More information about theuseClientSubnet
option can be found here. Unfortunately, you can not use the proxy protocol with Pi-holenewServer
optionshealthCheckMode='lazy'
,checkInterval=30
,lazyHealthCheckFailedInterval=30
,rise=2
,maxCheckFailures=3
,lazyHealthCheckThreshold=20
,lazyHealthCheckSampleSize=100
,lazyHealthCheckMinSampleCount=10
,lazyHealthCheckUseExponentialBackOff=true
andlazyHealthCheckMode='TimeoutOnly'
: These options are for the purpose of the health check. To check if the configured Pi-hole instances are up or down. You can have this done continuously or “lazy”, i.e. when search results fail. For my home situation, “lazy” is sufficient because I also want to avoid making a request every x seconds. With the ‘checkName’ field you can indicate which address is used to perform the health check. Default isa.root-servers.net
and that seems no problem to me. I’ll go through a few options:checkInterval
The time in seconds between health checkslazyHealthCheckFailedInterval
seconds and double between every proberise
(default is 1) Require number consecutive successful checks before declaring the backend upmaxCheckFailures
(default is1
) Allow number check failures before declaring the backend downlazyHealthCheckThreshold
(default is20
) The means 20% of the last lazyHealthCheckSampleSize queries should fail for a health-check to be triggeredlazyHealthCheckSampleSize
(default is100
) 100 means the result (failure or success) of the last 100 queries will be consideredlazyHealthCheckMinSampleCount
(default is1
) The minimum amount of regular queries that should have been recorded before the lazyHealthCheckThreshold threshold can be appliedlazyHealthCheckUseExponentialBackOff
(default isfalse
) When set to true, the delay between each probe starts at lazyHealthCheckFailedInterval seconds and double between every probe, capped at lazyHealthCheckMaxBackOff seconds. Using this option is great for my home network and will lower the amount of requests againlazyHealthCheckMode
(default isTimeoutOrServFail
)TimeoutOnly
means that only timeout and I/O errors of regular queries will be considered. This is what I need with the Pi-hole instances
setPoolServerPolicy(roundrobin, pool_backend)
: Set the load balancing policy to use. Roundrobin seems like a good choice to mesetECSSourcePrefixV4(32)
: If you use the optionuseClientSubnet=true
: Set the source prefix-length for the EDNS Client Subnet optiongetPool(pool_backend):setCache(pc)
: If you use the optionpc = newPacketCache(10000, {})
then enable cache for the pooladdAction(AllRule(),PoolAction(pool_backend))
: Matches all incoming traffic and send-it to the backend poolsetSecurityPollSuffix('')
: Disable security feature polling. Personally, I don’t think security polling to the domainsecpoll.powerdns.com
is necessary. If this setting is made empty, no polling will take place else if the result is that a given version has a security problem, the software will report this at levelError
during startupcontrolSocket('127.0.0.1:5199')
andsetKey('pVC5gO/HECwOfgFzQDjAy6v5mWYmpwcj2h546GjqDgg=')
: Use the console to manage dnsdist and enable encryption withsetkey
.
Dnsdist NixOS Installation
There are multiple methods to install Dnsdist. Specifically for NixOS, I have outlined the installation process below.
-
Add the following to
configuration.nix
to install Dnsdist using the previously explained configuration:/etc/nixos/configuration.nix # To edit use your text editor application, for example Nanoservices.dnsdist = {enable = true;listenAddress = "0.0.0.0"; # DefaultlistenPort = 53; # DefaultextraConfig ="-- Listen\n" +# This is configured with the listenAddress and listenPort options"-- addLocal('0.0.0.0:53')\n" +"addLocal('[::]:53')\n" +"\n" +"-- dns caching\n" +"-- pc = newPacketCache(10000, {})\n" +"\n" +"-- Pools\n" +"pool_backend = 'backend'\n" +"\n" +"-- members definition\n" +"newServer({\n" +"name = 'pihole-host1',\n" +"address = '<IP address>:53',\n" +"pool = pool_backend,\n" +"useClientSubnet=true,\n" +"healthCheckMode='lazy',\n" +"checkInterval=30,\n" +"lazyHealthCheckFailedInterval=30,\n" +"rise=2,\n" +"maxCheckFailures=3,\n" +"lazyHealthCheckThreshold=20,\n" +"lazyHealthCheckSampleSize=100,\n" +"lazyHealthCheckMinSampleCount=10,\n" +"lazyHealthCheckUseExponentialBackOff=true,\n" +"lazyHealthCheckMode='TimeoutOnly',\n" +"})\n" +"newServer({\n" +"name = 'pihole-host2',\n" +"address = '<IP address>:53',\n" +"pool = pool_backend,\n" +"useClientSubnet=true,\n" +"healthCheckMode='lazy',\n" +"checkInterval=30,\n" +"lazyHealthCheckFailedInterval=30,\n" +"rise=2,\n" +"maxCheckFailures=3,\n" +"lazyHealthCheckThreshold=20,\n" +"lazyHealthCheckSampleSize=100,\n" +"lazyHealthCheckMinSampleCount=10,\n" +"lazyHealthCheckUseExponentialBackOff=true,\n" +"lazyHealthCheckMode='TimeoutOnly',\n" +"})\n" +"\n" +"-- set the load balacing policy to use\n" +"setPoolServerPolicy(roundrobin, pool_backend)\n" +"\n" +"-- set the source prefix-length for the EDNS Client Subnet option\n" +"setECSSourcePrefixV4(32)\n" +"\n" +"-- enable cache for the pool\n" +"-- getPool(pool_backend):setCache(pc)\n" +"\n" +"-- Rules\n" +"-- matches all incoming traffic and send-it to the pool of resolvers\n" +"addAction(\n" +"AllRule(),\n" +"PoolAction(pool_backend)\n" +")\n" +"\n" +"-- disable security feature polling\n" +"setSecurityPollSuffix('')\n" +"\n" +"-- Use the console to manage dnsdist\n" +"controlSocket('127.0.0.1:5199')\n" +"-- Generate a key with makeKey()\n" +"setKey('pVC5gO/HECwOfgFzQDjAy6v5mWYmpwcj2h546GjqDgg=')\n" +"\n";};Instructions:
- Required Replace
<IP address>
with the IP addresses of your Pi-hole instances
- Required Replace
-
if you use the firewall, don’t forget to open port 53:
/etc/nixos/configuration.nix # To edit use your text editor application, for example Nanonetworking.firewall.allowedTCPPorts = [ 53 80 443 8080 ]; # DNS and HTTP(S)networking.firewall.allowedUDPPorts = [ 53 ]; # DNS -
Switch NixOS configuration
Run the following command:
# Open your terminal applicationsudo nix-collect-garbage # Optional: clean upsudo nixos-rebuild switch -
Check the results
Run the following command to check if the
dnsdist.service
is working properly:# Open your terminal applicationsystemctl status dnsdist.serviceIf dnsdist is running, you can use the
sudo dnsdist -c
command to go to the console and view the servers with the commandshowServers()
. Leave the console withquit
. See also the screenshot below.To start the Dnsdist console, you can copy the link after
cGroup
and paste as command and add the parameter-c
, for example:/nix/store/b5ipbxi5r7b72rbyfgqjdxzixkvsml1z-dnsdist-1.8.3/bin/dnsdist --supervised --disable-syslog --config /nix/store/flya5h2kz75zyafn2j2445vviqx01m4w-dnsdist.conf -cAnd then you can also check the Dnsdist configuration, for example:
# Open your terminal applicationcat /nix/store/flya5h2kz75zyafn2j2445vviqx01m4w-dnsdist.conf -
Testing with Dig:
To verify that DNS requests are being routed through Dnsdist to the Pi-hole server, while Dnsdist is running and at least one Pi-hole server is operational, execute the following steps from another Linux client:
# Open your terminal applicationdig pi-hole.net @<IP address> -p 53Instructions:
- Required Replace
<IP address>
with the IP addresses of your Dnsdist host
You can then test the roundrobin policy by executing a second request, which should end up in the second Pi-hole instance (check this with the
showServers()
command in the Dnsdist console). - Required Replace
No comments found for this note.
Join the discussion for this note on Github. Comments appear on this page instantly.