majorwiki/02-selfhosting/dns-networking/pihole-doh-dot-bypass-defense.md
majorlinux 91455fac39 Add 7 articles; update nav and existing articles (2026-04-25)
New articles:
- pihole-doh-dot-bypass-defense
- pihole-v6-adlist-management
- mastodon-db-maintenance
- mastodon-federation
- fantastical-google-phantom-calendar-syncselect
- rsync-tailscale-teardown-stall
- ollama-chat-template-pipe-stdin-bypass

Updated: wsl2-backup, wsl2-rebuild, ssh-config-key-management,
selfhosting index, mastodon-instance-tuning, ansible-check-mode,
windows-openssh, windows-sshd, yt-dlp, README, SUMMARY, index
Removed: fedora-usrmerge-ebtables-blocker (superseded by prior push)
2026-04-25 17:52:48 +00:00

9.9 KiB
Raw Blame History

title domain category tags status created updated
Pi-hole DoH / DoT Bypass Defense selfhosting dns-networking
pihole
dns
doh
dot
privacy
adblock
bypass
hagezi
published 2026-04-22 2026-04-23T09:09

Pi-hole DoH / DoT Bypass Defense

The Problem

A LAN-wide ad/tracker/threat-intel blocklist at the DNS layer is only effective if clients actually use the DNS server doing the blocking. Three classes of client routinely bypass LAN DNS:

  1. Modern browsers with built-in DNS-over-HTTPS (DoH). Chrome, Firefox, Safari, Edge all ship with DoH either on by default or a one-toggle opt-in. When enabled, the browser sends DNS queries over HTTPS directly to Cloudflare / Google / Quad9 / NextDNS, bypassing the OS resolver and every DNS-layer blocklist on the network.
  2. IoT / smart devices with hardcoded public DNS. Chromecast, Google Home, Nest, many Samsung TVs, some Amazon devices include hardcoded 8.8.8.8 or 1.1.1.1. They ignore DHCP-pushed DNS entirely.
  3. Applications using DNS-over-TLS (DoT). Rarer than DoH but used by some privacy-focused apps and occasional malware C2 — hits Cloudflare / Quad9 on port 853 instead of 53.

Without defense, a compromised IoT or a telemetry-hungry app can exfil DNS traffic freely even though Pi-hole is "running."

What This Guide Covers

  • How Pi-hole's blocking.mode = NULL structurally prevents the most common fallback-resolver bypass.
  • Why the HaGeZi doh-vpn-proxy-bypass adlist is the single highest-leverage defense against browser DoH.
  • What still leaks and how to assess whether the router-level firewall is worth the effort for your threat model.

Pi-hole's block mode matters

Pi-hole v6's default dns.blocking.mode is NULL. A blocked domain resolves to 0.0.0.0 — a valid DNS answer, not an NXDOMAIN. Verify on your host:

dig +short <blocked-domain> @<pihole-ip>
# → 0.0.0.0

Why this matters: multi-resolver OSes (macOS, iOS, Windows) only consult fallback resolvers on a failure (timeout, SERVFAIL). A valid NULL answer short-circuits that — the client accepts the 0.0.0.0, tries to connect, fails at TCP, and never retries DNS. Even if /etc/resolv.conf has 1.1.1.1 as a secondary, it's never queried.

If you've set blocking mode to NXDOMAIN, clients will fall back — and every telemetry domain on every adlist becomes bypassable through whatever secondary resolver the OS is configured with. Leave it at NULL.

Check:

pihole-FTL --config dns.blocking.mode
# → NULL

HaGeZi DoH/VPN/Proxy Bypass — the biggest single win

HaGeZi maintains adblock/doh-vpn-proxy-bypass.txt — ~18,000 DoH resolver hostnames, including the bootstrap domains used by every major browser:

Browser DoH bootstrap
Firefox mozilla.cloudflare-dns.com
Chrome chrome.cloudflare-dns.com, dns.google
Safari (iCloud Private Relay bootstrap) Apple-specific, not in this list — Apple uses QUIC
Edge dns.google, other public resolvers

When the bootstrap hostname can't be resolved (Pi-hole answers 0.0.0.0), the browser's DoH setup fails and it falls back to the system resolver — which is Pi-hole. This flips the default behavior from "browsers can bypass" to "browsers respect LAN DNS."

Adding it

NOW=$(date +%s)
sudo pihole-FTL sqlite3 /etc/pihole/gravity.db <<SQL
INSERT INTO adlist (address, enabled, comment, date_added, date_modified, type)
VALUES
  ('https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/doh-vpn-proxy-bypass.txt',
   1, 'HaGeZi DoH/VPN/Proxy bypass', $NOW, $NOW, 0);
SQL
sudo pihole -g

See pihole-v6-adlist-management for general adlist CRUD via SQL.

Verification

After pihole -g completes, probe major DoH hostnames:

for h in mozilla.cloudflare-dns.com dns.google chrome.cloudflare-dns.com dns.quad9.net; do
  echo -n "$h → "
  dig +short $h @<pihole-ip>
done
# All should return 0.0.0.0

Known false positives

