--- title: Pi-hole DoH / DoT Bypass Defense domain: selfhosting category: dns-networking tags: - pihole - dns - doh - dot - privacy - adblock - bypass - hagezi status: published created: 2026-04-22 updated: 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: ```bash dig +short @ # → 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: ```bash 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 ```bash NOW=$(date +%s) sudo pihole-FTL sqlite3 /etc/pihole/gravity.db < 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 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 ``` 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: ```sql 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: ```sql 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: ```bash # 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 -j DNAT --to :53 iptables -t nat -I PREROUTING -i br0 -p tcp --dport 53 ! -s -j DNAT --to :53 # Reject DoT so apps fall back to classic DNS (→ Pi-hole via above) iptables -I FORWARD -i br0 -p tcp --dport 853 ! -s -j REJECT --reject-with tcp-reset iptables -I FORWARD -i br0 -p udp --dport 853 ! -s -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 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