Skip to content

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:

dnsdist.conf
-- To create this script use your text editor application, for example Nano
setLocal('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') and addLocal('[::]:53'): Udp/tcp dns listening, bind on ip any
  • pc = 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 for
  • pool_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 pool
  • newServer options name = 'pihole-host1', address = '192.168.xx.xxx:53', pool = pool_backend and useClientSubnet=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, use useClientSubnet=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 the useClientSubnet option can be found here. Unfortunately, you can not use the proxy protocol with Pi-hole
  • newServer options healthCheckMode='lazy', checkInterval=30, lazyHealthCheckFailedInterval=30, rise=2, maxCheckFailures=3, lazyHealthCheckThreshold=20, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckUseExponentialBackOff=true and lazyHealthCheckMode='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 is a.root-servers.net and that seems no problem to me. I’ll go through a few options:
    • checkInterval The time in seconds between health checks
    • lazyHealthCheckFailedInterval seconds and double between every probe
    • rise (default is 1) Require number consecutive successful checks before declaring the backend up
    • maxCheckFailures (default is 1) Allow number check failures before declaring the backend down
    • lazyHealthCheckThreshold (default is 20) The means 20% of the last lazyHealthCheckSampleSize queries should fail for a health-check to be triggered
    • lazyHealthCheckSampleSize (default is 100) 100 means the result (failure or success) of the last 100 queries will be considered
    • lazyHealthCheckMinSampleCount (default is 1) The minimum amount of regular queries that should have been recorded before the lazyHealthCheckThreshold threshold can be applied
    • lazyHealthCheckUseExponentialBackOff (default is false) 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 again
    • lazyHealthCheckMode (default is TimeoutOrServFail) 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 me
  • setECSSourcePrefixV4(32): If you use the option useClientSubnet=true: Set the source prefix-length for the EDNS Client Subnet option
  • getPool(pool_backend):setCache(pc): If you use the option pc = newPacketCache(10000, {}) then enable cache for the pool
  • addAction(AllRule(),PoolAction(pool_backend)): Matches all incoming traffic and send-it to the backend pool
  • setSecurityPollSuffix(''): Disable security feature polling. Personally, I don’t think security polling to the domain secpoll.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 level Error during startup
  • controlSocket('127.0.0.1:5199') and setKey('pVC5gO/HECwOfgFzQDjAy6v5mWYmpwcj2h546GjqDgg='): Use the console to manage dnsdist and enable encryption with setkey.

Dnsdist NixOS Installation

There are multiple methods to install Dnsdist. Specifically for NixOS, I have outlined the installation process below.

  1. 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 Nano
    services.dnsdist = {
    enable = true;
    listenAddress = "0.0.0.0"; # Default
    listenPort = 53; # Default
    extraConfig =
    "-- 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
  2. 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 Nano
    networking.firewall.allowedTCPPorts = [ 53 80 443 8080 ]; # DNS and HTTP(S)
    networking.firewall.allowedUDPPorts = [ 53 ]; # DNS
  3. Switch NixOS configuration

    Run the following command:

    # Open your terminal application
    sudo nix-collect-garbage # Optional: clean up
    sudo nixos-rebuild switch
  4. Check the results

    Run the following command to check if the dnsdist.service is working properly:

    # Open your terminal application
    systemctl status dnsdist.service

    If dnsdist is running, you can use the sudo dnsdist -c command to go to the console and view the servers with the command showServers(). Leave the console with quit. 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 -c

    Dnsdist Status

    And then you can also check the Dnsdist configuration, for example:

    # Open your terminal application
    cat /nix/store/flya5h2kz75zyafn2j2445vviqx01m4w-dnsdist.conf
  5. 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 application
    dig pi-hole.net @<IP address> -p 53
    Instructions:
    • 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).

Favorites

Comments

    No comments found for this note.

    Join the discussion for this note on Github. Comments appear on this page instantly.

    Copyright 2021- Fiction Becomes Fact