The list is aggressive. Expect occasional pushback:

  • claude.ai — gets caught by the broader pro.txt or TIF list in some combinations; DoH bypass list itself is usually clean. If you use Claude on LAN and see blocks, allowlist claude.ai — note that api.anthropic.com is typically not on any of these lists, so Claude Code / API traffic is unaffected.

  • Zscaler ZPA / Zscaler Internet Accessthis will break work-from-home auth if you don't allowlist it. The DoH/VPN bypass list classifies Zscaler's ZTNA backbone as a "VPN proxy" and blocks it. Symptom: users see a blank / failed page at https://samlsp.private.zscaler.com/... during SAML sign-in, and the Zscaler Client Connector fails to authenticate.

    The critical piece is that Zscaler's SAML SP hostname is a CNAME chain:

    samlsp.private.zscaler.com.    CNAME samlsp.prod.zpath.net.
    samlsp.prod.zpath.net.         CNAME zapp2saml.gslb.prod.zpath.net.
    zapp2saml.gslb.prod.zpath.net. CNAME snico2br.gslb.prod.zpath.net.
    snico2br.gslb.prod.zpath.net.  A     <IP>
    

    Pi-hole walks the CNAME chain and blocks on the target (status 9 = blocked_gravity_cname), so an exact-hostname allowlist for samlsp.private.zscaler.com will NOT fix it — you have to allowlist the CNAME target domain. The GSLB subdomains rotate, so use a regex allowlist for the whole zpath.net zone:

    INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment)
    VALUES (2, '(\.|^)zpath\.net$', 1, 'Zscaler ZPA CNAME backbone — do not block');
    

    Don't forget pihole reloaddns after. Expect to also need regex allowlists for zscaler.net, zscalertwo.net, zscalerthree.net, zscalerone.net, zscloud.net if any are gravity-blocked — HaGeZi's lists may cover different combinations over time.

  • iCloud Private Relay — if you want iCPR to keep working on your Apple devices, allowlist its mask ingresses. The DoH/VPN bypass list blocks mask.icloud.com, mask-h2.icloud.com, and mask-api.icloud.com (Apple's iCPR entrance points). Without them, iCPR silently falls back to standard DNS — which means Pi-hole is covering the bypass whether you want it to or not. For hosts where iCPR is desired:

    INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment)
    VALUES (2, '(\.|^)mask[a-z0-9-]*\.icloud\.com$', 1, 'iCloud Private Relay ingress');
    

    Keep this surgical — do not allowlist all of icloud.com. Other subdomains (metrics.icloud.com, init.gc.apple.com family) are Apple telemetry that the adlists correctly block. After allowlist + pihole reloaddns, toggle Wi-Fi or flip iCPR off/on in Settings on each Apple device to force DNS re-resolution — iOS/macOS caches DNS aggressively and won't pick up the change otherwise.

  • dot.txt companion adlist — as of April 2026, HaGeZi's separate adblock/dot.txt URL returns 403. DoT resolver hostnames are folded into doh-vpn-proxy-bypass.txt already.

What still leaks

The DoH adlist does not defend against:

  1. IoT devices with hardcoded public DNS. Chromecast et al. send UDP/53 queries directly to 8.8.8.8. Pi-hole never sees them.
  2. Apps that hardcode a DoH or DoT endpoint by IP. If an app has 1.1.1.1 baked in rather than cloudflare-dns.com, the hostname block can't help.
  3. Apple iCloud Private Relay. Uses QUIC (UDP/443) to Cloudflare with oblivious DNS. Safari + Apple services route around Pi-hole entirely. Acceptable tradeoff for most users; mostly a privacy win even if it weakens your LAN-side visibility.

Estimated residual gap after the DoH adlist: ~3% of tracker/telemetry traffic, mostly from hardcoded-DNS IoT.

Router-level enforcement (optional, higher effort)

To close the remaining 3%, block outbound udp/53, tcp/53, tcp/853 at the router for everything except the Pi-hole's IP. Two rules:

# Transparently redirect all LAN :53 traffic to Pi-hole, except Pi-hole itself
iptables -t nat -I PREROUTING -i br0 -p udp --dport 53 ! -s <pihole-ip> -j DNAT --to <pihole-ip>:53
iptables -t nat -I PREROUTING -i br0 -p tcp --dport 53 ! -s <pihole-ip> -j DNAT --to <pihole-ip>:53

# Reject DoT so apps fall back to classic DNS (→ Pi-hole via above)
iptables -I FORWARD -i br0 -p tcp --dport 853 ! -s <pihole-ip> -j REJECT --reject-with tcp-reset
iptables -I FORWARD -i br0 -p udp --dport 853 ! -s <pihole-ip> -j REJECT

Design choices:

  • REDIRECT (DNAT), not DROP, for port 53 — devices with hardcoded 8.8.8.8 receive transparent answers from Pi-hole instead of silently breaking.
  • REJECT, not DROP, for port 853 — DoT clients see a fast error and fall back to classic DNS immediately instead of timing out.
  • Exempt the Pi-hole — it needs to reach upstream resolvers (1.1.1.1 etc.) unimpeded.
  • -i br0 only — LAN ingress, not WAN.

Persistence depends on router firmware

  • Asuswrt-Merlin: add rules to /jffs/scripts/firewall-start — runs on every firewall init.
  • Stock AsusWRT 388+: /jffs/scripts/firewall-start is not honored. Rules added live persist until the next restart_firewall event (reboot, WAN flap, GUI config change). Workarounds: flash to Merlin, use the GUI's "LAN ▸ Network Services Filter" (DROP-only, less flexible), or schedule a cron re-apply in /jffs/configs/crontab.
  • OpenWrt / pfSense / OPNsense: their respective firewall config persistence works out of the box.

Summary — minimum viable DoH defense

  1. Pi-hole block mode = NULL (default — verify).
  2. Install HaGeZi doh-vpn-proxy-bypass adlist.
  3. Run pihole -g.
  4. Verify major DoH bootstraps return 0.0.0.0.
  5. Optional: add router iptables rules to close the IoT/hardcoded-DNS gap.

Steps 14 give you ~97% effectiveness with zero client-side changes and no broken devices. Step 5 is polish for threat models where LAN-wide DNS visibility matters.