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)
180 lines
9.9 KiB
Markdown
180 lines
9.9 KiB
Markdown
---
|
||
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 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
|