DoH and DoT: Encrypting Your DNS Without Breaking the Sinkhole

Hide your lookups from the ISP without throwing away the ad-and-malware blocking you built

Contents

Classic DNS is a postcard. Every time any device on your network looks up a domain, it sends the name in clear text over UDP port 53, and everyone on the path — your ISP first among them — can read it. They can’t see the contents of the HTTPS page you then load, but they can see that you looked up example-clinic.com at 9am and some-bank.com at lunchtime, and that metadata is a remarkably complete diary of your life. Encrypting DNS closes the postcard into an envelope. DNS over HTTPS (DoH) and DNS over TLS (DoT) are the two envelopes on offer.

The trap that catches almost everyone on their first attempt is this: you enable DoH in Firefox, or point your router at Cloudflare’s encrypted resolver, feel virtuous, and quietly destroy the DNS sinkhole you spent an afternoon setting up. Because now the queries go straight out to Cloudflare, encrypted, sailing right past the Pi-hole or AdGuard Home box that was blocking ads, trackers and malware domains for your whole network. You bought privacy from your ISP at the cost of the filtering you actually use every day. The goal of this article is to have both — encrypted DNS to the outside world, and every query still passing through your local sinkhole first.

Why the naïve setup breaks the sinkhole

Advertisement

A DNS sinkhole works by being in the path. Every device asks it for names, it checks each name against blocklists, returns nothing for the bad ones and forwards the rest. That interception is the entire mechanism, and it depends on the client asking your sinkhole rather than someone else.

When you enable DoH on the client, the client stops asking your sinkhole. It opens an encrypted tunnel straight to a public resolver on port 443 and does its own DNS, invisible to your network. Your sinkhole never sees the query, so it can’t filter it, and it can’t even show you the query happened. The blocklists still exist; nothing consults them. This is why “just turn on DoH everywhere” is the wrong shape. Encryption at the client edge and filtering at the network edge are pulling in opposite directions.

The fix is to move the encryption to the correct leg of the journey. Think of a DNS query as making two hops: client → your sinkhole (a hop entirely inside your own LAN), and your sinkhole → the public internet (the hop your ISP can see). The only leg your ISP snoops is the second one. So encrypt that leg and leave the first as plain DNS. The client talks plaintext DNS to your sinkhole across your own switch, which is fine because you own that wire, and the sinkhole forwards its upstream queries over DoH or DoT to a public resolver. Your ISP sees an encrypted stream to Cloudflare or Quad9 and learns nothing; your sinkhole still sees and filters every query on the way through. Both properties, no conflict.

DoH or DoT — what’s the actual difference?

Both encrypt the same thing; they differ in how they’re carried and how easily they can be spotted.

DoT runs on its own dedicated port, 853, wrapped in TLS. It’s clean and easy to reason about, and easy for a monitoring tool to identify — which is a virtue on your own network and a liability on a hostile one, because a network that wants to block encrypted DNS can simply drop port 853.

DoH runs on 443, the same port as all your normal web traffic, and looks like any other HTTPS connection. That makes it far harder for a network to single out and block, which is the whole point on a censoring or captive network. The cost is that it’s slightly heavier and, on the client side, it’s the mechanism browsers use to quietly bypass your DNS entirely.

For encrypting your sinkhole’s upstream on a network you control, either works and DoT is marginally simpler to set up and monitor. If your threat model includes a network actively trying to block encrypted DNS, prefer DoH for the upstream. Either way, the client-to-sinkhole leg stays plain — and, as I’ll get to, you actively want to stop clients doing their own DoH.

Encrypting the upstream, three ways

Advertisement

If you run AdGuard Home, this is almost too easy: it speaks DoH and DoT upstream natively. In the DNS settings, set your upstream servers using the encrypted URL scheme and you’re done:

1
2
3
https://dns.quad9.net/dns-query
tls://dns.quad9.net
https://cloudflare-dns.com/dns-query

AdGuard resolves those, forwards over the encrypted transport, and every client on your LAN keeps pointing plainly at AdGuard. This is the least-effort path and it’s why AdGuard tends to win for people who want encrypted upstream without extra moving parts.

If you run Pi-hole, it doesn’t do encrypted upstreams itself, so you put a small proxy behind it. cloudflared in DoH-proxy mode is the common choice: it listens on plain 127.0.0.1#5053, and forwards everything it receives out over DoH. Point Pi-hole’s upstream at it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# docker-compose.yml — a DoH proxy for Pi-hole to forward to
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared-doh
    command: proxy-dns
    environment:
      - TUNNEL_DNS_UPSTREAM=https://1.1.1.1/dns-query,https://9.9.9.9/dns-query
      - TUNNEL_DNS_PORT=5053
      - TUNNEL_DNS_ADDRESS=0.0.0.0
    ports:
      - "5053:5053/udp"
    restart: unless-stopped

Then in Pi-hole’s settings you set the custom upstream to the proxy’s address on port 5053, and Pi-hole’s filtering runs exactly as before while its upstream is now encrypted.

If you run your own recursive resolver with Unbound, it can do DoT forwarding directly, no extra daemon. This is my preferred setup because it keeps the parts count low:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/unbound/unbound.conf.d/forward-tls.conf
server:
    # verify upstream certificates against the system trust store
    tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"

forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 9.9.9.9@853#dns.quad9.net
    forward-addr: 1.1.1.1@853#cloudflare-dns.com

The @853 sets the DoT port and the #hostname after each address is the certificate name Unbound validates against, which is what makes the TLS meaningful. Reload Unbound, point your sinkhole (or your clients) at it, and upstream DNS now leaves your network encrypted and authenticated.

Stop clients doing their own DNS

Encrypting the upstream solves half the problem. The other half is that your devices will, given the chance, ignore your sinkhole and do their own encrypted DNS anyway — modern browsers ship DoH and some enable it automatically. If Firefox or Chrome is resolving via its own built-in DoH, your sinkhole is bypassed for that browser no matter how nicely you configured the network. You have to actively close that door.

Three measures, in increasing order of firmness. First, honour the canary domain. Browsers that auto-enable DoH first look up a special name, use-application-dns.net; if that lookup returns NXDOMAIN, the browser backs off and uses system DNS. Configure your sinkhole to return NXDOMAIN for that domain and Firefox’s automatic DoH stands down network-wide. AdGuard and Pi-hole can both do this with a single blocklist entry.

Second, for a network you fully control, force all client port-53 traffic to your sinkhole with a NAT redirect on the router, so a device that hard-codes 8.8.8.8 still lands on your Pi-hole:

1
2
3
4
5
6
# nftables on the router: redirect all LAN DNS to the sinkhole (192.168.1.2)
chain prerouting {
    type nat hook prerouting priority -100;
    iifname "lan0" udp dport 53 ip daddr != 192.168.1.2 dnat to 192.168.1.2:53
    iifname "lan0" tcp dport 53 ip daddr != 192.168.1.2 dnat to 192.168.1.2:53
}

Third, block outbound DoT and known DoH endpoints — drop outbound port 853 at the firewall, and blocklist the well-known DoH server hostnames/IPs so a client can’t reach them directly. This is the firmest measure and the most maintenance-heavy, since DoH-on-443 is deliberately hard to block by IP and the lists shift. It’s also the natural companion to VLAN segmentation: if your IoT gear lives on its own VLAN, you can apply a hard “DNS only via the sinkhole, everything else dropped” policy to the segments that have no business talking to the wider internet directly. This same discipline — forcing DNS through the tunnel and blocking the escape routes — is exactly what a VPN kill switch does for a download box, and the mindset carries straight over.

Troubleshooting

Everything feels slower after switching to encrypted upstream. DoH and DoT add a TLS handshake to the first query, and if your resolver reopens the connection for every lookup you pay that cost constantly. Make sure your resolver keeps the TLS connection alive between queries — Unbound and cloudflared both pool connections by default — and that your sinkhole is caching aggressively, so most repeat lookups never leave the box at all. A warm cache hides almost all of the added latency. If you want to confirm rather than guess, put per-second eyes on the resolver box with something like Netdata and watch query rate and CPU while you load a busy page — you’ll see the cache doing its job.

DNS stops working entirely on first setup — a bootstrap chicken-and-egg. Your resolver needs to resolve the DoH server’s hostname (cloudflare-dns.com) before it can connect to it, but it can’t resolve anything because DNS is down. The fix is to give the upstream by IP with the hostname only for certificate validation — the 1.1.1.1@853#cloudflare-dns.com form in the Unbound example does exactly this, so no bootstrap lookup is needed.

TLS certificate validation fails. Either the CA bundle path is wrong or missing, or the hostname you pinned doesn’t match the certificate the resolver actually presents. Confirm the tls-cert-bundle points at a real, current CA store, and that the #hostname matches the provider’s documented DoT name exactly.

The sinkhole still shows zero queries from a particular device. That device is doing its own DoH. Check for a browser with DoH enabled first — the canary-domain trick handles Firefox, but a browser explicitly set to “DoH on, strict” ignores the canary and needs the firewall-level block, or a browser-policy change, to bring it back in line.

Port 853 is blocked on the network you’re on. Some networks drop DoT specifically. Switch that upstream to DoH on 443, which blends with normal web traffic and survives where DoT doesn’t.

Is it worth it?

For anyone who already runs a sinkhole, adding encrypted upstream is a genuine, low-effort win: your ISP loses its clear-text log of every domain your household visits, and you keep all the filtering. On AdGuard it’s three lines of config; on Pi-hole it’s one extra small container; on Unbound it’s a short forward-zone. I’d do it on any network I care about.

Two caveats keep it honest. Encrypting the upstream hides your queries from your ISP and hands that same visibility to whichever public resolver you forward to, so choose an upstream whose privacy policy you actually trust — you’ve moved the trust, you haven’t removed it. And the client-bypass problem is the part people underestimate: without the canary domain and, ideally, a NAT redirect forcing port 53 back to your sinkhole, a single browser quietly doing its own DoH punches a hole straight through the whole arrangement. Close that door and you get the outcome that seemed contradictory at the start — DNS your ISP can’t read, and a sinkhole that still catches everything. That combination is worth the afternoon.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.