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

180 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 <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:
```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 <<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:
```bash
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 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 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 <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.
## 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