diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..70d73bb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,197 @@ +# Working on Jarl + +Notes for coding agents (and people) changing this repository. Read this +first. For more depth, see [docs/development.md](docs/development.md) for +layout, building, tests and releasing, and [docs/concepts.md](docs/concepts.md) +for how Jarl works from the outside. + +Jarl manages a BIND 9.20 server. It has three parts: + +* `jarld`, a daemon with an HTTP API, an embedded web UI and SQLite storage; +* `jarl`, a CLI for that API; +* a Debian package. + +It's all Go: module path `jarl`, version in `go.mod`, `CGO_ENABLED=0`. The +SQLite driver is `modernc.org/sqlite`, so there is no cgo. The target is +Debian/Ubuntu with BIND 9.20, using Debian's paths (`/etc/bind`, the `bind` +group, AppArmor). + +## Ground rules + +* **The database is the source of truth.** Jarl renders `named.conf`, + `keys.conf` and the zone files from it. Never hand-edit rendered output or + add code that reads BIND's files back as state; the importer is the only + exception. +* **Every change goes through the engine** (`internal/engine`). An API + request becomes ops in `expand.go`/`ops.go`. The engine then runs + replay → validate → render → `named-checkconf`/`named-checkzone` → + atomic install → `rndc reload` → verify serials, and rolls back on + failure. Going through the engine gives a change permissions, dry runs, + changesets, history and undo for free, so don't write to the store + directly from handlers. +* **Every user-visible change updates `CHANGELOG.md`** under + `## [Unreleased]`, and the matching page in `docs/` in the same commit. + Docs that may need it: + + * `cli.md`, `api.md` and `configuration.md`; + * `validation.md`, for new validation codes; + * `metrics.md`; + * the feature's own page. + + Write docs in plain, direct English, like the existing pages. +* **Error messages** say what is wrong and how to fix it, naming the object. + For example: `view external is reachable from the internet …; set + minimal-any yes`. +* **New validation rules** get a stable `code`, a test, and a row in + `docs/validation.md`. +* **Schema changes** append a migration to the `migrations` slice in + `internal/store/store.go`, and never edit an old one. The statistics + database has its own list, `schema` in `internal/stats/store.go`. Add a note + under "Upgrading" in the CHANGELOG when a downgrade would break. +* `make test` runs `go vet` and a `gofmt -l` check, and both must pass. + `goimports` isn't installed, so fix imports by hand. + +## Building and testing + +```sh +make # bin/jarld, bin/jarl, bash completion +make test # vet + gofmt check + go test ./... +JARL_BIND_ROOT=DIR make test # also runs the integration tests against real BIND +JARL_BIND_ROOT=DIR make e2e # scripts/e2e.sh: real named + jarld, drives every CLI command +``` + +* **Real BIND without root.** Get the packages with `apt-get download` and + unpack them with `dpkg -x` into a directory; that directory is + `JARL_BIND_ROOT`. The package list is in docs/development.md, under "Tests + against real BIND". Without `JARL_BIND_ROOT` the integration tests skip, + so a green `make test` alone proves less than it looks. +* **Run e2e after any engine, render, CLI or server change.** It prints a + pass count, such as `606/606`; a failure prints the command and its + output. + * It uses fixed ports: DNS 25353, rndc 25953, HTTP 28053, lists 28054 and + metrics 28055. Stop anything left running on them first. + * `KEEP=1` leaves named and jarld running; the last line of the log has + their PIDs. + * `WORK=DIR` must name an empty directory. +* **Certificate tests** run against Pebble in Docker, unless you set + `PEBBLE=0`. If `docker info` fails, the Pebble block is silently skipped; + on this machine it needs `DOCKER_HOST=unix:///var/run/docker.sock`. +* **Unix socket paths** are limited to 107 bytes. Put test sockets under + `/run/user/$UID/`, not under a deep temporary directory. +* **The systemd sandbox isn't tested.** e2e runs outside systemd, so bugs + that only appear in the sandbox (`RestrictAddressFamilies`, + capabilities, read-only paths) need a package test. Run `make deb`, then + install it in a `debian:trixie` container. That container has BIND 9.20; + run it with `--privileged /sbin/init` when you need the real unit. + +## Layout and where things live + +docs/development.md has the full layout. Some places that are easy to miss: + +* **Engine** (`internal/engine`): + * `expand.go` turns requests into ops. + * `linked.go`: linked zones, kept in step across views by `linkSync`. + * `reverse.go`: PTR fill, auto-PTR and reverse zone suggestions. + * `compare.go`: compare views. + * `exposure.go`: fixes for internet exposure. +* **Validation**: `internal/validate` holds all checks and lints. + `internal/catalog` is the BIND 9.20 grammar and ARM text for every + statement. Use it rather than hard-coding option knowledge. +* **Rendering**: `internal/render` writes the configuration. + `generated.go` holds the statements Jarl adds itself, shown to users as + "Added by Jarl". +* **Server**: `internal/server` has one `h_*.go` file per API area; routes + are in `routes.go`. The JSON types are in `internal/api`, shared with the + Go client in `internal/client`. +* **CLI**: `cmd/jarl` is built on cobra. `complete.go` completes the + placeholders in usage lines (`ZONE`, `VIEW`, …), so name them + consistently. +* **Statistics**: `internal/stats` is query statistics, kept in a + **separate** SQLite file, `stats.db`, with its own migrations. +* **Feature packages**: each has its own doc page. + * `encdns`: encrypted DNS; + * `certs` and `acme`: certificates and ACME; + * `blocking`: response policy zones; + * `dnssec`; + * `nscheck`: queries to other name servers; + * `mailcheck`: mail reverse DNS; + * `totp`: two-factor login; + * `backup`; + * `porkbun`: registrar API, with an in-memory `Fake`. Tests never call + the real API. + +## Web UI rules + +`internal/web/static/` holds the UI: plain ES modules, no framework, no +build step. The files are embedded with `go:embed`. + +* **Rebuild jarld after editing them**, or you'll be testing the old files. +* **Build DOM only with `h()`** from `assets/ui.js`. Never use `innerHTML` + or `insertAdjacentHTML`. That's how record data can't inject markup. +* **The CSP is `self` only.** That rules out: + * inline scripts; + * `style="…"` attributes and inline style strings (`el.setAttribute("style", …)`); + * external fonts and CDNs. + + Use a CSS class in `style.css`, or set individual properties with + `el.style.prop = …` (CSSOM is allowed). +* **Colors are CSS custom properties** with light and dark values. Reuse the + existing tokens. +* **Pages live in `assets/pages/`**, one file per route. The router is in + `app.js` and the API calls are in `api.js`. +* **What the user wants:** functionality and ease of use matter more than + visual polish. In pick lists, leave every checkbox unticked by default. + +There's no JS test suite. To catch syntax and import errors without a +browser, bundle `assets/app.js` with esbuild, or load the page in headless +Chrome. + +## BIND facts that bit us + +* named 9.20 crashes on an empty `keys {}` in a `dnssec-policy`. Validation + refuses it. +* `named-checkconf` doesn't catch a TTL above `max-zone-ttl`, but named then + refuses to load the zone. +* `responselog` only takes effect when named starts; use `rndc responselog + on|off` instead. +* `named -g` sends all logging to stderr, so tests use `-f`. +* RPZ client-IP rules see IPv4 clients as v4-mapped (`::ffff:0:0/96`), so + IPv6 prefix covers must leave that range out. +* named serves DoH in RFC 8484 wire format only, not the JSON API. Test it + with BIND's `dig +https` / `dig +tls`, not curl or kdig. +* named can't forward over DoH, only over DoT (`forwarders port 853 tls NAME`). +* `rndc reconfig` makes named reload renewed TLS certificate files. + +## Git and releases + +These are the maintainer's rules; follow them unless told otherwise: + +* **Commit only when asked. Never push**; the maintainer pushes, to Gitea + at gitea.adbraun.lol. +* **Work on `dev-X.Y.Z` branches** (for example `dev-0.6.1`) off `main`. + Don't name a branch like a tag (`v0.5.2`): `git push` then fails with + "src refspec matches more than one". +* **A release** is a commit titled `Release X`. + * It sets `VERSION`. + * It updates the status line in the README. + * For a final release, it moves `[Unreleased]` to + `## [X] - YYYY-MM-DD`. A release candidate (`X-rc.N`) may leave the + notes under `[Unreleased]`. + * Tag it annotated: `git tag -a vX -m "Jarl X"`. + * Before tagging, `make test` and the e2e run must pass. + * Check the notes with `VERSION=X scripts/gitea-release.sh notes`. +* **Pushing a `v*` tag** runs `.gitea/workflows/release.yml`. It builds the + tarballs, the CLI for each OS and the `.deb`s, and publishes them. +* **Before a minor release**, test an upgrade from the previous release's + `.deb`s in a container. Check data, history, answers from named, and that + an old backup restores. + +## Shell hygiene + +* **Never kill processes by pattern** with `pkill -f` or + `pgrep -f … | xargs kill` in the same shell command. The pattern matches + the shell running it, and kills that shell. Find the PID with `ps` and + kill it by number. +* **Start long-running test servers in the background** (your tool's + background mode, not `&` in a shell that exits), so they outlive the + command. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6096346 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +@AGENTS.md + +## Claude Code specifics + +- End commit messages with a `Co-Authored-By:` trailer naming the model, + e.g. `Co-Authored-By: Claude Opus 5.5 `. +- Keep temporary scripts, extracted BIND trees, screenshots and test + servers' work directories in the session scratchpad, not in the repo. diff --git a/docs/development.md b/docs/development.md index 3596ddc..04de83d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -32,6 +32,12 @@ internal/ blocking/ blocklist parser, response policy zone writers, and matching a name the way named does (the downloads are in engine/blocklists.go) config/ jarld's YAML configuration + certs/ certificates: Jarl as an ACME client (lego), renewal, files for named + encdns/ encrypted DNS (DoT/DoH) setup, status and health checks + mailcheck/ reverse DNS check of the addresses mail is sent from + totp/ two-factor login codes and recovery codes + version/ what a build reports (release tag, commit, commits since a tag) + testutil/ test helpers, chiefly finding BIND's tools (JARL_BIND_ROOT) deploy/ systemd units (jarld, the daily backup service and timer), sysusers.d and tmpfiles.d files deploy/debian/ the Debian package's control template and postinst/prerm/postrm contrib/acme.sh/ acme.sh DNS API hook