initial commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user