Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG keys, zones of every type, records, named.conf options) in SQLite and renders named.conf, keys.conf and zone files from it. Every change goes through one changeset pipeline: whole-configuration validation, named-checkconf -z and named-checkzone in a staging directory, atomic install, rndc reconfig/reload, serial verification with rndc zonestatus, and rollback if named does not load the change. Changes apply one at a time by default or are bundled and applied together. Applied changesets can be reverted. - jarld: the daemon with the REST API and an embedded web UI, plus import, adopt, check, resync, backup and local user recovery commands - jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh - users with argon2id passwords and CSRF-protected sessions; hashed API tokens with granular grants and roles; an audit log - systemd unit, sysusers.d/tmpfiles.d files, Makefile - unit tests, integration tests against a real named, scripts/e2e.sh - docs/ guides, README and CHANGELOG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.8 KiB
Developing Jarl
Layout
cmd/jarld/ daemon: serve, init, import, adopt, check, render, resync, backup, user
cmd/jarl/ CLI
internal/
model/ configuration objects (State: views, ACLs, keys, zones, records, options) and changesets
dnsname/ DNS name handling: FQDNs, relative names, check-names rules, reverse zones
rr/ record data parsing/canonicalization (miekg/dns), TXT quoting, the record type catalog
namedconf/ named.conf lexer, parser and printer; address match lists
catalog/ every BIND 9.20 statement: grammar (from named.conf(5)) + ARM descriptions, embedded
validate/ whole-configuration validation and security lints
render/ State → named.conf, keys.conf and zone files, with a source map for error messages
bind/ named-checkconf, named-checkzone and rndc wrappers
engine/ changesets: expand requests into ops, replay, validate, render, check, deploy, reload, verify, revert;
import and adopt
importer/ reads an existing named.conf tree and its zone files
store/ SQLite (modernc.org/sqlite, no cgo): state, changesets, users, tokens, sessions, audit
auth/ argon2id passwords, tokens, sessions, grants and roles
server/ HTTP API, authentication, the web UI
web/static/ the web UI: plain ES modules, no build step, embedded with go:embed
api/, client/ JSON types and the Go client shared by server and CLI
config/ jarld's YAML configuration
deploy/ systemd unit, sysusers.d and tmpfiles.d files
contrib/acme.sh/ acme.sh DNS API hook
scripts/e2e.sh end-to-end test against a real named
The main flow is in internal/engine/engine.go (applyOps):
expand → replay → validate → render → check → install → reload → verify →
record. concepts.md describes it from the outside.
Building and testing
make # bin/jarld, bin/jarl (CGO_ENABLED=0, version from ./VERSION)
make test # go vet, gofmt check, go test ./...
make e2e # scripts/e2e.sh
Tests against real BIND
The render, engine and adopt tests run the real named-checkconf,
named-checkzone, named, rndc and dig when they can find them, and
skip otherwise. They are found through JARL_BIND_ROOT (an extracted
package tree) or on the system. To get them without root or AppArmor in the
way, extract the packages:
mkdir -p ~/bind-root/debs && cd ~/bind-root/debs
apt-get download bind9 bind9-utils bind9-libs bind9-dnsutils \
liblmdb0 libmaxminddb0 liburcu8t64 libuv1t64 libjemalloc2 libjson-c5 libnghttp2-14
for f in *.deb; do dpkg -x "$f" ~/bind-root; done
cd -
JARL_BIND_ROOT=~/bind-root make test
JARL_BIND_ROOT=~/bind-root make e2e
The tests start named unprivileged on random high ports in temporary
directories. On Debian/Ubuntu, AppArmor confines the installed
/usr/sbin/named to BIND's own directories, so the tests that start
named (engine integration tests, make e2e) need the extracted copy. The
checker tests work with the installed bind9-utils too.
scripts/e2e.sh builds a legacy BIND setup, starts named, runs
jarld init, import, adopt and serve, then drives every CLI command
(records, batches, changesets, views, ACLs, TSIG, options, users, tokens,
ACME hooks, audit) and checks the answers with dig. KEEP=1 leaves named
and jarld running afterwards. The output says where; set JARL_CLI_CONFIG
to that directory's cli.yaml to keep using the CLI against it.
The web UI
The UI is plain JavaScript modules under internal/web/static/, with no
framework and no build step. All DOM is built with the h() helper in
ui.js, which never uses innerHTML, so record data and comments can't
inject markup. The Content-Security-Policy only allows the app's own
scripts and styles. To work on it, run jarld and reload the browser: the
files are embedded at build time, so rebuild jarld after changes.
Conventions
- Every user-visible change goes into
CHANGELOG.md(under "Unreleased"), and the docs indocs/are updated in the same change. - Errors that reach users say what is wrong and how to fix it, naming the object.
- New validation rules get a stable
code, a test ininternal/validate/validate_test.go, and a row in validation.md. - New API operations go through the engine (
internal/engine/expand.go,ops.go) so they get permissions, validation, history and revert for free, and are documented in api.md. gofmtandgo vetclean (make vet).
Releasing
- Move the "Unreleased" section of
CHANGELOG.mdunder the new version and date. - Put the version in
VERSION. make test && JARL_BIND_ROOT=... make e2e && make dist.