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
@@ -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