Zum Inhalt

Network-Wide DNS Sinkhole: Pi-hole v6 Embedded Engine, FTL Tuning & DoH/DoT Prevention

Executive Summary: Client-side browser ad blockers cannot protect IoT appliances, smart TVs, mobile background telemetry, or embedded firmware from exfiltrating behavioral metadata. This architectural guide details the production deployment of Pi-hole v6, leveraging its monolithic C-based FTL (pihole-FTL) engine—which natively embeds dnsmasq and a Civetweb HTTP/REST API server—combined with SQLite3 Gravity database optimization, VLAN-aware client group policies, and strict outbound firewall enforcement against hard-coded DNS-over-HTTPS (DoH) bypasses.

  • Pi-hole v6 Monolithic FTL


    Replace legacy Lighttpd/PHP stacks with the unified pihole-FTL daemon featuring native REST API endpoints, embedded TLS, and shared-memory query processing.

  • B-Tree Gravity Engine


    Query millions of domain blocklist entries and compiled POSIX Extended Regular Expressions (regex_blacklist) in sub-millisecond latency via SQLite3 (gravity.db).

  • VLAN Client Group Policy


    Assign distinct filtering profiles to IoT subnets, guest networks, and engineering workstations using MAC-address or CIDR-based client groups.

  • Anti-Bypass Firewalling


    Intercept hard-coded 8.8.8.8:53 queries via DNAT redirection and block outbound DoT (853/tcp) and DoH canary resolvers at the gateway perimeter.


1. Architectural Evolution: Pi-hole v6 & FTL Shared-Memory Internals

In Pi-hole v6, the deprecated Lighttpd + PHP-FPM architecture has been completely replaced. The pihole-FTL binary (Faster Than Light) compiles a customized fork of dnsmasq, a high-concurrency Civetweb HTTP/HTTPS server, and an in-memory ring buffer into a single daemon.

%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
    subgraph LANClients["Internal VLAN Subnets"]
        Workstation["Workstation VLAN 10<br/>Group: Engineering"]
        IoT["IoT Subnet VLAN 30<br/>Group: Strict Sinkhole"]
    end

    subgraph PiholeEngine["Pi-hole v6 (pihole-FTL Daemon)"]
        FTLDNS["Embedded dnsmasq<br/>UDP/TCP Port 53"]
        GravityDB[("gravity.db (SQLite3)<br/>Domains + Regex Rules")]
        SHM["POSIX Shared Memory<br/>In-RAM Query Cache"]
        FTLDB[("pihole-FTL.db<br/>Long-Term Query Log")]
    end

    subgraph UpstreamDNS["Sovereign Recursive Resolver"]
        Unbound["Local Unbound Daemon<br/>127.0.0.1:5335 (DNSSEC)"]
    end

    Workstation -->|"DNS Query A/AAAA"| FTLDNS
    IoT -->|"Telemetry Lookup"| FTLDNS
    FTLDNS <-->|"Sub-ms B-Tree Match"| GravityDB
    FTLDNS <-->|"Zero-Disk Cache"| SHM
    FTLDNS -.->|"Async Batch Commit"| FTLDB
    FTLDNS -->|"Permitted Cache Miss"| Unbound

Blocking Mode Semantics (NULL vs NXDOMAIN)

Pi-hole defaults to BLOCKINGMODE=NULL, returning 0.0.0.0 (IPv4) and :: (IPv6) for blocked domains:

Blocking Mode DNS Response Client OS Behavior Recommended Use Case
NULL (Default) 0.0.0.0 / :: Immediate socket termination (ECONNREFUSED / ENETUNREACH in < 1 ms). Production Standard: Eliminates connection timeout delays and retry storms.
NXDOMAIN RCODE=3 (Non-Existent Domain) RFC-compliant indication that the domain does not exist. Useful for strict RFC validation, though some poorly written IoT firmware retries aggressively on NXDOMAIN.
NODATA RCODE=0 with 0 Answer records Indicates domain exists, but not for the requested record type (A/HTTPS). Used internally for canary domains (use-application-dns.net).

2. Rootless / Podman Container Deployment & Host Tuning

On modern Linux distributions using systemd-resolved, port 53 on 127.0.0.53 is bound by the local stub listener. Disable the stub listener before binding Pi-hole to host port 53:

Releasing Port 53 from systemd-resolved Stub Listener
sudo mkdir -p /etc/systemd/resolved.conf.d
cat << 'EOF' | sudo tee /etc/systemd/resolved.conf.d/disable-stub.conf
[Resolve]
DNSStubListener=no
EOF
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved

Production Compose Specification (Pi-hole v6 + Unbound)

