initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
---
name: pihole
description: Debug and manage a self-hosted Pi-hole v6 Docker deployment — DNS troubleshooting, database queries, whitelisting, and common failure patterns.
category: self-hosting
triggers:
- "Pi-hole"
- "pihole"
- "DNS blocking"
- "internet not working + Pi-hole"
- "devices can't connect + DNS"
---
# Pi-hole v6 Debugging & Management
Self-hosted Pi-hole v6 in Docker on a Linux server. Covers diagnosing DNS issues, querying the FTL database, whitelisting, and common false-positive patterns.
See `references/v6-api-quirks.md` for Pi-hole v6 CLI/API changes, `references/query-db.py` for a reusable database query script, and `references/cname-false-positive-evidence.md` for empirical evidence of CNAME false-positive blocking patterns.
## Quick Health Check
```bash
docker ps --filter name=pihole # is it running?
docker exec pihole pihole status # FTL + blocking status
dig +short google.com @<pihole-ip> # does DNS resolution work?
```
## Accessing the FTL Database
Pi-hole v6 stores queries in `/etc/pihole/pihole-FTL.db` inside the container. The host path is under `/var/lib/docker/volumes/pihole_etc/_data/`.
**Permission pitfall:** `/var/lib/docker` has `drwx--x---` permissions — regular users can't traverse it even if the volume files are user-owned. Copy the database out with sudo:
```bash
sudo cp /var/lib/docker/volumes/pihole_etc/_data/pihole-FTL.db /tmp/pihole-FTL.db
sudo cp /var/lib/docker/volumes/pihole_etc/_data/pihole-FTL.db-wal /tmp/pihole-FTL.db-wal
sudo cp /var/lib/docker/volumes/pihole_etc/_data/pihole-FTL.db-shm /tmp/pihole-FTL.db-shm
sudo chown $USER:$USER /tmp/pihole-FTL.db*
```
Then query with Python sqlite3 (the container is Alpine — no python/sqlite3 inside):
```python
import sqlite3
conn = sqlite3.connect('/tmp/pihole-FTL.db')
cur = conn.cursor()
cur.execute("SELECT datetime(timestamp,'unixepoch','localtime'), domain, client, status FROM queries ORDER BY id DESC LIMIT 30")
```
## Pi-hole v6 Status Codes
| Code | Meaning | Notes |
|------|---------|-------|
| 1 | GRAVITY_BLOCKED | Domain in adlist |
| 2 | FORWARDED | Sent to upstream DNS |
| 3 | CACHE | Answered from cache |
| 4 | REGEX_BLOCKED | Matches regex denylist |
| 5 | EXACT_BLOCKED | Exact denylist match |
| 6 | UPSTREAM_BLOCKED | Upstream returned NXDOMAIN/blocked |
| 14 | CACHED_BLOCKED | Previously blocked, cached result — **KEY FALSE-POSITIVE SIGNAL** |
| 17 | RETRIED | First attempt failed, retry succeeded — normal but high % = problem |
**Status 14 is the smoking gun for false positives.** It means a domain was cached as blocked (usually via CNAME chain inspection). Essential domains like `connectivitycheck.gstatic.com` or `dns.msftncsi.com` getting status 14 will break device connectivity checks.
## Deep CNAME Inspection False Positives
**Symptom:** Some devices work, others don't. Android TV, Windows, and IoT devices are most affected — they have strict connectivity checks that fail on first blocked DNS response.
**Root cause:** Pi-hole v6 has `CNAMEdeepInspect = true` by default. When a domain's CNAME chain passes through any blocked domain, the entire chain is blocked. Google infrastructure domains (`gstatic.com`, `l.google.com`, `googlehosted.com`, `1e100.net`) often end up in blocklists as subdomain entries, causing collateral damage.
**Diagnosis:**
```bash
# Check if deep CNAME inspection is on
docker exec pihole cat /etc/pihole/pihole.toml | grep CNAMEdeepInspect
# Find domains getting status 14 (cached false blocks)
python3 -c "
import sqlite3; conn = sqlite3.connect('/tmp/pihole-FTL.db')
cur = conn.cursor()
cur.execute('SELECT domain, count(*) FROM queries WHERE status=14 GROUP BY domain ORDER BY count(*) DESC LIMIT 20')
for d,c in cur.fetchall(): print(f'{c:5}x | {d}')
"
```
**Fix — whitelist the critical domains:**
```bash
docker exec pihole pihole allow connectivitycheck.gstatic.com
docker exec pihole pihole allow dns.msftncsi.com
docker exec pihole pihole allow ota.nvidia.com
docker exec pihole pihole allow clients3.google.com
docker exec pihole pihole allow android.apis.google.com
# Google Cast / Chromecast mtalk domains (alt1 through alt8)
docker exec pihole pihole allow alt1-mtalk.google.com
docker exec pihole pihole allow alt2-mtalk.google.com
docker exec pihole pihole allow alt3-mtalk.google.com
docker exec pihole pihole allow alt4-mtalk.google.com
docker exec pihole pihole allow alt5-mtalk.google.com
docker exec pihole pihole allow alt6-mtalk.google.com
docker exec pihole pihole allow alt7-mtalk.google.com
docker exec pihole pihole allow alt8-mtalk.google.com
# Reload DNS to flush the stale block cache
docker exec pihole pihole reloaddns
```
**Pi-hole v6 CLI change:** `pihole -w` (whitelist) no longer works — use `pihole allow` instead. `pihole -c` (chronometer) is also gone.
## Common Failure Patterns
### "Worked for a minute after restart, then died"
- If DNS queries stop entirely (check DB): device is going to sleep or Ethernet power management is killing the connection. Check device-side power/sleep settings. Android TV: enable Developer Options → Stay Awake.
- If status 14 appears on connectivity domains: CNAME inspection false positive (see above).
- If status 17 rate is >30% for a device: upstream DNS or network issue.
### Some devices have internet, some don't
- Devices bypassing Pi-hole (hardcoded 8.8.8.8) → always work
- Devices using Pi-hole via DHCP → affected by false blocks
- Check which clients are querying Pi-hole vs not: compare ARP table with Pi-hole client list
### ASUS Router DNS Director + Pi-hole = Catastrophic Overblocking
On ASUS routers (RT-AX82U, etc.), the **DNS Director / DNS Filter** feature (under LAN settings) hijacks ALL device DNS queries and redirects them through Pi-hole. Combined with Pi-hole's deep CNAME inspection blocking Google Cast domains, this breaks:
- Chromecast / Google Cast (Android TV, Shield) — `alt1-8-mtalk.google.com` blocked
- Smart home devices, IoT, game consoles — many hardcode 8.8.8.8 but DNS Director captures it
- Any device that expects unfiltered DNS for connectivity checks
**Fix options (pick one):**
1. **Whitelist the domains** (preferred — keeps ad blocking): allow `connectivitycheck.gstatic.com`, `clients3.google.com`, `alt1-mtalk.google.com` through `alt8-mtalk.google.com`, then `pihole reloaddns`.
2. **Disable DNS Director globally**: ASUS LAN → DNS Director → set global rule to "Router" or "No Filtering". Pi-hole stays running; only devices manually pointed to it use it.
3. **Per-device exemption**: DNS Director → set specific devices (Shield IP, etc.) to "No Filtering" while everyone else goes through Pi-hole.
This is a router-level issue — not a Pi-hole bug. DNS Director is the feature that forces all traffic through Pi-hole without the user realizing it.
### "pihole query returns nothing but domain resolves"
- Pi-hole v6 uses a port 4711 telnet API internally. The pihole CLI may need authentication. Direct database queries are more reliable for investigation.
## Router DHCP Setup
The ASUS router at 192.168.50.1 should have Pi-hole set as the ONLY DNS server in DHCP settings. No secondary/fallback DNS — clients will use the fallback to bypass Pi-hole entirely when the primary is slow.
@@ -0,0 +1,51 @@
# Deep CNAME Inspection False-Positive Evidence
## Reproduction (2026-06-03, Pi-hole v6.4.2 + v6.6.2 FTL)
### Test: domains caught by CNAME chain propagation
When `CNAMEdeepInspect = true`, Pi-hole follows CNAME chains and blocks the ENTIRE
chain if ANY intermediate domain is in a blocklist. The blocklist (StevenBlack
hosts) includes specific Google ad subdomains like `pagead.l.google.com` and
`ssl-google-analytics.l.google.com`, but deep CNAME inspection propagates the
block to the PARENT domains that many essential services depend on.
### Confirmed: parent domains in blocklist (via pihole -q)
```
gstatic.com → IN BLOCKLIST (subdomains: various ad-related)
l.google.com → IN BLOCKLIST (subdomains: pagead, ssl-google-analytics, etc.)
googlehosted.com → IN BLOCKLIST
1e100.net → IN BLOCKLIST
```
### Domains getting false-positive status 14 (cached-as-blocked)
| Domain | Status 14 count | What it breaks |
|---|---|---|
| dns.msftncsi.com | 514 | Windows internet connectivity check |
| ps5.np.playstation.net | 313 | PlayStation Network |
| api.weather.com | 303 | Weather apps/widgets |
| connectivitycheck.gstatic.com | 83 | Android TV connectivity check |
| google.com | 121 | Basic connectivity |
| ota.nvidia.com | 64 | Shield TV system updates |
| api.ring.com | 143 | Ring doorbell |
| graph.facebook.com | 80 | Facebook/Instagram |
### The connectivity kill chain
1. Android TV queries `connectivitycheck.gstatic.com`
2. Pi-hole follows CNAME → `some-host.l.google.com`
3. `l.google.com` matches blocklist (subdomain entries)
4. Deep CNAME inspection: entire chain → blocked
5. Pi-hole caches status 14 (blocked)
6. Android TV: "No internet" → disables network features
7. Device stops making DNS queries entirely (confirmed via FTL DB: Shield TV .24 went silent after status 14 on connectivitycheck.gstatic.com)
### Confirming the Shield TV case
Shield TV at 192.168.50.24, Ethernet-connected:
- Last DNS queries at 19:27 (connectivitycheck.gstatic.com = RETRIED, ota.nvidia.com = status 14)
- Ping: 100% packet loss (IP stack unresponsive)
- ARP: REACHABLE (Ethernet hardware alive)
- Android TV's strict connectivity check: one failed DNS → "no internet" → gives up
@@ -0,0 +1,44 @@
"""Query Pi-hole v6 FTL database from host.
Requires: the DB copied to /tmp/pihole-FTL.db (see SKILL.md for copy command).
Usage: python3 query-db.py recent|clients|status14|blocks|all
"""
import sqlite3, sys
DB = '/tmp/pihole-FTL.db'
def recent(n=30):
cur.execute("SELECT datetime(timestamp,'unixepoch','localtime'), domain, client, status FROM queries ORDER BY id DESC LIMIT ?", (n,))
for ts, dom, cl, st in cur.fetchall():
print(f"{ts} | {cl:22} | {st:2} | {dom[:60]}")
def clients(hours=1):
cur.execute("SELECT client, count(*) FROM queries WHERE timestamp > strftime('%s','now',?||' hours') GROUP BY client ORDER BY count(*) DESC", (f'-{hours}',))
for cl, cnt in cur.fetchall():
print(f" {cl:22} -> {cnt} queries")
def status14(n=20):
cur.execute("SELECT domain, count(*) as cnt FROM queries WHERE status=14 GROUP BY domain ORDER BY cnt DESC LIMIT ?", (n,))
for dom, cnt in cur.fetchall():
print(f" {cnt:5}x | {dom}")
def blocks(hours=1):
cur.execute("SELECT datetime(timestamp,'unixepoch','localtime'), domain, client FROM queries WHERE status IN (1,4,5) AND timestamp > strftime('%s','now',?||' hours') ORDER BY id DESC LIMIT 30", (f'-{hours}',))
for ts, dom, cl in cur.fetchall():
print(f" {ts} | {cl:22} | {dom}")
def all_stats():
print("=== Status code distribution (last hour) ===")
cur.execute("SELECT status, count(*) FROM queries WHERE timestamp > strftime('%s','now','-1 hour') GROUP BY status ORDER BY count(*) DESC")
for st, cnt in cur.fetchall():
print(f" {st}: {cnt}")
print("\n=== Blocked vs Allowed (last hour) ===")
cur.execute("SELECT CASE WHEN status IN (1,4,5,6) THEN 'BLOCKED' ELSE 'ALLOWED' END, count(*) FROM queries WHERE timestamp > strftime('%s','now','-1 hour') GROUP BY 1")
for lbl, cnt in cur.fetchall():
print(f" {lbl}: {cnt}")
if __name__ == '__main__':
conn = sqlite3.connect(DB)
cur = conn.cursor()
cmd = sys.argv[1] if len(sys.argv) > 1 else 'all'
{'recent': recent, 'clients': clients, 'status14': status14, 'blocks': blocks, 'all': all_stats}[cmd]()
conn.close()
@@ -0,0 +1,41 @@
# Pi-hole v6 API & CLI Quirks
## CLI Changes from v5
| v5 command | v6 equivalent | Notes |
|---|---|---|
| `pihole -w <domain>` | `pihole allow <domain>` | Whitelist/allow |
| `pihole -b <domain>` | `pihole deny <domain>` | Blacklist/deny |
| `pihole -c` | REMOVED | Use PADD instead |
| `pihole -g` | `pihole updateGravity` | Same, but `-g` still works |
## API Endpoints
| Old (v5) | New (v6) |
|---|---|
| `/admin/api.php` | `/api` |
| Query string auth | POST `/api/auth` with `{"password":"..."}` |
| No auth needed for some endpoints | Auth required for most endpoints |
## Password & Auth
- Password stored as hash in `/etc/pihole/pihole.toml``pwhash`
- Temporary CLI password at `/etc/pihole/cli_pw` (regenerated on FTL restart, limited permissions)
- Docker env `WEBPASSWORD` sets the password but is masked in `docker inspect`
- App passwords for 2FA: set `app_pwhash` in pihole.toml
## FTL Telnet API
- Port 4711 inside container (`127.0.0.1:4711`)
- Not exposed outside the container by default
- Commands like `>stats`, `>recent` work via `nc 127.0.0.1 4711` inside container
- May require authentication in newer versions
## Database Schema (v6)
Key tables:
- `queries` — all DNS queries (timestamp, domain, client, status)
- `gravity` — no longer exists as separate table; integrated differently
- `domainlist` — may not exist; allow/deny lists stored in different structure
- `network`, `network_addresses` — client tracking
- `counters` — summary stats