bbrkn/CLAUDE.md
goodvin 8598a1a48f
All checks were successful
Deploy DNS Configuration / deploy (push) Successful in 1h15m4s
warm: self-heal wedged dnsmasq, run after deploy, shared state dir
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
2026-10-13 21:26:49 +03:00

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 via 92-resolve-bbrkn.conf (server=/domain/127.0.0.1#5350) to a host dnsmasq on :5350, which resolves via VPN and applies 90-nftset.conf (nftset=…#bbrkn_v4,…#bbrkn_v6) to populate nft sets inet 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 by wg-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 under API FAILURES). Invariant: every domain in domains.txt reaches 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 than CACHE_TTL_DAYS; corrupt or expired entries refetch. find -mtime +N decides freshness.
  • Ordering pass emits each base domain, a # domain — N subdomains comment, then its not-yet-written related domains; a related domain is attributed to whichever base comes first alphabetically. A final uniqueness check exit 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; default NFTSET_SPEC=4#inet#filter#bbrkn_v4,6#inet#filter#bbrkn_v6) and server=/<domain>/$DNS_SERVER (RESOLVE_CONF → 92-resolve-bbrkn.conf; DNS_SERVER default 127.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=0 on the :5350 instance and the hourly warmer (/etc/cron.d/bbrkn-warm → /usr/local/sbin/bbrkn-warm, a manually installed copy of scripts/warm-nftset.sh). The :5350 main process can also wedge (nftset … cache initialization failed: Protocol error on every add until restart); the warmer detects that via journalctl _PID= and restarts it.
  • domains.txt is the primary data file; commits are typically domain additions. Run make check after editing it.
  • IGNORE_PARTS is 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 as 127.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 bash 4.2+ (associative arrays, ${var,,}), curl, jq, docker, nft, systemctl; dig/nslookup optional (health checks skipped if absent).