Ground rules, building and testing with real BIND, where things live, web UI rules, BIND gotchas, and the git and release workflow. docs/development.md lists the packages its layout was missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9.2 KiB
Working on Jarl
Notes for coding agents (and people) changing this repository. Read this first. For more depth, see docs/development.md for layout, building, tests and releasing, and 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.confand 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 inexpand.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.mdunder## [Unreleased], and the matching page indocs/in the same commit. Docs that may need it:cli.md,api.mdandconfiguration.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 indocs/validation.md. -
Schema changes append a migration to the
migrationsslice ininternal/store/store.go, and never edit an old one. The statistics database has its own list,schemaininternal/stats/store.go. Add a note under "Upgrading" in the CHANGELOG when a downgrade would break. -
make testrunsgo vetand agofmt -lcheck, and both must pass.goimportsisn't installed, so fix imports by hand.
Building and testing
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 downloadand unpack them withdpkg -xinto a directory; that directory isJARL_BIND_ROOT. The package list is in docs/development.md, under "Tests against real BIND". WithoutJARL_BIND_ROOTthe integration tests skip, so a greenmake testalone 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=1leaves named and jarld running; the last line of the log has their PIDs.WORK=DIRmust name an empty directory.
- Certificate tests run against Pebble in Docker, unless you set
PEBBLE=0. Ifdocker infofails, the Pebble block is silently skipped; on this machine it needsDOCKER_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. Runmake deb, then install it in adebian:trixiecontainer. That container has BIND 9.20; run it with--privileged /sbin/initwhen 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.goturns requests into ops.linked.go: linked zones, kept in step across views bylinkSync.reverse.go: PTR fill, auto-PTR and reverse zone suggestions.compare.go: compare views.exposure.go: fixes for internet exposure.
- Validation:
internal/validateholds all checks and lints.internal/catalogis the BIND 9.20 grammar and ARM text for every statement. Use it rather than hard-coding option knowledge. - Rendering:
internal/renderwrites the configuration.generated.goholds the statements Jarl adds itself, shown to users as "Added by Jarl". - Server:
internal/serverhas oneh_*.gofile per API area; routes are inroutes.go. The JSON types are ininternal/api, shared with the Go client ininternal/client. - CLI:
cmd/jarlis built on cobra.complete.gocompletes the placeholders in usage lines (ZONE,VIEW, …), so name them consistently. - Statistics:
internal/statsis 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;certsandacme: 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-memoryFake. 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()fromassets/ui.js. Never useinnerHTMLorinsertAdjacentHTML. That's how record data can't inject markup. -
The CSP is
selfonly. 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 withel.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 inapp.jsand the API calls are inapi.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 adnssec-policy. Validation refuses it. named-checkconfdoesn't catch a TTL abovemax-zone-ttl, but named then refuses to load the zone.responselogonly takes effect when named starts; userndc responselog on|offinstead.named -gsends 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 reconfigmakes 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.Zbranches (for exampledev-0.6.1) offmain. Don't name a branch like a tag (v0.5.2):git pushthen 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 testand the e2e run must pass. - Check the notes with
VERSION=X scripts/gitea-release.sh notes.
- It sets
- Pushing a
v*tag runs.gitea/workflows/release.yml. It builds the tarballs, the CLI for each OS and the.debs, and publishes them. - Before a minor release, test an upgrade from the previous release's
.debs in a container. Check data, history, answers from named, and that an old backup restores.
Shell hygiene
- Never kill processes by pattern with
pkill -forpgrep -f … | xargs killin the same shell command. The pattern matches the shell running it, and kills that shell. Find the PID withpsand 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.