I stumbled onto NextDNS recently — it’s like a cloud hosted Pi-hole. I tried it for a few days, but this post isn’t about that.

Reading about the different block lists in NextDNS, and digging deeper into DNS ad blocking gave me motivation to change my current setup — and that is what this post is about 🙂

Table of contents

Introduction

I’ve been using a local Unbound DNS resolver, with ad blocking, for a while. It’s been working just fine, although I haven’t paid much attention to it — like updating the block list 🤷

After about three days of “internet research”, I decided to give Knot Resolver a try. It’s made by CZ.NIC — the same people behind Knot DNS, which I have played with in the past and really liked 🙂

Knot Resolver is a minimalistic implementation of a caching validating DNS resolver. Modular architecture keeps the core tiny and efficient, and it provides a state-machine like API for extensions.

The Cloudflare DNS resolver 1.1.1.1 is also build around Knot Resolver.

Ad blocking guidelines

There are lots of block lists out there, and many are overlapping. I spend some time finding the “best” ones — osid was one that many people seemed happy with. It contained lots of entries, few false positives, and is actively maintained.

I also found a Github repository with a great deal of useful information about NextDNS — and DNS ad blocking in general. It recommended 1Hosts and osid for a balanced blocking:

Balanced: minimal breakage; largely set-and-forget but you may need to allowlist occasionally to unsubscribe from junk email

The Balanced tier is recommended for everyday browsing, based on my testing and user feedback.

It also followed some some good guidelines for DNS ad blocking:

  1. Prevent overblocking by utilizing the law of diminishing returns (e.g., using sane, quality blocklists; allowing most TLDs; etc.).
  2. Pass the girlfriend test with few exceptions. These deviations are documented throughout the guide.

The repository goes into great detail on the NextDNS configuration, but is also very relevant for DNS ad blocking in general. I think the point on diminishing returns is very important.

Setting up Knot Resolver

Time to get this Knot Resolver show on the road — I’m going to set it up as a container in Proxmox. On the same host that has my Mikrotik CHR (Cloud Hosted Router).

Container in Proxmox

Since the container will run on the same Proxmos host as my router; I set up a Linux bridge without bridge ports. This creates a local network — only on this machine.

Linux bridge, in Proxmox

Container specs:

  • Template: Debian 11
  • CPU cores: 1
  • Memory: 512 GB
  • Disk: 8 GB
  • Network: The bridge created above, vmbr2

I didn’t set up a DHCP server on this network, instead I set the IPv4 and IPv6 addresses manually.

Container network device, in Proxmox

After adding the interface to my virtual router, and setting IP addresses on its interface — I had connectivity between the router and new container 🙂

Installing and configuring Knot Resolver

Installation is easy, and explained in the documentation:

$ wget https://secure.nic.cz/files/knot-resolver/knot-resolver-release.deb
$ sudo dpkg -i knot-resolver-release.deb
$ sudo apt update
$ sudo apt install -y knot-resolver

I used the following configuration in /etc/knot-resolver/kresd.conf:

-- SPDX-License-Identifier: CC0-1.0
-- vim:syntax=lua:set ts=4 sw=4:
-- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/

-- Network interface configuration
net.listen('127.0.0.1', 53, { kind = 'dns' })
net.listen('10.xxx.xxx.53', 53, { kind = 'dns' })
net.listen('127.0.0.1', 853, { kind = 'tls' })
--net.listen('127.0.0.1', 443, { kind = 'doh2' })

net.listen('::1', 53, { kind = 'dns', freebind = true })
net.listen('2a01:xxxx:xxxx:xxxx::53', 53, { kind = 'dns', freebind = true })
net.listen('::1', 853, { kind = 'tls', freebind = true })
--net.listen('::1', 443, { kind = 'doh2' })

-- Load useful modules
modules = {
	'hints > iterate',  -- Allow loading /etc/hosts or custom root hints
	'stats',            -- Track internal statistics
	'predict',          -- Prefetch expiring/frequent records
	'serve_stale < cache',
	'workarounds < iterate',
}

-- Cache size
cache.size = 100 * MB

-- Prefetch learning
predict.config({
    window = 30, -- 30 minutes sampling window
    period = 24*(60/15) -- track last 24 hours
})

-- Split-horizon DNS
policy.add(policy.rpz(policy.DENY, '/etc/knot-resolver/local_domains.rpz', true))

-- Ad blocking lists
policy.add(policy.rpz(policy.DENY_MSG('Blocked by 1Hosts'), '/etc/knot-resolver/1hosts-lite.rpz', true))
policy.add(policy.rpz(policy.DENY_MSG('Blocked by osid'), '/etc/knot-resolver/osid.rpz', true))

-- If the response is our WAN address, replace it with the local IP of the reverse proxy
policy.add(policy.all(policy.REROUTE({['my.wan.ip.adr'] = '10.xxx.xx.1'})), true)

-- Log policy operations
log_groups({'policy'})

-- Forward local DNS queries to the router
internalDomains = policy.todnames({'lan.uctrl.net', '10.in-addr.arpa', '168.192.in-addr.arpa'})
policy.add(policy.suffix(policy.FLAGS({'NO_CACHE'}), internalDomains))
policy.add(policy.suffix(policy.STUB({'10.xxx.xxx.1'}), internalDomains))

