Home-Lab DNS

April 2022

Since setting up PowerDNS and Pi-Hole as part of my Homelab Refresh, DNS has become a little bit complicated in my home network. This post will explain how it all fits together.

  1. Overview
  2. Router
  3. PowerDNS
  4. Pi-Hole
  5. Results

Overview

Broadly, DNS traffic from clients (from both the wireless and client networks) is forced to go through my router. From there DNS is forwarded to PowerDNS, for any non local requests it is then forwarded to Pi-Hole, which then forwards it to CloudFlare. The following Diagram visually shows what is going on.

Homelab DNS diagram

Router

The configuration of the router is primarily what drives the effectiveness of this setup. Taking inspiration from a blog post by Labzilla that raises the issue of devices ignoring DHCP provided DNS, I’ve used the router to force clients to use my configuration. Since this is a Mikrotik device, I can use the CLI to configure it. Firstly I have the router configured to use the PowerDNS servers as its upstream DNS.

[adminuser@router.lab.alexgardner.id.au] > /ip dns print
                      servers: 10.1.1.31,10.1.1.32,10.1.1.33
              dynamic-servers: 
               use-doh-server: 
              verify-doh-cert: no
        allow-remote-requests: yes
          max-udp-packet-size: 4096
         query-server-timeout: 2s
          query-total-timeout: 10s
       max-concurrent-queries: 100
  max-concurrent-tcp-sessions: 20
                   cache-size: 2048KiB
                cache-max-ttl: 1w
                   cache-used: 47KiB

Destination NAT

I’m using Destination NAT (dst-nat) rules to redirect all DNS traffic from clients to the router. The Clients interface list contains both the wireless interface as well as the interfaces used exclusively by clients. To prevent any redirection issues from occurring, there is an address-list of approved IPs that can ignore these rules.

[adminuser@router.lab.alexgardner.id.au] > /ip firewall address-list print where list="PrivateDNS"
Flags: X - disabled, D - dynamic 
 #   LIST                      ADDRESS                                       CREATION-TIME        TIMEOUT             
 0   PrivateDNS                10.1.1.70                                     dec/28/2020 15:55:25
 1   PrivateDNS                10.1.1.33                                     aug/09/2021 18:00:56
 2   PrivateDNS                10.1.1.31                                     aug/09/2021 18:01:55
 3   PrivateDNS                10.1.1.32                                     aug/09/2021 18:01:57
[adminuser@router.lab.alexgardner.id.au] > /ip firewall nat print where chain=dstnat 
Flags: X - disabled, I - invalid, D - dynamic 
 0    ;;; Allow Pihole DNS
      chain=dstnat action=accept src-address-list=PrivateDNS log=no log-prefix="" 

 1    ;;; Force DNS to Pihole
      chain=dstnat action=dst-nat to-addresses=10.1.1.1 protocol=tcp in-interface-list=Clients dst-port=53 log=no 
      log-prefix="" 

 2    chain=dstnat action=dst-nat to-addresses=10.1.1.1 protocol=udp in-interface-list=Clients dst-port=53 log=no 
      log-prefix="" 

Firewall

Additionally, I’m using firewall rules to block DNS traffic that is using DNS over TLS/HTTPS. This is done with another address-list using IPs sourced from public-dns.info and a corresponding set of firewall rules.

[adminuser@router.lab.alexgardner.id.au] > /ip firewall address-list print count-only where list="PublicDNS"
5953
[adminuser@router.lab.alexgardner.id.au] > /ip firewall address-list print where list="PublicDNS"
Flags: X - disabled, D - dynamic 
 #   LIST                      ADDRESS                                       CREATION-TIME        TIMEOUT             
 0   PublicDNS                 1.1.1.1                                       aug/09/2021 17:47:02
 1   PublicDNS                 8.8.8.8                                       aug/09/2021 17:51:13
 2   PublicDNS                 8.8.4.4                                       aug/09/2021 17:51:14
 3   PublicDNS                 199.255.137.34                                aug/09/2021 17:51:53
 4   PublicDNS                 103.112.162.165                               aug/09/2021 17:51:53
 5   PublicDNS                 103.133.222.202                               aug/09/2021 17:51:53
 6   PublicDNS                 82.146.26.2                                   aug/09/2021 17:51:53
 7   PublicDNS                 94.236.218.254                                aug/09/2021 17:51:53
 8   PublicDNS                 185.81.41.81                                  aug/09/2021 17:51:53
 9   PublicDNS                 103.209.52.250                                aug/09/2021 17:51:53
