dnsmasq adds IPs to an nft set only on a fresh upstream reply; a cache hit never refreshes the set. On Archie the :5350 instance also wedged: TCP child processes first, then the main process, logged 'nftset ... netlink: Error: cache initialization failed: Protocol error' on every add and stopped filling bbrkn_v4/v6 until restarted. warm-nftset.sh now counts those errors for the main PID during the run (journalctl _PID=) and, if any, restarts the service and warms again; exit 1 if it still fails. The state dir defaults to /var/lib/bbrkn so the root cron job and the deploy share one lock (LOCK_WAIT lets deploy wait). deploy-to-gateway.sh runs the warmer right after flushing the sets, since clients keep old IPs cached and would bypass the tunnel until the next fresh resolve. A warm failure only warns; it does not roll back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DR7zpSsdX28T4Kn6v4jPSS
6.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Generates Pi-hole / dnsmasq configs that route blocked domains through a WireGuard VPN via nftables sets. Input is domains.txt; output is two dnsmasq files deployed to two different dnsmasq instances on the Archie gateway (see Architecture). README.md (Russian) is the authoritative reference — read it for the classification table, env-var descriptions, and report format.
Routing model (Archie gateway): client → pihole (docker,
:53) delegates bbrkn domains via92-resolve-bbrkn.conf(server=/domain/127.0.0.1#5350) to a host dnsmasq on:5350, which resolves via VPN and applies90-nftset.conf(nftset=…#bbrkn_v4,…#bbrkn_v6) to populate nft setsinet filter bbrkn_v4/bbrkn_v6. An nft mangle chain marks packets to those set IPs (0x1/0x2) and policy-routing sends them out a WireGuard exit. Dual-exit DPI-bypass failover (10.77.1.2/10.77.2.2) is handled bywg-ha.service, external to bbrkn — bbrkn only maintains domain→set membership.
Commands
make all # generate + deploy (full cycle)
make generate # generate configs into /tmp only
make deploy # deploy existing configs to Pi-hole
make check # validate domains.txt syntax (regex per line)
make clean # remove generated /tmp configs
make cache-clean # wipe .cache/api/
./scripts/generate-configs.sh --dry-run # generate, print report, write nothing
DEBUG=1 make generate # verbose log to $DEBUG_LOG
STRICT_MODE=1 make generate # exit 1 if any domain's API query failed
CACHE_TTL_DAYS=0 make generate # bypass API cache for one run
No test suite. Verification = --dry-run + reading the DEBUG REPORT (see README) and diffing generated /tmp configs.
Architecture
Two bash scripts, driven by env vars (defaults in each script's header, overridden by Makefile and CI):
scripts/generate-configs.sh — classifies each domain by querying a Chromium headless API ($CHROME_SERVER/domains?domain=), builds the ordered domain list, writes both config files in one batch. Key logic:
- Classification (drives whether subdomains are added): API JSON with
relatedDomains→ site (base + related subdomains);ERR_NAME_NOT_RESOLVED/Timeout→ dead, skipped entirely; any other API error → service (base only, no subdomains); API fully unreachable → fallback (base only, recorded underAPI FAILURES). Invariant: every domain indomains.txtreaches the output unless it's dead. - State is held in bash associative arrays (
DOM_ROLE,EXPANDED,SOURCES,SITE_RELATED, …). The main loop deliberately avoids subshells so counters survive — do not wrap counter-mutating code in$(...)or pipes. - Caching: successful API responses cached to
$CACHE_DIR/<domain>.json, reused while newer thanCACHE_TTL_DAYS; corrupt or expired entries refetch.find -mtime +Ndecides freshness. - Ordering pass emits each base domain, a
# domain — N subdomainscomment, then its not-yet-written related domains; a related domain is attributed to whichever base comes first alphabetically. A final uniqueness checkexit 5s (config NOT written) if any domain would appear twice — treat that as a real bug in the ordering logic, not a data issue. - Output lines:
nftset=/<domain>/$NFTSET_SPEC(NFTSET_CONF →90-nftset.conf; defaultNFTSET_SPEC=4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6) andserver=/<domain>/$DNS_SERVER(RESOLVE_CONF →92-resolve-bbrkn.conf;DNS_SERVERdefault127.0.0.1#5350, the host resolver pihole delegates to).
scripts/deploy-to-gateway.sh — deploys to two targets: 90-nftset.conf → host $NFTSET_TARGET_DIR (/etc/dnsmasq.d, the :5350 instance) and 92-resolve-bbrkn.conf → pihole $RESOLVE_TARGET_DIR (/opt/appdata/pihole/etc/dnsmasq.d). Flow: timestamped-backup both → copy → systemctl restart $HOST_DNSMASQ_SVC + docker restart $DOCKER_CONTAINER (a full restart is required — dnsmasq SIGHUP does not re-read nftset=/server= directives, only hosts files) → wait for :5350 listening + pihole running → nft flush set on bbrkn_v4/bbrkn_v6 → DNS health check via pihole :53 → end-to-end nftset-capture check (resolve a bbrkn domain via :5350, assert the set populated). Then it runs warm-nftset.sh (non-fatal) to refill the just-flushed sets. Backups go to $BACKUP_DIR (/var/backups/bbrkn, newest BACKUP_KEEP=10) — never into a dnsmasq conf dir, which would load them as live config. On any failure it calls rollback(), restoring both backups and restarting both instances. Edits must preserve that rollback path — a broken deploy can break DNS for every client on the gateway.
Deployment / CI
.forgejo/workflows/deploy.yaml runs on push to main (and workflow_dispatch) on a self-hosted forgejo-runner on the gateway itself — no SSH, deploy is local. Note: the runner is ephemeral (no persisted .cache/api was found on the host despite the older "persists workspace" claim), so a fresh CI run performs a cold gekata crawl of every domain (~30-70s each, serialized) — the first run after a cache reset is slow and heavy on gekata. CI runs make clean && make all and uploads the generated configs + debug log as artifacts even on failure. CI-only values (NFTSET_TARGET_DIR, TARGET_DIR, CHROME_SERVER, DOCKER_CONTAINER, HOST_DNSMASQ_SVC, DNS_SERVER, IGNORE_PARTS) live in the workflow env: block, not the scripts.
Gotchas
- dnsmasq only adds IPs to an nft set on a fresh upstream reply, never on a cache hit — hence
cache-size=0on the:5350instance and the hourly warmer (/etc/cron.d/bbrkn-warm→/usr/local/sbin/bbrkn-warm, a manually installed copy ofscripts/warm-nftset.sh). The:5350main process can also wedge (nftset … cache initialization failed: Protocol erroron every add until restart); the warmer detects that viajournalctl _PID=and restarts it. domains.txtis the primary data file; commits are typically domain additions. Runmake checkafter editing it.IGNORE_PARTSis space-separated substrings; any domain (base or related) containing one is dropped.#gotcha:DNS_SERVER=127.0.0.1#5350—#starts a comment in Makefiles (escape as127.0.0.1\#5350) and can start one in YAML (quote it). The scripts themselves take the plain value.- The nft sets (
bbrkn_v4/bbrkn_v6,inet filter) and mangle/routing rules live on the gateway (/etc/nftables.d/, Ansible-managed) — bbrkn only fills the sets via dnsmasq, it does not define them. - Requires
bash4.2+ (associative arrays,${var,,}),curl,jq,docker,nft,systemctl;dig/nslookupoptional (health checks skipped if absent).