/opt/pihole/compose.yaml
services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole-ftl
    hostname: dns-sinkhole
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "127.0.0.1:8080:80/tcp"
      - "127.0.0.1:8443:443/tcp"
    environment:
      TZ: "Europe/Berlin"
      FTLCONF_webserver_api_password: "${PIHOLE_API_PASSWORD}"
      FTLCONF_dns_upstreams: "127.0.0.1#5335"
      FTLCONF_dns_dnssec: "true"
      FTLCONF_dns_listeningMode: "single"
      FTLCONF_dns_cache_size: "25000"
    volumes:
      - ./etc-pihole:/etc/pihole:Z
    cap_add:
      - NET_BIND_SERVICE
      - NET_RAW
    shm_size: "256m"

3. Advanced FTL Configuration (pihole.toml) & SD/NVMe Wear Reduction

Pi-hole v6 consolidates all legacy configuration files (setupVars.conf, pihole-FTL.conf, dnsmasq.d) into a single declarative /etc/pihole/pihole.toml file.

/etc/pihole/pihole.toml
[dns]
  upstreams = [ "127.0.0.1#5335" ]
  CNAMEdeepInspect = true
  blockESNI = true
  EDNS0ECS = true
  dnssec = true
  listeningMode = "local"

  [dns.cache]
    size = 25000
    optimizer = 3600

  [dns.rateLimit]
    count = 1500
    interval = 60

[database]
  # Commit in-memory queries to SQLite every 600s (10 min) instead of 60s to reduce SSD wear
  DBimport = true
  maxDBdays = 90
  DBinterval = 600
  useWAL = true

[misc]
  privacylevel = 0

CNAME Cloaking Deep Inspection (CNAMEdeepInspect = true)

Ad networks frequently evade simple domain blocklists by instructing website owners to delegate a first-party subdomain (e.g., metrics.example.com) via a CNAME record to a third-party tracker (tracker.adnetwork.tld). With CNAMEdeepInspect = true, pihole-FTL inspects every intermediate and terminal target in the CNAME resolution chain against gravity.db before returning the response.


4. SQLite3 Gravity Management & High-Precision Regex Filters

Instead of relying solely on the web UI, you can automate Gravity blocklist synchronization, Group Policy bindings, and compiled POSIX Regular Expressions directly via pihole-FTL sqlite3 or the v6 CLI.

Inspecting & Automating Gravity Rules via SQLite3 (/etc/pihole/gravity.db)
-- Create a dedicated IoT Strict Filtering Group
INSERT OR IGNORE INTO "group" (enabled, name, description)
VALUES (1, 'IoT_Quarantine', 'Block all telemetry and cloud phone-home endpoints');

-- Add high-value compiled Regex Blacklist entries to block Smart TV & OS telemetry
INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment) VALUES
(3, '(^|\.)(metrics|telemetry|analytics|adservice|doubleclick)\.', 1, 'Generic Telemetry Subdomains'),
(3, '^.*(acr|samsungcloudsolution|samsungelectronics|lgad)\.com$', 1, 'Smart TV ACR Fingerprinting'),
(3, '(^|\.)wpad\.', 1, 'Prevent WPAD Proxy Auto-Discovery Poisoning');

Reload the Gravity database in shared memory without restarting the DNS socket:

Compiling Gravity & Reloading FTL Lists
pihole reloadlists

5. Preventing Hard-Coded DNS & DoH/DoT Bypasses at the Firewall

Many commercial IoT devices (Chromecast, Roku, smart cameras) ignore DHCP Option 6 (DNS Server) and send UDP queries directly to 8.8.8.8:53 or attempt encrypted DNS-over-TLS (853/tcp). Enforce sovereign DNS dominance on your router via nftables:

/etc/nftables.d/dns-enforcement.nft
table inet dns_enforcement {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;

        # Intercept any rogue LAN client querying external port 53 and transparently DNAT to Pi-hole (192.168.10.2)
        iifname "lan0" ip saddr != 192.168.10.2 udp dport 53 dnat to 192.168.10.2:53
        iifname "lan0" ip saddr != 192.168.10.2 tcp dport 53 dnat to 192.168.10.2:53
    }

    chain forward {
        type filter hook forward priority filter; policy accept;

        # Block outbound DNS-over-TLS (Port 853) from all clients except the Pi-hole/Unbound host
        iifname "lan0" ip saddr != 192.168.10.2 tcp dport 853 reject with tcp reset
        iifname "lan0" ip saddr != 192.168.10.2 udp dport 853 reject
    }
}

6. Verification & Diagnostic Telemetry

Query internal FTL metrics over the local DNS chaos (CHAOS TXT) class or verify CNAME and DNSSEC resolution behavior:

Verifying Sinkhole Resolution, Cache Hit Ratio & DNSSEC
# 1. Verify blocked domain returns 0.0.0.0 in < 1ms
dig @127.0.0.1 doubleclick.net A +short

# 2. Query live FTL cache metrics via CHAOS TXT records
dig +short chaos txt cachesize.bind @127.0.0.1
dig +short chaos txt hits.bind @127.0.0.1
dig +short chaos txt misses.bind @127.0.0.1

# 3. Verify DNSSEC validation fails on intentionally broken signatures (SERVFAIL expected)
dig @127.0.0.1 sigfail.verteiltesysteme.net A