diff --git a/README.md b/README.md index 6f42dec..5eeb546 100644 --- a/README.md +++ b/README.md @@ -210,14 +210,19 @@ another way in. sudo nix-store --verify --check-contents # add --repair to fix ``` A card that has corrupted one path will corrupt more. Replace it and reflash. -- **A reflash wipes `/var/lib/pihole`**, which is NOT declarative. The - `FTLCONF_*` env vars rebuild the DNS/DHCP config, but the gravity database - goes with it — so adlists and blocking are gone and resolution silently - keeps working with nothing blocked (`Blocked DNS queries: 0` in the logs is - the tell). Re-add the adlists, then: +- **A reflash wipes `/var/lib/pihole`**, taking the gravity database with it. + The blocklists themselves are declared in `services/network/pihole.nix`, and + the `pihole-adlists` unit re-seeds them on boot and rebuilds gravity when it + finds it empty — so this heals itself, but the first boot after a reflash + spends several minutes downloading lists. Query history and dynamic DHCP + leases are genuinely lost (static leases are declarative). Check with: ``` - sudo podman exec pihole pihole -g + systemctl status pihole-adlists + sudo podman exec pihole pihole-FTL sqlite3 /etc/pihole/gravity.db \ + "SELECT address,enabled FROM adlist; SELECT COUNT(*) FROM gravity;" ``` + `Blocked DNS queries: 0` in the pihole logs means gravity is empty — DNS + resolves fine, nothing is filtered. - mercury's own `resolv.conf` is deliberately public resolvers, not its own pihole (`resolveLocalQueries = false`, see `CLAUDE.md`) — so `.sol` names do not resolve *on mercury itself*. That is expected, not a fault. diff --git a/services/network/pihole.nix b/services/network/pihole.nix index 16c34a5..e14ead9 100644 --- a/services/network/pihole.nix +++ b/services/network/pihole.nix @@ -1,5 +1,15 @@ { pkgs, lib, ... }: +let + # Blocklists, kept here so a reflash restores them. /var/lib/pihole is NOT + # declarative: the gravity database lives there and goes with the card, and + # the failure is quiet — DNS keeps resolving, just with nothing blocked. + adlists = [ + "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" + "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/pro.txt" + ]; +in + # Pi-hole via the official container (the native nixpkgs pihole-ftl module # segfaults on aarch64 / Pi 3B+). Host networking so it can serve DHCP and reach # the host's unbound at 127.0.0.1:5335. Config via FTLCONF_* env vars (pihole v6) @@ -43,8 +53,59 @@ }; }; - # Bind-mount source must exist (podman won't create it). - systemd.tmpfiles.rules = [ "d /var/lib/pihole 0755 root root -" ]; + # Bind-mount source must exist (podman won't create it), and it must be + # owned by 1000 — the `pihole` user FTL drops to after the entrypoint's root + # phase. Podman here is rootful with no userns remapping, so that number is + # the same inside and out (on the host it collides with darman, harmlessly). + # + # Ownership of gravity.db alone is not enough: sqlite creates a sibling + # gravity.db-journal for every write transaction, so FTL needs to CREATE + # files in this directory. Root-owned, it fails with + # open(/etc/pihole/gravity.db-journal) - (14) + # attempt to write a readonly database + # which reads like a corrupt or read-only database and is neither. + systemd.tmpfiles.rules = [ "d /var/lib/pihole 0750 1000 1000 -" ]; + + # Seed the adlists above into gravity. `INSERT OR IGNORE` keyed on the URL + # makes this idempotent, so it is safe on every boot; the expensive rebuild + # (`pihole -g`, which downloads every list) only runs when gravity is empty, + # i.e. after a reflash. Add a list above and run `pihole -g` by hand. + systemd.services.pihole-adlists = { + description = "Seed pihole's blocklists from the Nix config"; + after = [ "podman-pihole.service" "network-online.target" ]; + wants = [ "network-online.target" ]; + requires = [ "podman-pihole.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + podman="${pkgs.podman}/bin/podman" + db=/etc/pihole/gravity.db + sql() { $podman exec pihole pihole-FTL sqlite3 "$db" "$1"; } + + # The container creates gravity.db on first start; wait for it. + for _ in $(seq 1 60); do + $podman exec pihole test -f "$db" && break + sleep 2 + done + $podman exec pihole test -f "$db" || { + echo "gravity.db never appeared; is podman-pihole healthy?" >&2 + exit 1 + } + + ${lib.concatMapStringsSep "\n" (url: '' + sql "INSERT OR IGNORE INTO adlist (address, enabled, comment) + VALUES ('${url}', 1, 'declared in services/network/pihole.nix');" + '') adlists} + + if [ "$(sql 'SELECT COUNT(*) FROM gravity;')" = "0" ]; then + echo "gravity is empty — building blocklists (this downloads every list)" + $podman exec pihole pihole -g + fi + ''; + }; networking.firewall.allowedTCPPorts = [ 53 80 ]; networking.firewall.allowedUDPPorts = [ 53 67 ];