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)
9.9 KiB
| title | domain | category | tags | status | created | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Pi-hole DoH / DoT Bypass Defense | selfhosting | dns-networking |
|
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:
- 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.
- IoT / smart devices with hardcoded public DNS. Chromecast, Google Home, Nest, many Samsung TVs, some Amazon devices include hardcoded
8.8.8.8or1.1.1.1. They ignore DHCP-pushed DNS entirely. - 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 = NULLstructurally prevents the most common fallback-resolver bypass. - Why the
HaGeZi doh-vpn-proxy-bypassadlist 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 broaderpro.txtor TIF list in some combinations; DoH bypass list itself is usually clean. If you use Claude on LAN and see blocks, allowlistclaude.ai— note thatapi.anthropic.comis typically not on any of these lists, so Claude Code / API traffic is unaffected. -
Zscaler ZPA / Zscaler Internet Access — this 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 forsamlsp.private.zscaler.comwill NOT fix it — you have to allowlist the CNAME target domain. The GSLB subdomains rotate, so use a regex allowlist for the wholezpath.netzone: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 reloaddnsafter. Expect to also need regex allowlists forzscaler.net,zscalertwo.net,zscalerthree.net,zscalerone.net,zscloud.netif 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, andmask-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.comfamily) 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.txtcompanion adlist — as of April 2026, HaGeZi's separateadblock/dot.txtURL returns 403. DoT resolver hostnames are folded intodoh-vpn-proxy-bypass.txtalready.
What still leaks
The DoH adlist does not defend against:
- IoT devices with hardcoded public DNS. Chromecast et al. send UDP/53 queries directly to
8.8.8.8. Pi-hole never sees them. - Apps that hardcode a DoH or DoT endpoint by IP. If an app has
1.1.1.1baked in rather thancloudflare-dns.com, the hostname block can't help. - 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.8receive 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.1etc.) unimpeded. -i br0only — 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-startis not honored. Rules added live persist until the nextrestart_firewallevent (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
- Pi-hole block mode =
NULL(default — verify). - Install HaGeZi
doh-vpn-proxy-bypassadlist. - Run
pihole -g. - Verify major DoH bootstraps return
0.0.0.0. - Optional: add router iptables rules to close the IoT/hardcoded-DNS gap.
Steps 1–4 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.
Related
- MajorPi — local Pi-hole deployment
- pihole-v6-adlist-management — adlist CRUD via SQL (v5 CLI commands don't work in v6)
- Network Overview — fleet network context