10   PublicDNS                 119.160.80.164                                aug/09/2021 17:51:53
11   PublicDNS                 151.80.222.79                                 aug/09/2021 17:51:53
12   PublicDNS                 194.27.192.6                                  aug/09/2021 17:51:53
13   PublicDNS                 109.205.112.9                                 aug/09/2021 17:51:53
-- [Q quit|D dump|down]
[adminuser@router.lab.alexgardner.id.au] > /ip firewall filter print where dst-address-list="PublicDNS"
Flags: X - disabled, I - invalid, D - dynamic 
 0    ;;; Block HTTPS - PublicDNS list
      chain=forward action=reject reject-with=icmp-network-unreachable protocol=tcp src-address-list=!PrivateDNS dst-address-list=PublicDNS 
      in-interface-list=Clients dst-port=443 log=no log-prefix="" 

 1    chain=forward action=reject reject-with=icmp-network-unreachable protocol=udp src-address-list=!PrivateDNS dst-address-list=PublicDNS 
      in-interface-list=Clients dst-port=443 log=no log-prefix="" 

 2    chain=forward action=reject reject-with=icmp-network-unreachable protocol=tcp src-address-list=!PrivateDNS dst-address-list=PublicDNS 
      in-interface-list=Clients dst-port=853 log=no log-prefix="" 

 3    chain=forward action=reject reject-with=icmp-network-unreachable protocol=udp src-address-list=!PrivateDNS dst-address-list=PublicDNS 
      in-interface-list=Clients dst-port=853 log=no log-prefix="" 

This results in clients not being able to connect to public DNS servers on either port 443 (DNS over HTTPS) or 853 (DNS over TLS). Setting this up was extremely painful because Mikrotik do not currently have address-lists that can be sourced from URLs, instead each IP needed to be manually added.

I ended up grabbing the plaintext list of public DNS servers, manually removing the IPV6 servers, and then using sed to build out a list of Mikrotik CLI commands (example below) to add each IP address individually to the address list.

[adminuser@router.lab.alexgardner.id.au] > /ip firewall address-list add address=12.90.208.78 list=PublicDNS
[adminuser@router.lab.alexgardner.id.au] >

Yes, I did copy almost 6000 commands into my Mikrotik CLI! I obviously was not able to do this all at once and had to chunk it over a period of time. I’ll likely automate this process when I get around to updating the list.

PowerDNS

PowerDNS is configured as both an Authoritative server for local zones, as well as a Recursor for everything else. The Recursor service is what listens on port 53 and it forwards anything that is not a local zone to Pi-Hole.

etc/powerdns/recursor.d/recursor.conf

# allow-from=127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10
allow-from=127.0.0.0/8,10.1.1.0/24,10.1.2.0/24,10.1.3.0/24

# forward-zones=
forward-zones=lab.alexgardner.id.au=127.0.0.1:5300
forward-zones+=1.1.10.in-addr.arpa=127.0.0.1:5300

# forward-zones-recurse=
forward-zones-recurse=.=10.1.1.70;1.0.0.1;1.1.1.1

# local-address=127.0.0.1
local-address=0.0.0.0, ::

# max-cache-ttl=86400
max-cache-ttl=3600

# max-negative-ttl=3600
max-negative-ttl=300

# version-string=PowerDNS Recursor 4.1.11
version-string=PowerDNS Recursor

Pi-Hole

Since Pi-Hole is deployed into my Kubernetes Cluster, the configuration of this is set using Helm values. At the end of the day Pi-Hole is running from a docker container, so I only need to expose that container to my network. This is done using MetalLB, which provides an IP address for the Pi-Hole Kubernetes pod using the LoadBalancer resource.

kubernetes/apps/pihole/values.yaml

---
pihole:
  image:
    tag: '2022.02.1'
  serviceWeb:
    loadBalancerIP: 10.1.1.70
    annotations:
      metallb.universe.tf/allow-shared-ip: pihole-svc
    type: LoadBalancer
  serviceDns:
    loadBalancerIP: 10.1.1.70
    annotations:
      metallb.universe.tf/allow-shared-ip: pihole-svc
    type: LoadBalancer
  serviceDhcp:
    enabled: false
  podDnsConfig:
    enabled: true
    policy: "None"
    nameservers:
    - 127.0.0.1
    - 1.1.1.1
  admin:
    existingSecret: "admin-password"
    #checkov:skip=CKV_SECRET_6:Checkov thinks this is actually a password
    passwordKey: "password"
  DNS1: 1.1.1.1
  DNS2: 1.0.0.1

This configuration translates to the following in the Pi-Hole GUI.

Pi-Hole settings GUI

As indicated, DNS requests that are not blocked by Pi-Hole are forwarded to Cloudflare’s public DNS resolvers.

Results

The outcome of all of this is that any DNS requests on port 53 from clients are transparently run through both PowerDNS and Pi-Hole before being forwarded to Cloudflare’s public DNS resolvers if required, regardless of the DNS server specified.

[user@workstation homelab]$ nslookup router.lab.alexgardner.id.au 1.1.1.1
Server:   1.1.1.1
Address:  1.1.1.1#53

Non-authoritative answer:
Name: router.lab.alexgardner.id.au
Address: 10.1.1.1

[user@workstation homelab]$ nslookup router.lab.alexgardner.id.au 8.8.8.8
Server:   8.8.8.8
Address:  8.8.8.8#53

Non-authoritative answer:
Name: router.lab.lab.alexgardner.id.au
Address: 10.1.1.1

Additionally, any DNS requests that attempt to bypass this by using DNS over HTTPS or TLS are blocked.

[user@workstation homelab]$ telnet 1.1.1.1 443
Trying 1.1.1.1...
telnet: connect to address 1.1.1.1: Network is unreachable
[user@workstation homelab]$ telnet 1.1.1.1 853
Trying 1.1.1.1...
telnet: connect to address 1.1.1.1: Network is unreachable