-- Forward everything else over TLS to CloudFlare 1.1.1.1 resolver
policy.add(policy.all(policy.TLS_FORWARD({
  {'1.1.1.2', hostname='security.cloudflare-dns.com'},
  {'1.0.0.2', hostname='security.cloudflare-dns.com'},
  {'2606:4700:4700::1112', hostname='security.cloudflare-dns.com'},
  {'2606:4700:4700::1002', hostname='security.cloudflare-dns.com'}
})))
I’m using 1.1.1.1 for families as my upstream DNS provider, as it blocks malware.

For blocklists and local DNS entries; I chose to use RPZ, Response policy zone, which Knot Resolver supports.

For local DNS entries; I created /etc/knot-resolver/local_domains.rpz:

; left hand side          ; TTL and class  ; right hand side
; encodes RPZ trigger     ; ignored        ; encodes action
; (i.e. filter)

$TTL 300
@		SOA  localhost. root.localhost.  (
                        2   ; serial 
                        3H  ; refresh 
                        1H  ; retry 
                        1W  ; expiry 
                        1H) ; minimum 
                NS    localhost.

status.my-domain.no CNAME   rpz-passthru.
mail.my-domain.no   A       192.168.1.12
*.my-domain.no      A       10.xxx.xx.10
I’ve obscured some IP addresses in the configuration files above. You need to use values that match your environment.

For blocklists; I created a simple Bash script that downloads the lists and copies them to the Knot Resolver configuration folder:

#!/bin/bash

wget https://o0.pages.dev/Lite/rpz.txt -O 1hosts-lite.rpz
wget https://rpz.oisd.nl/ -O osid.rpz
sudo mv *.rpz /etc/knot-resolver/

For all RPZ policies; I set Knot Resolver to reload if the file changes.

Reloading is controlled by setting watch to true in the RPZ policy:

policy.rpz(action, path[, watch = true])

All that is left now is to start, or restart, Knot Resolver:

$ sudo systemctl restart kresd@1.service
I didn’t understand the @1.service at first, why @1? This is because you can run multiple instances of Knot Resolver, to spread the load on multiple CPUs, and do zero-downtime restart. More information in the documentation.

Since we have log_groups({'policy'}) in the configuration — operations related to policy will be logged. So you should see things like local DNS rewrites, ad blocking, and RPZ reloading in the journal:

$ sudo journalctl -f -t kresd

[policy] RPZ reloading: 1hosts-lite.rpz
[policy] RPZ reloading: osid.rpz
[policy] ANSWER (forged) applied for mail.my-domain.no. A
[policy] DENY_MSG applied for logs.browser-intake-datadoghq.com. A

Getting clients to use the resolver

Now we need to get local network clients to use the new resolver, and there are two ways to do that:

  1. Knot Resolver is upstream DNS server to the router
    • Clients continue to use the router IP as their DNS server.
    • Requests for local DNS hostnames are answered by the router and not forwarded to Knot Resolver.
    • The only thing we need to change is the upstream DNS server in the router.
    • Knot Resolver will only see the router as client.
  2. Clients use Knot Resolver directly
    • Clients gets assigned Knot Resolver as DNS server — by DHCP, or manually.
      • It can take some time for the change to propagate to all clients, depending on the DHCP lease time.
    • Knot Resolver must forward local DNS hostname requests to the router.
    • Clients must be allowed to reach Knot Resolver through any firewall if they are on different networks.
    • Knot Resolver will see all actual clients.
Flowchart for a local DNS resolver

Both options work just fine and have their pros and cons. I’m personally leaning towards option #2 — simply because I think a dedicated DNS resolver may do a better job of responding to queries than the router. Depending entirely on the router DNS implementation of course.

I’m not sure how well the Knot Resolver predict module works — but I assume it would be better if clients query it directly, not through the router.

If using option #1, you can block Knot Resolver from forwarding local DNS lookups. They should be answered by the router, if not — they probably don’t exist. Either way; there’s no point asking a public resolver for a local DNS record.

policy.add(policy.suffix(policy.DENY, {todname('lan.uctrl.net.')}))

Compared with NextDNS

I did use NextDNS as my upstream DNS provider for about a week — this gave me some insight into what got through my local DNS blocking.

Here are some statistics for six days, from NextDNS:

  • On day #1 NextDNS did all the ad blocking
  • On day #2 I started using local blocking
  • The rest shows entries that made it through my local DNS blocking
Queries Blocked % Blocked
53,508 6,364 11.89
47,283 1,285 2.72
42,442 288 0.68
47,685 237 0.50
48,778 235 0.48
55,608 749 1.35

Looking at the requests that wasn’t blocked locally, almost all queries was for these five domains:

  • device-metrics-us-2.amazon.com
  • lcprd1.samsungcloudsolution.net
  • oempprd.samsungcloudsolution.com
  • cdn-settings.appsflyersdk.com
  • geolocation.onetrust.com

These domains are included in the NextDNS Ads & Trackers Blocklist blocklist, but not in 1Hosts or osid.

I found some posts on Reddit claiming that blocking the Amazon and Samsung ones caused problems with some devices. I haven’t spent any more time looking into it — something something about diminishing returns 😉

Remember kids: it’s always DNS 🖖